|
| 1 | +import type { Metadata } from 'next' |
| 2 | +import { notFound } from 'next/navigation' |
| 3 | + |
1 | 4 | import { ClaimVotePanel } from '@/components/claims/claim-vote-panel' |
| 5 | +import { getConfig } from '@/config/env' |
| 6 | +import type { Claim } from '@/lib/schemas/vote' |
| 7 | +import { ClaimSchema } from '@/lib/schemas/vote' |
2 | 8 |
|
3 | 9 | interface ClaimPageProps { |
4 | 10 | params: Promise<{ claimId: string }> |
5 | 11 | } |
6 | 12 |
|
| 13 | +async function fetchClaimForMeta(claimId: string): Promise<Claim | null> { |
| 14 | + try { |
| 15 | + const { apiUrl } = getConfig() |
| 16 | + const res = await fetch(`${apiUrl}/api/claims/${claimId}`, { |
| 17 | + next: { revalidate: 60 }, |
| 18 | + }) |
| 19 | + if (res.status === 404) return null |
| 20 | + if (!res.ok) return null |
| 21 | + const data = await res.json() |
| 22 | + return ClaimSchema.parse(data) |
| 23 | + } catch { |
| 24 | + return null |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +export async function generateMetadata({ params }: ClaimPageProps): Promise<Metadata> { |
| 29 | + const { claimId } = await params |
| 30 | + const claim = await fetchClaimForMeta(claimId) |
| 31 | + |
| 32 | + if (!claim) { |
| 33 | + return { |
| 34 | + title: 'Claim Not Found', |
| 35 | + description: 'The requested claim could not be found.', |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + const title = `Claim ${claimId} — ${claim.status}` |
| 40 | + const description = `Vote on claim ${claimId}. Current status: ${claim.status}. Approve votes: ${claim.approve_votes}, Reject votes: ${claim.reject_votes}.` |
| 41 | + |
| 42 | + return { |
| 43 | + title, |
| 44 | + description, |
| 45 | + // Canonical URL prevents duplicate content from query params |
| 46 | + alternates: { |
| 47 | + canonical: `/claims/${claimId}`, |
| 48 | + }, |
| 49 | + openGraph: { |
| 50 | + title, |
| 51 | + // Description intentionally omits wallet addresses and sensitive claim details |
| 52 | + description: `Claim status: ${claim.status}. Cast your vote on this insurance claim.`, |
| 53 | + type: 'website', |
| 54 | + }, |
| 55 | + } |
| 56 | +} |
| 57 | + |
7 | 58 | /** |
8 | | - * /claims/[claimId] |
| 59 | + * /claims/[claimId] — server component. |
9 | 60 | * |
10 | | - * Wallet address and currentLedger are passed as props here. |
11 | | - * In production, replace the stubs below with your wallet context |
12 | | - * (e.g. Freighter / WalletConnect) and a Horizon ledger sequence fetch. |
| 61 | + * Fetches claim metadata server-side for SEO/OG without exposing |
| 62 | + * wallet-specific data in the initial HTML. Wallet connection and |
| 63 | + * eligibility checks happen client-side in ClaimVotePanel. |
13 | 64 | */ |
14 | 65 | export default async function ClaimPage({ params }: ClaimPageProps) { |
15 | 66 | const { claimId } = await params |
16 | 67 |
|
17 | | - // TODO: replace with wallet context hook (e.g. useFreighter / useWalletConnect) |
| 68 | + // Validate the claim exists; show 404 for unknown IDs |
| 69 | + const claim = await fetchClaimForMeta(claimId) |
| 70 | + if (!claim) { |
| 71 | + notFound() |
| 72 | + } |
| 73 | + |
| 74 | + // Wallet address and currentLedger are resolved client-side to avoid |
| 75 | + // exposing wallet-specific data in server-rendered HTML (issue #228 req). |
18 | 76 | const walletAddress: string | null = null |
19 | | - // TODO: replace with real ledger sequence from Horizon /fee_stats or polling |
20 | 77 | const currentLedger = 0 |
21 | 78 |
|
22 | 79 | return ( |
23 | 80 | <main |
24 | 81 | className="mx-auto max-w-2xl px-4 py-10 pb-[calc(2.5rem+env(safe-area-inset-bottom,0px))]" |
25 | | - style={{ paddingLeft: 'max(1rem, env(safe-area-inset-left, 0px))', paddingRight: 'max(1rem, env(safe-area-inset-right, 0px))' }} |
| 82 | + style={{ |
| 83 | + paddingLeft: 'max(1rem, env(safe-area-inset-left, 0px))', |
| 84 | + paddingRight: 'max(1rem, env(safe-area-inset-right, 0px))', |
| 85 | + }} |
26 | 86 | > |
27 | 87 | <h1 className="mb-6 text-xl font-bold"> |
28 | | - Claim vote - <span className="font-mono text-base">{claimId}</span> |
| 88 | + Claim vote — <span className="font-mono text-base">{claimId}</span> |
29 | 89 | </h1> |
30 | 90 | <ClaimVotePanel |
31 | 91 | claimId={claimId} |
|
0 commit comments