|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useEffect } from "react"; |
4 | | -import { useParams, useRouter } from "next/navigation"; |
5 | | -import { SiteShell } from "@/components/site-shell"; |
6 | | -import { api } from "@/lib/api"; |
| 3 | +import Link from "next/link"; |
| 4 | +import { useParams } from "next/navigation"; |
| 5 | +import { FormEvent, useEffect, useState } from "react"; |
| 6 | + |
| 7 | +import { api, type Dispute, type Evidence, type Verdict } from "@/lib/api"; |
| 8 | +import { connectWallet, signTransaction } from "@/lib/stellar"; |
7 | 9 |
|
8 | 10 | export default function DisputePage() { |
9 | | - const { id } = useParams<{ id: string }>(); |
10 | | - const router = useRouter(); |
| 11 | + const params = useParams<{ id: string }>(); |
| 12 | + const disputeId = params.id; |
| 13 | + |
| 14 | + const [dispute, setDispute] = useState<Dispute | null>(null); |
| 15 | + const [verdict, setVerdict] = useState<Verdict | null>(null); |
| 16 | + const [walletAddress, setWalletAddress] = useState(""); |
| 17 | + const [signature, setSignature] = useState(""); |
| 18 | + const [evidence, setEvidence] = useState<Evidence | null>(null); |
| 19 | + const [content, setContent] = useState( |
| 20 | + "Freelancer uploaded timestamped delivery proof and reviewer notes.", |
| 21 | + ); |
| 22 | + const [status, setStatus] = useState("Loading dispute state..."); |
| 23 | + const [isSubmitting, setIsSubmitting] = useState(false); |
11 | 24 |
|
12 | 25 | useEffect(() => { |
13 | 26 | let active = true; |
14 | 27 |
|
15 | | - void api.disputes |
16 | | - .get(id) |
17 | | - .then((dispute) => { |
18 | | - if (!active) return; |
19 | | - router.replace(`/jobs/${dispute.job_id}/dispute?disputeId=${dispute.id}`); |
20 | | - }) |
21 | | - .catch(() => { |
22 | | - if (!active) return; |
23 | | - }); |
| 28 | + async function loadDispute() { |
| 29 | + try { |
| 30 | + const [nextDispute, nextVerdict] = await Promise.all([ |
| 31 | + api.disputes.get(disputeId), |
| 32 | + api.disputes.verdict(disputeId), |
| 33 | + ]); |
| 34 | + |
| 35 | + if (!active) { |
| 36 | + return; |
| 37 | + } |
| 38 | + |
| 39 | + setDispute(nextDispute); |
| 40 | + setVerdict(nextVerdict); |
| 41 | + setStatus("Dispute loaded. Evidence can now be signed and submitted."); |
| 42 | + } catch (error) { |
| 43 | + if (!active) { |
| 44 | + return; |
| 45 | + } |
| 46 | + |
| 47 | + setStatus( |
| 48 | + error instanceof Error ? error.message : "Unable to load dispute.", |
| 49 | + ); |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + void loadDispute(); |
24 | 54 |
|
25 | 55 | return () => { |
26 | 56 | active = false; |
27 | 57 | }; |
28 | | - }, [id, router]); |
| 58 | + }, [disputeId]); |
| 59 | + |
| 60 | + async function ensureWallet(): Promise<string> { |
| 61 | + if (walletAddress) { |
| 62 | + return walletAddress; |
| 63 | + } |
| 64 | + |
| 65 | + const address = await connectWallet(); |
| 66 | + setWalletAddress(address); |
| 67 | + return address; |
| 68 | + } |
| 69 | + |
| 70 | + async function onSubmitEvidence(event: FormEvent<HTMLFormElement>) { |
| 71 | + event.preventDefault(); |
| 72 | + setIsSubmitting(true); |
| 73 | + setStatus("Connecting wallet for evidence approval..."); |
| 74 | + |
| 75 | + try { |
| 76 | + const address = await ensureWallet(); |
| 77 | + setStatus("Signing evidence receipt..."); |
| 78 | + |
| 79 | + const signed = await signTransaction( |
| 80 | + JSON.stringify({ |
| 81 | + action: "submit_evidence", |
| 82 | + dispute_id: disputeId, |
| 83 | + submitted_by: address, |
| 84 | + content, |
| 85 | + }), |
| 86 | + ); |
| 87 | + |
| 88 | + setSignature(signed); |
| 89 | + setStatus("Signature approved. Sending evidence to mocked backend..."); |
| 90 | + |
| 91 | + const savedEvidence = await api.disputes.evidence.submit(disputeId, { |
| 92 | + submitted_by: address, |
| 93 | + content, |
| 94 | + file_hash: "bafybeigdyrdeterministicevidence", |
| 95 | + }); |
| 96 | + |
| 97 | + setEvidence(savedEvidence); |
| 98 | + setStatus("Evidence stored. UI reflects the signed submission."); |
| 99 | + } catch (error) { |
| 100 | + setStatus( |
| 101 | + error instanceof Error ? error.message : "Evidence submission failed.", |
| 102 | + ); |
| 103 | + } finally { |
| 104 | + setIsSubmitting(false); |
| 105 | + } |
| 106 | + } |
29 | 107 |
|
30 | 108 | return ( |
31 | | - <SiteShell |
32 | | - eyebrow="Dispute Center" |
33 | | - title="Redirecting to job-linked dispute center" |
34 | | - description="Legacy dispute URLs now resolve to the canonical job-based route." |
35 | | - > |
36 | | - <div className="h-64 animate-pulse rounded-[2rem] border border-slate-200 bg-white/70" /> |
37 | | - </SiteShell> |
| 109 | + <main className="min-h-screen bg-stone-950 px-6 py-10 text-stone-50"> |
| 110 | + <div className="mx-auto flex max-w-5xl flex-col gap-8"> |
| 111 | + <header className="space-y-3"> |
| 112 | + <Link href="/jobs" className="text-sm text-amber-300 hover:text-amber-200"> |
| 113 | + Back to jobs |
| 114 | + </Link> |
| 115 | + <p className="text-xs uppercase tracking-[0.35em] text-amber-300"> |
| 116 | + Dispute Workspace |
| 117 | + </p> |
| 118 | + <h1 className="text-4xl font-semibold">Dispute Verdict</h1> |
| 119 | + <p className="max-w-3xl text-sm text-stone-300">{status}</p> |
| 120 | + </header> |
| 121 | + |
| 122 | + <div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]"> |
| 123 | + <section className="space-y-4 rounded-3xl border border-stone-800 bg-stone-900/70 p-6"> |
| 124 | + <h2 className="text-xl font-semibold">Case Snapshot</h2> |
| 125 | + <dl className="space-y-3 text-sm text-stone-300"> |
| 126 | + <div> |
| 127 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 128 | + Dispute ID |
| 129 | + </dt> |
| 130 | + <dd className="break-all">{dispute?.id ?? disputeId}</dd> |
| 131 | + </div> |
| 132 | + <div> |
| 133 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 134 | + Opened by |
| 135 | + </dt> |
| 136 | + <dd className="break-all">{dispute?.opened_by ?? "Loading..."}</dd> |
| 137 | + </div> |
| 138 | + <div> |
| 139 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 140 | + Verdict |
| 141 | + </dt> |
| 142 | + <dd> |
| 143 | + {verdict |
| 144 | + ? `${verdict.winner} · ${verdict.freelancer_share_bps} bps` |
| 145 | + : "Pending"} |
| 146 | + </dd> |
| 147 | + </div> |
| 148 | + <div> |
| 149 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 150 | + Reasoning |
| 151 | + </dt> |
| 152 | + <dd>{verdict?.reasoning ?? "Loading..."}</dd> |
| 153 | + </div> |
| 154 | + <div> |
| 155 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 156 | + Settlement Tx |
| 157 | + </dt> |
| 158 | + <dd className="break-all">{verdict?.on_chain_tx ?? "Pending"}</dd> |
| 159 | + </div> |
| 160 | + <div> |
| 161 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 162 | + Wallet |
| 163 | + </dt> |
| 164 | + <dd className="break-all">{walletAddress || "Not connected"}</dd> |
| 165 | + </div> |
| 166 | + <div> |
| 167 | + <dt className="text-xs uppercase tracking-[0.3em] text-stone-500"> |
| 168 | + Signed payload |
| 169 | + </dt> |
| 170 | + <dd className="break-all font-mono text-xs text-stone-400"> |
| 171 | + {signature || "Awaiting wallet approval"} |
| 172 | + </dd> |
| 173 | + </div> |
| 174 | + </dl> |
| 175 | + </section> |
| 176 | + |
| 177 | + <section className="rounded-3xl border border-amber-400/20 bg-amber-400/5 p-6"> |
| 178 | + <h2 className="text-xl font-semibold">Submit Evidence</h2> |
| 179 | + <p className="mt-2 text-sm text-stone-300"> |
| 180 | + The test suite injects a mock Freighter-compatible wallet and signs |
| 181 | + this evidence payload in-browser before the request is accepted. |
| 182 | + </p> |
| 183 | + |
| 184 | + <form onSubmit={onSubmitEvidence} className="mt-6 space-y-4"> |
| 185 | + <label className="block space-y-2"> |
| 186 | + <span className="text-sm font-medium text-stone-200"> |
| 187 | + Evidence summary |
| 188 | + </span> |
| 189 | + <textarea |
| 190 | + rows={6} |
| 191 | + value={content} |
| 192 | + onChange={(event) => setContent(event.target.value)} |
| 193 | + className="w-full rounded-2xl border border-stone-700 bg-stone-950 px-4 py-3 outline-none transition focus:border-amber-400" |
| 194 | + /> |
| 195 | + </label> |
| 196 | + |
| 197 | + <button |
| 198 | + type="submit" |
| 199 | + disabled={isSubmitting} |
| 200 | + className="inline-flex min-h-12 items-center justify-center rounded-full bg-amber-300 px-6 py-3 font-semibold text-stone-950 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:bg-stone-700 disabled:text-stone-300" |
| 201 | + > |
| 202 | + {isSubmitting ? "Submitting..." : "Sign and Submit Evidence"} |
| 203 | + </button> |
| 204 | + </form> |
| 205 | + |
| 206 | + {evidence ? ( |
| 207 | + <div className="mt-6 rounded-2xl border border-emerald-400/30 bg-emerald-400/10 p-4"> |
| 208 | + <p className="text-xs uppercase tracking-[0.3em] text-emerald-300"> |
| 209 | + Evidence Recorded |
| 210 | + </p> |
| 211 | + <p className="mt-2 break-all text-sm text-stone-100"> |
| 212 | + {evidence.content} |
| 213 | + </p> |
| 214 | + <p className="mt-2 break-all font-mono text-xs text-stone-400"> |
| 215 | + {evidence.id} |
| 216 | + </p> |
| 217 | + </div> |
| 218 | + ) : null} |
| 219 | + </section> |
| 220 | + </div> |
| 221 | + </div> |
| 222 | + </main> |
38 | 223 | ); |
39 | 224 | } |
0 commit comments