From 3958a284a48f8c67fb56d54e8b52e1c7b6b25261 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Mon, 31 Aug 2026 22:09:19 +0100 Subject: [PATCH] feat(dashboard-list): add Storybook stories (issue #271) - Add DashboardList.stories.tsx covering loading, error, empty, single/multiple jobs, expanded panels, role badges, and large milestone counts --- app/components/DashboardList.stories.tsx | 498 +++++++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 app/components/DashboardList.stories.tsx diff --git a/app/components/DashboardList.stories.tsx b/app/components/DashboardList.stories.tsx new file mode 100644 index 0000000..6e7c97b --- /dev/null +++ b/app/components/DashboardList.stories.tsx @@ -0,0 +1,498 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; +import LoadingSkeleton from "./LoadingSkeleton"; +import EmptyStateCard from "./EmptyStateCard"; +import MilestoneCard from "./MilestoneCard"; + +// Mock job data for stories +const mockJob = (overrides = {}) => ({ + id: "job-1234567890abcdef", + client: "GCLIENTADDRESS1234567890123456789012345678901234", + freelancer: "GFREELANCERADDRESS12345678901234567890123456", + arbiter: "GARBITERADDRESS12345678901234567890123456789012", + funded: true, + milestones: [ + { index: 0, amount: "250000000", status: "Pending" }, + { index: 1, amount: "500000000", status: "Delivered" }, + ], + tokenSymbol: "USDC", + tokenDecimals: 7, + ...overrides, +}); + +const mockJobs = [ + mockJob({ id: "job-1", funded: true }), + mockJob({ id: "job-2", funded: false }), + mockJob({ id: "job-3", funded: true, milestones: [] }), +]; + +const mockActions = { + partialReleaseState: { phase: "idle" as const, error: null, txHash: null }, + claimAutoReleaseState: { phase: "idle" as const, error: null, txHash: null }, + isPartialReleasePending: false, + isClaimAutoReleasePending: false, + onMarkDelivered: fn(), + onApprove: fn(), + onDispute: fn(), + onPartialRelease: fn(), + onClaimAutoRelease: fn(), + onResolveDispute: fn(), +}; + +// Mock components for story isolation +const MockDashboardList = ({ jobs = mockJobs, loading = false, error = null, expandedJobId = null, onExpand = fn() }) => { + const mockAddress = "GCLIENTADDRESS1234567890123456789012345678901234"; + + if (loading) return ; + + if (error) { + return ( +
+

Error loading jobs

+

{error}

+
+ ); + } + + if (!jobs.length) { + return ( + + ); + } + + return ( +
+
+ {jobs.map((job, index) => { + const isExpanded = expandedJobId === job.id; + const roleBadges = [ + mockAddress === job.client ? "Client" : null, + mockAddress === job.freelancer ? "Freelancer" : null, + mockAddress === job.arbiter ? "Arbiter" : null, + ].filter(Boolean) as string[]; + + return ( +
+ + + {isExpanded && ( +
+ {job.milestones?.length ? ( + job.milestones.map((m) => ( + + )) + ) : ( + + )} +
+ )} +
+ ); + })} +
+
+ ); +}; + +const meta = { + title: "Components/DashboardList", + component: MockDashboardList, + tags: ["autodocs"], + parameters: { + layout: "padded", + backgrounds: { + default: "dark", + values: [ + { name: "dark", value: "#030712" }, + { name: "light", value: "#ffffff" }, + ], + }, + }, + argTypes: { + loading: { control: "boolean", description: "Show loading skeleton" }, + error: { control: "text", description: "Error message to display" }, + expandedJobId: { control: "text", description: "ID of expanded job" }, + onExpand: { action: "expanded" }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// --------------------------------------------------------------------------- +// 1. Loading state - skeleton shown +// --------------------------------------------------------------------------- +export const Loading: Story = { + name: "Loading — skeleton", + args: { + loading: true, + jobs: [], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 2. Error state +// --------------------------------------------------------------------------- +export const Error: Story = { + name: "Error — backend failure", + args: { + loading: false, + jobs: [], + error: "Failed to fetch jobs. Please try again later.", + }, +}; + +// --------------------------------------------------------------------------- +// 3. Empty state - no jobs +// --------------------------------------------------------------------------- +export const Empty: Story = { + name: "Empty — no jobs found", + args: { + loading: false, + jobs: [], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 4. Single job - funded +// --------------------------------------------------------------------------- +export const SingleJobFunded: Story = { + name: "Single Job — funded", + args: { + loading: false, + jobs: [mockJob({ id: "job-funded-1", funded: true })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 5. Single job - not funded +// --------------------------------------------------------------------------- +export const SingleJobNotFunded: Story = { + name: "Single Job — not funded", + args: { + loading: false, + jobs: [mockJob({ id: "job-unfunded-1", funded: false })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 6. Multiple jobs - mixed states +// --------------------------------------------------------------------------- +export const MultipleJobs: Story = { + name: "Multiple Jobs — mixed funded/unfunded", + args: { + loading: false, + jobs: [ + mockJob({ id: "job-1-funded", funded: true }), + mockJob({ id: "job-2-unfunded", funded: false }), + mockJob({ id: "job-3-funded", funded: true }), + ], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 7. Job with milestones - expanded +// --------------------------------------------------------------------------- +export const ExpandedWithMilestones: Story = { + name: "Expanded — job with milestones", + args: { + loading: false, + jobs: [ + mockJob({ id: "job-with-milestones", funded: true, milestones: [ + { index: 0, amount: "250000000", status: "Pending" }, + { index: 1, amount: "500000000", status: "Delivered" }, + { index: 2, amount: "750000000", status: "Released" }, + ]}), + ], + error: null, + expandedJobId: "job-with-milestones", + }, +}; + +// --------------------------------------------------------------------------- +// 8. Job with no milestones - expanded +// --------------------------------------------------------------------------- +export const ExpandedNoMilestones: Story = { + name: "Expanded — job with no milestones", + args: { + loading: false, + jobs: [mockJob({ id: "job-no-milestones", funded: true, milestones: [] })], + error: null, + expandedJobId: "job-no-milestones", + }, +}; + +// --------------------------------------------------------------------------- +// 9. Role badges - client view +// --------------------------------------------------------------------------- +export const ClientView: Story = { + name: "Role — client view", + args: { + loading: false, + jobs: [mockJob({ + id: "job-client-view", + funded: true, + client: "GCURRENTUSER1234567890123456789012345678901234", + freelancer: "GFREELANCERADDRESS12345678901234567890123456", + })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 10. Role badges - freelancer view +// --------------------------------------------------------------------------- +export const FreelancerView: Story = { + name: "Role — freelancer view", + args: { + loading: false, + jobs: [mockJob({ + id: "job-freelancer-view", + funded: true, + client: "GCLIENTADDRESS1234567890123456789012345678901234", + freelancer: "GCURRENTUSER1234567890123456789012345678901234", + })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 11. Role badges - arbiter view +// --------------------------------------------------------------------------- +export const ArbiterView: Story = { + name: "Role — arbiter view", + args: { + loading: false, + jobs: [mockJob({ + id: "job-arbiter-view", + funded: true, + client: "GCLIENTADDRESS1234567890123456789012345678901234", + freelancer: "GFREELANCERADDRESS12345678901234567890123456", + arbiter: "GCURRENTUSER1234567890123456789012345678901234", + })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 12. Role badges - multiple roles (client + freelancer) +// --------------------------------------------------------------------------- +export const MultiRoleView: Story = { + name: "Role — multiple roles (client + freelancer)", + args: { + loading: false, + jobs: [mockJob({ + id: "job-multi-role", + funded: true, + client: "GCURRENTUSER1234567890123456789012345678901234", + freelancer: "GCURRENTUSER1234567890123456789012345678901234", + arbiter: "GARBITERADDRESS12345678901234567890123456789012", + })], + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 13. Multiple jobs expanded - first expanded +// --------------------------------------------------------------------------- +export const FirstJobExpanded: Story = { + name: "Multiple — first job expanded", + args: { + loading: false, + jobs: [ + mockJob({ id: "job-1-expanded", funded: true }), + mockJob({ id: "job-2-collapsed", funded: false }), + mockJob({ id: "job-3-collapsed", funded: true }), + ], + error: null, + expandedJobId: "job-1-expanded", + }, +}; + +// --------------------------------------------------------------------------- +// 14. Pagination - many jobs +// --------------------------------------------------------------------------- +export const ManyJobs: Story = { + name: "Many Jobs — pagination scenario", + args: { + loading: false, + jobs: Array.from({ length: 12 }, (_, i) => + mockJob({ id: `job-${i + 1}`, funded: i % 2 === 0 }) + ), + error: null, + }, +}; + +// --------------------------------------------------------------------------- +// 15. Large milestone count - layout stress +// --------------------------------------------------------------------------- +export const LargeMilestoneCount: Story = { + name: "Stress — many milestones", + args: { + loading: false, + jobs: [mockJob({ + id: "job-many-milestones", + funded: true, + milestones: Array.from({ length: 10 }, (_, i) => ({ + index: i, + amount: String(100000000 * (i + 1)), + status: i % 3 === 0 ? "Pending" : i % 3 === 1 ? "Delivered" : "Released", + })), + })], + error: null, + expandedJobId: "job-many-milestones", + }, +}; + +// --------------------------------------------------------------------------- +// 16. All states overview +// --------------------------------------------------------------------------- +export const AllStates: Story = { + name: "All States — overview", + render: () => ( +
+
+

Loading

+ +
+
+

Error

+ +
+
+

Empty

+ +
+
+

Single Funded

+ +
+
+

Multiple Mixed

+ +
+
+ ), +}; + +// --------------------------------------------------------------------------- +// 17. Interactive - expanded job +// --------------------------------------------------------------------------- +export const InteractiveExpanded: Story = { + name: "Interactive — expand/collapse", + args: { + loading: false, + jobs: [ + mockJob({ id: "interactive-1", funded: true }), + mockJob({ id: "interactive-2", funded: false }), + ], + expandedJobId: "interactive-1", + }, + parameters: { + pseudo: { hover: true }, + }, +}; + +// --------------------------------------------------------------------------- +// 18. Job with auto-release countdown (delivered milestone) +// --------------------------------------------------------------------------- +export const WithAutoReleaseCountdown: Story = { + name: "Delivered — auto-release countdown", + args: { + loading: false, + jobs: [mockJob({ + id: "job-auto-release", + funded: true, + milestones: [ + { index: 0, amount: "250000000", status: "Pending" }, + { index: 1, amount: "500000000", status: "Delivered" }, + ], + })], + error: null, + expandedJobId: "job-auto-release", + }, +}; + +// --------------------------------------------------------------------------- +// 19. Disputed milestone +// --------------------------------------------------------------------------- +export const DisputedMilestone: Story = { + name: "Disputed — milestone in dispute", + args: { + loading: false, + jobs: [mockJob({ + id: "job-disputed", + funded: true, + milestones: [ + { index: 0, amount: "250000000", status: "Pending" }, + { index: 1, amount: "500000000", status: "Disputed" }, + ], + })], + error: null, + expandedJobId: "job-disputed", + }, +}; + +// --------------------------------------------------------------------------- +// 20. Partially released milestone +// --------------------------------------------------------------------------- +export const PartiallyReleased: Story = { + name: "Partially Released — 60% released", + args: { + loading: false, + jobs: [mockJob({ + id: "job-partial", + funded: true, + milestones: [ + { index: 0, amount: "1000000000", status: "PartiallyReleased", releasedAmount: "600000000" }, + ], + })], + error: null, + expandedJobId: "job-partial", + }, +}; \ No newline at end of file