From 7cdbd1b7f04ec1cbfcfcb373cf75b35874b1d277 Mon Sep 17 00:00:00 2001 From: A5cend-dev Date: Mon, 27 Apr 2026 13:55:25 +0000 Subject: [PATCH 01/11] Fix Playwright E2E tests for Accept Bid Flow: add data-testid and id attributes, update test selector --- apps/web/components/jobs/bid-list.tsx | 34 +++++++++++++++++++ apps/web/components/jobs/submit-bid-modal.tsx | 1 + tests/e2e/gig-lifecycle.spec.ts | 6 ++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/apps/web/components/jobs/bid-list.tsx b/apps/web/components/jobs/bid-list.tsx index ea52bdb6..b3273fb3 100644 --- a/apps/web/components/jobs/bid-list.tsx +++ b/apps/web/components/jobs/bid-list.tsx @@ -197,6 +197,40 @@ export function BidList({ )} + {/* Accept action */} + {canAccept && !isAccepted && ( +
+ +
+ )} + + {isAccepted && ( +

+

+ )} + {/* Accept action */} {canAccept && !isAccepted && (
diff --git a/apps/web/components/jobs/submit-bid-modal.tsx b/apps/web/components/jobs/submit-bid-modal.tsx index 87e04a8f..3a14a3eb 100644 --- a/apps/web/components/jobs/submit-bid-modal.tsx +++ b/apps/web/components/jobs/submit-bid-modal.tsx @@ -197,6 +197,7 @@ export function SubmitBidModal({ Cancel
+ + + ) : null} + + + + ); +} diff --git a/apps/web/app/jobs/page.tsx b/apps/web/app/jobs/page.tsx index 038b92b6..76408bb1 100644 --- a/apps/web/app/jobs/page.tsx +++ b/apps/web/app/jobs/page.tsx @@ -8,9 +8,7 @@ import { DollarSign, Layers, Plus, - Shield, Sparkles, - TrendingUp, Users, Zap, } from "lucide-react"; diff --git a/apps/web/components/jobs/bid-list.tsx b/apps/web/components/jobs/bid-list.tsx index d6b5adea..de425b63 100644 --- a/apps/web/components/jobs/bid-list.tsx +++ b/apps/web/components/jobs/bid-list.tsx @@ -7,7 +7,6 @@ import { shortenAddress, formatDate } from "@/lib/format"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { EmptyState } from "@/components/ui/empty-state"; -import { Stars } from "@/components/stars"; import { cn } from "@/lib/utils"; import { AcceptBidFlow } from "./accept-bid-flow"; @@ -129,15 +128,6 @@ export function BidList({ const canAccept = isClientOwner && job.status === "open"; - const sortedBids = [...bids].sort((left, right) => { - const leftScore = left.freelancerReputation?.scoreBps ?? 0; - const rightScore = right.freelancerReputation?.scoreBps ?? 0; - if (rightScore !== leftScore) return rightScore - leftScore; - return ( - new Date(left.created_at).getTime() - new Date(right.created_at).getTime() - ); - }); - return ( {({ handleAcceptClick, acceptingBidId }) => ( diff --git a/apps/web/components/jobs/milestone-tracker.tsx b/apps/web/components/jobs/milestone-tracker.tsx index 23c7bc58..c3f21825 100644 --- a/apps/web/components/jobs/milestone-tracker.tsx +++ b/apps/web/components/jobs/milestone-tracker.tsx @@ -417,12 +417,11 @@ export function MilestoneTracker({ busyMilestoneId = null, onRelease, }: MilestoneTrackerProps) { - const { releasedAmount, totalAmount } = useMemo(() => { + const { releasedAmount } = useMemo(() => { const released = milestones .filter((m) => m.status === "released") .reduce((sum, m) => sum + m.amount_usdc, 0); - const total = milestones.reduce((sum, m) => sum + m.amount_usdc, 0); - return { releasedAmount: released, totalAmount: total }; + return { releasedAmount: released }; }, [milestones]); const releasedCount = milestones.filter((m) => m.status === "released").length; diff --git a/apps/web/components/ui/sonner.tsx b/apps/web/components/ui/sonner.tsx index 9e48cbec..5a7e4df3 100644 --- a/apps/web/components/ui/sonner.tsx +++ b/apps/web/components/ui/sonner.tsx @@ -1,6 +1,5 @@ "use client" -import { useTheme } from "next-themes" import { Toaster as Sonner } from "sonner" type ToasterProps = React.ComponentProps diff --git a/apps/web/components/wallet-connect.tsx b/apps/web/components/wallet-connect.tsx index 314dc68f..8ecf5f55 100644 --- a/apps/web/components/wallet-connect.tsx +++ b/apps/web/components/wallet-connect.tsx @@ -10,7 +10,6 @@ import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Wallet, LogOut, ShieldCheck, ChevronDown, Loader2 } from "lucide-react"; -import { cn } from "@/lib/utils"; export function WalletConnect() { const { connect, disconnect, isConnecting, isLoggedIn, address } = useWallet(); diff --git a/apps/web/lib/job-registry.test.ts b/apps/web/lib/job-registry.test.ts index 9b883d32..4b73eee1 100644 --- a/apps/web/lib/job-registry.test.ts +++ b/apps/web/lib/job-registry.test.ts @@ -4,7 +4,7 @@ * Validates acceptBid (Issue #162) and other contract interactions */ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach } from "vitest"; import { acceptBid, AcceptBidParams, @@ -22,9 +22,9 @@ beforeEach(() => { describe("acceptBid", () => { it("should throw error when clientAddress is missing", async () => { const params: AcceptBidParams = { - jobId: 1n, + jobId: BigInt(1), clientAddress: "", - bidId: 1n, + freelancerAddress: "GD...", }; await expect(acceptBid(params)).rejects.toThrow( @@ -36,7 +36,7 @@ describe("acceptBid", () => { const params: AcceptBidParams = { jobId: 0n, clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - bidId: 1n, + freelancerAddress: "GD...", }; await expect(acceptBid(params)).rejects.toThrow( @@ -44,15 +44,15 @@ describe("acceptBid", () => { ); }); - it("should throw error when bidId is zero or negative", async () => { + it("should throw error when freelancerAddress is missing", async () => { const params: AcceptBidParams = { jobId: 1n, clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - bidId: 0n, + freelancerAddress: "", }; await expect(acceptBid(params)).rejects.toThrow( - "bidId must be greater than zero." + "freelancerAddress is required." ); }); @@ -62,7 +62,7 @@ describe("acceptBid", () => { const params: AcceptBidParams = { jobId: 1n, clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - bidId: 1n, + freelancerAddress: "GD...", }; const lifecycleSteps: string[] = []; @@ -91,7 +91,7 @@ describe("acceptBid", () => { const params: AcceptBidParams = { jobId: 1n, clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - bidId: 1n, + freelancerAddress: "GD...", }; await expect(acceptBid(params)).rejects.toThrow( @@ -105,7 +105,7 @@ describe("acceptBid", () => { const params: AcceptBidParams = { jobId: 123n, clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", - bidId: 456n, + freelancerAddress: "GD...", }; const steps: string[] = []; From 1f585cf64e8ce48a3b6ab3ef39740d6623a4d6df Mon Sep 17 00:00:00 2001 From: A5cend-dev Date: Wed, 29 Apr 2026 12:07:21 +0100 Subject: [PATCH 05/11] new --- apps/web/app/jobs/[id]/page.tsx | 3 ++- apps/web/components/jobs/job-card.tsx | 2 -- apps/web/components/wallet/wallet-connect.tsx | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/app/jobs/[id]/page.tsx b/apps/web/app/jobs/[id]/page.tsx index 77d2f49a..4de087e2 100644 --- a/apps/web/app/jobs/[id]/page.tsx +++ b/apps/web/app/jobs/[id]/page.tsx @@ -468,10 +468,11 @@ export default function JobDetailsPage() { id="deliverable-file" /> + ) : null} - ) : null} + diff --git a/apps/web/components/jobs/job-card.tsx b/apps/web/components/jobs/job-card.tsx index 8db94792..78e30aba 100644 --- a/apps/web/components/jobs/job-card.tsx +++ b/apps/web/components/jobs/job-card.tsx @@ -115,8 +115,6 @@ function StarRating({ rating }: { rating: number }) { } export function JobCard({ job }: JobCardProps) { - const statusConfig = STATUS_CONFIG[job.status] || STATUS_CONFIG.open; - return (
Date: Wed, 29 Apr 2026 12:27:51 +0100 Subject: [PATCH 06/11] good --- apps/web/app/jobs/[id]/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/app/jobs/[id]/page.tsx b/apps/web/app/jobs/[id]/page.tsx index 4de087e2..00980746 100644 --- a/apps/web/app/jobs/[id]/page.tsx +++ b/apps/web/app/jobs/[id]/page.tsx @@ -474,6 +474,8 @@ export default function JobDetailsPage() { + ) : null} + ); From 8e2a6f111fa70c70bc0f4a5b3d0560fba765793d Mon Sep 17 00:00:00 2001 From: A5cend-dev Date: Wed, 29 Apr 2026 12:51:09 +0100 Subject: [PATCH 07/11] fixed the suntax issue on the frontend [id]/page.tsx --- apps/web/app/jobs/[id]/page.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/web/app/jobs/[id]/page.tsx b/apps/web/app/jobs/[id]/page.tsx index 00980746..5f710eb2 100644 --- a/apps/web/app/jobs/[id]/page.tsx +++ b/apps/web/app/jobs/[id]/page.tsx @@ -48,6 +48,7 @@ export default function JobDetailsPage() { const [deliverableLink, setDeliverableLink] = useState(""); const [deliverableFile, setDeliverableFile] = useState(null); const [busyAction, setBusyAction] = useState(null); + const [proposal, setProposal] = useState(null); useEffect(() => { void getConnectedWalletAddress().then(setViewerAddress); @@ -69,7 +70,7 @@ export default function JobDetailsPage() { (await getConnectedWalletAddress()) ?? "GD...FREELANCER"; await api.bids.create(id, { freelancer_address: freelancerAddress, - proposal, + proposal: proposal || "", }); setProposal(""); await workspace.refresh(); @@ -302,8 +303,9 @@ export default function JobDetailsPage() { - ) : null} + ) :
p
} +
{job.status === "open" ? (
@@ -398,7 +400,7 @@ export default function JobDetailsPage() { Boolean(viewerAddress) && viewerAddress === job.client_address } - workflowLocked={workflowLocked} + workflowLocked ={workflowLocked} busyMilestoneId={ busyAction?.startsWith("release-") ? busyAction.replace("release-", "") @@ -469,11 +471,8 @@ export default function JobDetailsPage() { /> ) : null} -
- - ) : null} From 51ca275258a32d869e32f3083c53a7e18824937b Mon Sep 17 00:00:00 2001 From: A5cend-dev Date: Wed, 29 Apr 2026 14:09:13 +0100 Subject: [PATCH 08/11] fixed the accept bid issue --- apps/web/components/jobs/accept-bid-modal.tsx | 195 ++++++++++-------- apps/web/lib/api.ts | 10 + 2 files changed, 114 insertions(+), 91 deletions(-) diff --git a/apps/web/components/jobs/accept-bid-modal.tsx b/apps/web/components/jobs/accept-bid-modal.tsx index 89e7f5a9..e01b61d0 100644 --- a/apps/web/components/jobs/accept-bid-modal.tsx +++ b/apps/web/components/jobs/accept-bid-modal.tsx @@ -1,99 +1,112 @@ -import { render, screen, fireEvent } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; -import { AcceptBidModal } from "./accept-bid-modal"; -import { type Bid, type Job } from "@/lib/api"; - -const mockBid: Bid = { - id: "bid-1", - job_id: "job-1", - freelancer_address: "GD...FREELANCER", - proposal: "I can do this job perfectly.", - status: "pending", - created_at: new Date().toISOString(), -}; +"use client"; -const mockJob: Job = { - id: "job-1", - title: "Test Job", - description: "Test Description", - budget_usdc: 1000 * 10_000_000, // 1000 USDC in micro units - milestones: 3, - client_address: "GD...CLIENT", - status: "open", - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), -}; - -describe("AcceptBidModal", () => { - it("renders correctly when open", () => { - render( - - ); +import { type Bid, type Job } from "@/lib/api"; +import { formatUsdc, shortenAddress } from "@/lib/format"; - expect(screen.getByText(/Accept Freelancer Bid/i)).toBeDefined(); - expect(screen.getByText(/GD/i)).toBeDefined(); - expect(screen.getByText(/1,000/i)).toBeDefined(); - expect(screen.getByText(/perfectly/i)).toBeDefined(); - }); +interface AcceptBidModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + bid: Bid | null; + job: Job; + isPending: boolean; +} - it("calls onConfirm when clicking the confirm button", () => { - const onConfirm = vi.fn(); - render( - - ); +export function AcceptBidModal({ + isOpen, + onClose, + onConfirm, + bid, + job, + isPending, +}: AcceptBidModalProps) { + if (!isOpen || !bid) return null; - const confirmButton = screen.getByRole("button", { name: /Confirm & Accept/i }); - fireEvent.click(confirmButton); - expect(onConfirm).toHaveBeenCalledTimes(1); - }); + return ( +
+
event.stopPropagation()} + > +
+
+

+ Accept Freelancer Bid +

+

+ Review the freelancer's proposal and accept their bid to start the contract. +

+
+ +
- it("calls onClose when clicking the cancel button", () => { - const onClose = vi.fn(); - render( - - ); +
+
+
+
+

Freelancer

+

+ {shortenAddress(bid.freelancer_address)} +

+
+
+

Bid Amount

+

+ {formatUsdc(job.budget_usdc)} +

+
+
+
- const cancelButton = screen.getByRole("button", { name: /Cancel/i }); - fireEvent.click(cancelButton); - expect(onClose).toHaveBeenCalledTimes(1); - }); +
+

Proposal

+

+ {bid.proposal} +

+
- it("shows loading state when isPending is true", () => { - render( - - ); +
+

Contract Details

+
+

• Contract Value: {formatUsdc(job.budget_usdc)}

+

• Milestones: {job.milestones}

+

• Client: {shortenAddress(job.client_address)}

+
+
+
- const acceptButton = screen.getByRole("button", { name: /Accepting.../i }); - expect(acceptButton).toBeDefined(); - expect(acceptButton).toBeDisabled(); - expect(screen.getByRole("button", { name: /Cancel/i })).toBeDisabled(); - }); -}); \ No newline at end of file +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 1716647a..2a5f6d29 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -361,3 +361,13 @@ export interface UpdateProfileBody { portfolio_links: string[]; } +export interface ActivityLog { + id: string; + user_address?: string; + job_id?: string; + event_type: string; + level: string; + details: any; + created_at: string; +} + From 2569e438d5a4ef298e28ab98321e743ee43b1f0b Mon Sep 17 00:00:00 2001 From: A5cend-dev Date: Wed, 29 Apr 2026 14:16:26 +0100 Subject: [PATCH 09/11] done --- apps/web/app/jobs/[id]/page.tsx | 21 +------------------ apps/web/components/jobs/accept-bid-modal.tsx | 2 +- apps/web/lib/api.ts | 2 +- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/apps/web/app/jobs/[id]/page.tsx b/apps/web/app/jobs/[id]/page.tsx index 5f710eb2..99895182 100644 --- a/apps/web/app/jobs/[id]/page.tsx +++ b/apps/web/app/jobs/[id]/page.tsx @@ -61,26 +61,7 @@ export default function JobDetailsPage() { return connected; } - async function handleBid(event: React.FormEvent) { - event.preventDefault(); - setBusyAction("bid"); - - try { - const freelancerAddress = - (await getConnectedWalletAddress()) ?? "GD...FREELANCER"; - await api.bids.create(id, { - freelancer_address: freelancerAddress, - proposal: proposal || "", - }); - setProposal(""); - await workspace.refresh(); - } catch { - alert("Failed to submit bid"); - } finally { - setBusyAction(null); - } - } - + async function handleSubmitDeliverable(event: React.FormEvent) { diff --git a/apps/web/components/jobs/accept-bid-modal.tsx b/apps/web/components/jobs/accept-bid-modal.tsx index e01b61d0..99561af1 100644 --- a/apps/web/components/jobs/accept-bid-modal.tsx +++ b/apps/web/components/jobs/accept-bid-modal.tsx @@ -42,7 +42,7 @@ export function AcceptBidModal({ Accept Freelancer Bid

- Review the freelancer's proposal and accept their bid to start the contract. + Review the freelancer's proposal and accept their bid to start the contract.