diff --git a/src/components/ArbitratorPicker.tsx b/src/components/ArbitratorPicker.tsx new file mode 100644 index 0000000..1b47a95 --- /dev/null +++ b/src/components/ArbitratorPicker.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { truncateAddress } from "@stellar-split/sdk"; + +interface Arbitrator { + address: string; + name: string; + resolvedDisputeCount: number | null; +} + +interface Props { + /** List of arbitrators to display */ + arbitrators: Arbitrator[]; + /** Currently selected arbitrator address */ + selectedAddress: string; + /** Callback when an arbitrator is selected */ + onSelect: (address: string) => void; + /** Whether the component is disabled (e.g., dispute resolved) */ + disabled?: boolean; + /** Heading text (defaults to "Assigned Arbitrators") */ + heading?: string; + /** Show vote status badges (default: false) */ + showVoteStatus?: boolean; + /** List of arbitrators who have voted (used when showVoteStatus is true) */ + votedArbitrators?: string[]; +} + +/** + * ArbitratorPicker — displays a selectable list of arbitrators. + * Extracted from DisputePanel for reusability and testability. + * Can show vote status badges and is fully typed. + */ +export default function ArbitratorPicker({ + arbitrators, + selectedAddress, + onSelect, + disabled = false, + heading = "Assigned Arbitrators", + showVoteStatus = false, + votedArbitrators = [], +}: Props) { + return ( +
+

{heading}

+
+ {arbitrators.length === 0 ? ( +

+ No arbitrators assigned +

+ ) : ( + arbitrators.map((arb) => { + const isSelected = arb.address === selectedAddress; + const hasVoted = votedArbitrators.includes(arb.address); + + return ( + + ); + }) + )} +
+
+ ); +} diff --git a/src/components/DisputePanel.tsx b/src/components/DisputePanel.tsx index 338c032..a0b4626 100644 --- a/src/components/DisputePanel.tsx +++ b/src/components/DisputePanel.tsx @@ -5,6 +5,7 @@ import type { Invoice } from "@stellar-split/sdk"; import { truncateAddress } from "@stellar-split/sdk"; import { uploadToIpfs } from "@/lib/ipfs"; import { getSplitClient } from "@/lib/stellar"; +import ArbitratorPicker from "@/components/ArbitratorPicker"; interface DisputeMetadata { reason: string; @@ -281,31 +282,18 @@ export default function DisputePanel({ invoice, publicKey, onRefresh }: Props) { {/* Arbitrators List */} -
-

Assigned Arbitrators

-
- {dispute.arbitrators.map((arb, idx) => { - const voted = dispute.votedArbitrators.includes(arb); - return ( -
- {truncateAddress(arb)} - - {voted ? "✓ Voted" : "Pending"} - -
- ); - })} -
-
+ ({ + address, + name: truncateAddress(address), + resolvedDisputeCount: null, + }))} + selectedAddress="" + onSelect={() => {}} + disabled={true} + showVoteStatus={true} + votedArbitrators={dispute.votedArbitrators} + /> {/* Evidence Upload Modal */} diff --git a/src/components/DisputeWizard.tsx b/src/components/DisputeWizard.tsx index 2e4d449..7057832 100644 --- a/src/components/DisputeWizard.tsx +++ b/src/components/DisputeWizard.tsx @@ -11,12 +11,59 @@ const DISPUTE_REASONS = [ "Other", ]; +const TOTAL_STEPS = 4; + interface Props { invoiceId: string; onSubmit: (reason: string, description: string, arbitratorAddress?: string) => Promise; onClose: () => void; } +/** + * StepIndicator — displays dot-based progress through the wizard steps. + * Completed steps are filled, the current step is highlighted, upcoming steps are muted. + */ +function StepIndicator({ current, total }: { current: number; total: number }) { + return ( + + ); +} + +/** + * DisputeWizard — multi-step modal form for filing an on-chain dispute. + * Guides the user through: reason → description → arbitrator → confirmation. + */ export default function DisputeWizard({ invoiceId, onSubmit, onClose }: Props) { const [step, setStep] = useState(1); const [reason, setReason] = useState(""); @@ -78,10 +125,18 @@ export default function DisputeWizard({ invoiceId, onSubmit, onClose }: Props) { + {/* Step indicator */} +
+ +

+ Step {step} of {TOTAL_STEPS} +

+
+ {/* Step 1: Reason Selection */} {step === 1 && (
-

Step 1 of 4: Select reason

+

Select reason

{DISPUTE_REASONS.map((r) => (