Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions apps/web/app/jobs/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
const [deliverableLink, setDeliverableLink] = useState("");
const [deliverableFile, setDeliverableFile] = useState<File | null>(null);
const [busyAction, setBusyAction] = useState<string | null>(null);
const [proposal, setProposal] = useState<string | null>(null);

Check warning on line 43 in apps/web/app/jobs/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Web Frontend

'setProposal' is assigned a value but never used

Check warning on line 43 in apps/web/app/jobs/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Web Frontend

'proposal' is assigned a value but never used

useEffect(() => {
void getConnectedWalletAddress().then(setViewerAddress);
Expand All @@ -52,6 +53,9 @@
return connected;
}




async function handleSubmitDeliverable(event: React.FormEvent) {
event.preventDefault();
if (!workspace.job) return;
Expand Down Expand Up @@ -232,8 +236,9 @@
</div>
</div>
</div>
) : null}
) : <div> p </div>}
</div>
<section/>

{job.status === "open" ? (
<div className="grid gap-6 xl:grid-cols-[1fr_0.95fr]">
Expand Down Expand Up @@ -328,7 +333,7 @@
Boolean(viewerAddress) &&
viewerAddress === job.client_address
}
workflowLocked={workflowLocked}
workflowLocked ={workflowLocked}
busyMilestoneId={
busyAction?.startsWith("release-")
? busyAction.replace("release-", "")
Expand Down Expand Up @@ -388,13 +393,17 @@
<FileUp className="h-4 w-4 text-amber-600" />
<span>{deliverableFile ? deliverableFile.name : "Upload ZIP, image, JSON, or PDF evidence"}</span>
</label>
<input
type="file"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) setDeliverableFile(file);
}}
className="hidden"
id="deliverable-file"
/>
</form>
) : (
<div className="mt-5 text-center text-slate-500">
<FileUp className="mx-auto h-8 w-8 text-slate-400" />
<p className="mt-2 text-sm">Deliverables are locked during dispute resolution</p>
</div>
)}
) : null}
</section>
</div>
) : null}
Expand Down
106 changes: 58 additions & 48 deletions apps/web/components/jobs/accept-bid-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { LoaderCircle } from "lucide-react";
import { type Bid, type Job } from "@/lib/api";
import { formatUsdc, shortenAddress } from "@/lib/format";

interface AcceptBidModalProps {
isOpen: boolean;
Expand Down Expand Up @@ -36,66 +36,76 @@ export function AcceptBidModal({
className="w-full max-w-2xl rounded-[1.75rem] border border-white/10 bg-zinc-950/95 p-6 shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.22em] text-zinc-500">
Review & Accept
</p>
<h3 id="accept-bid-title" className="mt-2 text-2xl font-semibold text-zinc-50">
<h2 id="accept-bid-title" className="text-xl font-semibold text-zinc-50">
Accept Freelancer Bid
</h3>
<p id="accept-bid-description" className="mt-2 max-w-2xl text-sm text-zinc-300">
Review the freelancer&apos;s proposal and budget. Once accepted, you&apos;ll need to fund the job.
</h2>
<p id="accept-bid-description" className="mt-2 text-sm text-zinc-400">
Review the freelancer&apos;s proposal and accept their bid to start the contract.
</p>
</div>
<button
onClick={onClose}
disabled={isPending}
className="h-8 w-8 rounded-full bg-zinc-800 text-zinc-400 hover:bg-zinc-700 disabled:opacity-50"
aria-label="Close modal"
>
×
</button>
</div>

<div className="mt-6 space-y-4">
<div className="rounded-2xl border border-zinc-800 bg-black/30 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="text-sm font-medium text-zinc-100">Freelancer Address</span>
<span className="font-mono text-xs text-zinc-400">{bid.freelancer_address}</span>
</div>

<div className="mb-3">
<span className="text-sm font-medium text-zinc-100">Proposal</span>
<p className="mt-2 text-sm text-zinc-300 leading-relaxed">{bid.proposal}</p>
<div className="rounded-[1.25rem] border border-zinc-800 bg-zinc-900/50 p-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-zinc-300">Freelancer</p>
<p className="mt-1 font-mono text-sm text-zinc-400">
{shortenAddress(bid.freelancer_address)}
</p>
</div>
<div className="text-right">
<p className="text-sm font-medium text-zinc-300">Bid Amount</p>
<p className="mt-1 text-lg font-semibold text-zinc-100">
{formatUsdc(job.budget_usdc)}
</p>
</div>
</div>
</div>

<div className="flex items-center justify-between">
<span className="text-sm font-medium text-zinc-100">Job Budget</span>
<span className="text-lg font-semibold text-emerald-400">
${(job.budget_usdc / 10_000_000).toLocaleString()} USDC
</span>
</div>
<div className="rounded-[1.25rem] border border-zinc-800 bg-zinc-900/50 p-4">
<p className="text-sm font-medium text-zinc-300 mb-2">Proposal</p>
<p className="text-sm text-zinc-400 leading-relaxed">
{bid.proposal}
</p>
</div>

<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button
type="button"
onClick={onClose}
disabled={isPending}
className="rounded-xl border border-zinc-700 px-4 py-2 text-sm font-semibold text-zinc-200 transition duration-150 hover:border-zinc-500 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-200 active:translate-y-px disabled:opacity-50"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
disabled={isPending}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-emerald-500 px-5 py-2.5 text-sm font-semibold text-zinc-950 transition duration-150 hover:bg-emerald-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-60"
>
{isPending ? (
<>
<LoaderCircle className="h-4 w-4 animate-spin" />
Accepting...
</>
) : (
"Confirm & Accept"
)}
</button>
<div className="rounded-[1.25rem] border border-amber-500/20 bg-amber-950/20 p-4">
<p className="text-sm font-medium text-amber-300 mb-2">Contract Details</p>
<div className="space-y-1 text-sm text-amber-200/80">
<p>• Contract Value: {formatUsdc(job.budget_usdc)}</p>
<p>• Milestones: {job.milestones}</p>
<p>• Client: {shortenAddress(job.client_address)}</p>
</div>
</div>
</div>

<div className="mt-6 flex gap-3">
<button
onClick={onClose}
disabled={isPending}
className="flex-1 rounded-[1.25rem] border border-zinc-700 bg-zinc-800 px-4 py-3 text-sm font-medium text-zinc-300 hover:bg-zinc-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
<button
onClick={onConfirm}
disabled={isPending}
className="flex-1 rounded-[1.25rem] bg-amber-500 px-4 py-3 text-sm font-medium text-zinc-950 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isPending ? "Accepting..." : "Confirm & Accept"}
</button>
</div>
</section>
</div>
);
Expand Down
30 changes: 30 additions & 0 deletions apps/web/components/jobs/bid-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,36 @@ export function BidList({
</button>
)}

{/* Accept action */}
{canAccept && !isAccepted && (
<div className="mt-4 flex justify-end">
<Button
size="sm"
onClick={() => handleAcceptClick(bid.id)}
disabled={isAccepting || Boolean(acceptingBidId)}
aria-label={`Accept bid from ${shortenAddress(bid.freelancer_address)}`}
aria-busy={isAccepting}
className="rounded-full bg-emerald-600 text-xs font-medium text-white shadow-sm shadow-emerald-500/20 transition-all duration-150 hover:bg-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 disabled:opacity-60"
>
{isAccepting ? (
<>
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden="true" />
Accepting…
</>
) : (
<span className="text-zinc-500">No reputation yet</span>
)}
</Button>
</div>
)}

{isAccepted && (
<p className="mt-3 flex items-center gap-1.5 text-xs font-medium text-emerald-400">
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
Bid accepted — work in progress
</p>
)}

{/* Accept action */}
{canAccept && !isAccepted && (
<div className="mt-4 flex justify-end">
Expand Down
2 changes: 0 additions & 2 deletions apps/web/components/jobs/job-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ function StarRating({ rating }: { rating: number }) {
}

export function JobCard({ job }: JobCardProps) {
const statusConfig = STATUS_CONFIG[job.status] || STATUS_CONFIG.open;

return (
<article className="group relative flex flex-col rounded-3xl border border-white/10 bg-zinc-950/70 p-6 shadow-[0_20px_60px_-45px_rgba(15,23,42,0.55)] backdrop-blur-md transition-all duration-150 hover:-translate-y-1 hover:border-amber-500/30 hover:shadow-[0_24px_64px_-40px_rgba(15,23,42,0.65)] focus-within:ring-2 focus-within:ring-amber-500/50 focus-within:ring-offset-2 focus-within:ring-offset-zinc-950">
<Link
Expand Down
1 change: 0 additions & 1 deletion apps/web/components/wallet/wallet-connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export function WalletConnect() {
const {
address,
network,
status,
connect,
disconnect,
isConnected,
Expand Down
23 changes: 12 additions & 11 deletions apps/web/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,17 @@ export interface JobMetadata {
estimated_duration_days?: number | null;
}

export interface AuthChallengeResponse {
challenge: string;
expires_at: string;
}

export interface AuthVerifyResponse {
token: string;
expires_at: string;
user_address: string;
}

export interface MetadataUploadResponse {
cid: string;
metadata_hash: string;
Expand Down Expand Up @@ -388,17 +399,7 @@ export interface ActivityLog {
job_id?: string;
event_type: string;
level: string;
details: Record<string, unknown> | string;
details: unknown;
created_at: string;
}

export interface AuthChallengeResponse {
address: string;
challenge: string;
}

export interface AuthVerifyResponse {
address: string;
token: string;
}

18 changes: 9 additions & 9 deletions apps/web/lib/job-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -36,23 +36,23 @@ describe("acceptBid", () => {
const params: AcceptBidParams = {
jobId: 0n,
clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
bidId: 1n,
freelancerAddress: "GD...",
};

await expect(acceptBid(params)).rejects.toThrow(
"jobId must be greater than zero."
);
});

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."
);
});

Expand All @@ -62,7 +62,7 @@ describe("acceptBid", () => {
const params: AcceptBidParams = {
jobId: 1n,
clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
bidId: 1n,
freelancerAddress: "GD...",
};

const lifecycleSteps: string[] = [];
Expand Down Expand Up @@ -91,7 +91,7 @@ describe("acceptBid", () => {
const params: AcceptBidParams = {
jobId: 1n,
clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
bidId: 1n,
freelancerAddress: "GD...",
};

await expect(acceptBid(params)).rejects.toThrow(
Expand All @@ -105,7 +105,7 @@ describe("acceptBid", () => {
const params: AcceptBidParams = {
jobId: 123n,
clientAddress: "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
bidId: 456n,
freelancerAddress: "GD...",
};

const steps: string[] = [];
Expand Down
Loading