@orangecheck/vote-react 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/poll.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/use-tally.ts","../src/poll.tsx"],"names":["useState","useRef","useCallback","useEffect","useMemo","jsxs","jsx","memo"],"mappings":"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","file":"poll.js","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [data, setData] = useState<TallyResponse | null>(opts.initialData ?? null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(!opts.initialData);\n const pollIdRef = useRef(pollId);\n pollIdRef.current = pollId;\n\n const fetchOnce = useCallback(async () => {\n const pid = pollIdRef.current;\n if (!POLL_ID_RE.test(pid)) {\n setError(new Error('pollId must be 64 hex chars'));\n setLoading(false);\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n setData(body);\n setError(null);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n setLoading(false);\n }\n }, [baseUrl]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n return { data, error, loading, refetch: fetchOnce };\n}\n","'use client';\n\n/**\n * <OcPoll pollId=\"…\" />\n *\n * Full poll card: question, options as bars, turnout, status pill, \"cast a\n * ballot\" CTA that opens vote.ochk.io/p/<id> in a new tab.\n *\n * Read-only — the component doesn't sign anything. Voting requires a Bitcoin\n * wallet; we delegate that to the full web app at vote.ochk.io. Embed this\n * card in your own site / Discord webhook preview / docs page as the\n * canonical reference view.\n */\n\nimport type { CSSProperties } from 'react';\nimport { memo, useMemo } from 'react';\n\nimport { THEMES, type Theme, type TallyResponse } from './types.js';\nimport { useTally } from './use-tally.js';\n\nexport interface OcPollProps {\n /** 64-hex poll id. */\n pollId: string;\n /** Theme. Default `light`. */\n theme?: 'light' | 'dark';\n /** Override /api/tally origin. Default `https://vote.ochk.io`. */\n baseUrl?: string;\n /** Auto-refresh interval in ms. Default 60000. Set to 0 to disable. */\n refreshMs?: number;\n /** SSR-hydrated initial data. */\n initialData?: TallyResponse;\n /** Extra class on the outer element. */\n className?: string;\n /** Inline style override. */\n style?: CSSProperties;\n /** Hide the \"cast a ballot\" CTA (read-only mode). */\n hideCta?: boolean;\n}\n\nconst OcPollImpl = ({\n pollId,\n theme = 'light',\n baseUrl,\n refreshMs,\n initialData,\n className,\n style,\n hideCta = false,\n}: OcPollProps) => {\n const { data, error, loading } = useTally(pollId, {\n ...(baseUrl !== undefined ? { baseUrl } : {}),\n ...(refreshMs !== undefined ? { refreshMs } : {}),\n ...(initialData !== undefined ? { initialData } : {}),\n });\n const t = THEMES[theme];\n const resolvedBase = baseUrl ?? 'https://vote.ochk.io';\n const isPast = data?.deadline ? Date.parse(data.deadline) < Date.now() : false;\n\n const total = data?.turnout?.weight ?? 0;\n const sorted = useMemo(() => {\n if (!data?.tallies) return [] as Array<[string, number]>;\n return Object.entries(data.tallies).sort((a, b) => b[1] - a[1]);\n }, [data]);\n\n const outerStyle: CSSProperties = {\n border: `1px solid ${t.border}`,\n borderRadius: 8,\n background: t.bg,\n color: t.text,\n fontFamily:\n 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',\n fontSize: 14,\n lineHeight: 1.5,\n padding: 16,\n maxWidth: 520,\n ...style,\n };\n const monoStyle: CSSProperties = {\n fontFamily: 'ui-monospace, Menlo, monospace',\n fontSize: 11,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n color: t.muted,\n };\n\n return (\n <div className={className} style={outerStyle} data-slot=\"oc-poll\">\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>\n <div style={monoStyle}>oc vote · {data?.weight_mode?.replace('_', ' ') ?? '—'}</div>\n <StatusPill data={data} error={error} loading={loading} theme={t} />\n </div>\n <div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }}>\n {data?.question ?? (loading ? 'loading poll…' : error ? error.message : pollId.slice(0, 16) + '…')}\n </div>\n\n {data?.state === 'awaiting_reveal' ? (\n <div\n style={{\n padding: 10,\n border: `1px solid ${t.border}`,\n background: t.accentSoft,\n color: t.text,\n fontSize: 13,\n }}\n >\n <strong style={{ color: t.accent }}>awaiting reveal</strong> — the creator\n of this secret-mode poll has not yet published reveal_sk.{' '}\n <span style={{ color: t.muted }}>\n ballots observed: {data.ballot_count ?? 0}\n </span>\n </div>\n ) : data?.state === 'tallied' ? (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n {sorted.map(([opt, weight]) => {\n const pct = total > 0 ? (weight / total) * 100 : 0;\n return (\n <div key={opt}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: 13,\n marginBottom: 4,\n }}\n >\n <span style={{ fontWeight: 600 }}>{opt}</span>\n <span style={{ color: t.muted, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12 }}>\n {fmt(weight)} · {pct.toFixed(1)}%\n </span>\n </div>\n <div\n style={{\n height: 6,\n background: t.border,\n borderRadius: 3,\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${Math.max(2, pct)}%`,\n background: t.accent,\n }}\n />\n </div>\n </div>\n );\n })}\n {sorted.length === 0 && (\n <div style={{ color: t.muted, fontSize: 13 }}>no ballots yet</div>\n )}\n </div>\n ) : (\n <div style={{ color: t.muted, fontSize: 13 }}>\n {loading ? 'loading…' : error?.message ?? 'no data'}\n </div>\n )}\n\n <div\n style={{\n marginTop: 14,\n paddingTop: 10,\n borderTop: `1px solid ${t.border}`,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 8,\n fontSize: 12,\n color: t.muted,\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n <span>\n {data?.turnout?.voters ?? 0} voters\n {data?.deadline && ' · '}\n {data?.deadline && (isPast ? 'closed' : 'closes')}\n {data?.deadline && ' ' + data.deadline.slice(0, 16).replace('T', ' ')}\n </span>\n {!hideCta && !isPast && (\n <a\n href={`${resolvedBase}/p/${pollId}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{\n padding: '5px 10px',\n background: t.accent,\n color: '#0a0a0a',\n textDecoration: 'none',\n borderRadius: 4,\n fontWeight: 700,\n fontSize: 11,\n letterSpacing: '0.05em',\n textTransform: 'uppercase',\n }}\n >\n cast a ballot →\n </a>\n )}\n </div>\n </div>\n );\n};\n\nfunction StatusPill({\n data,\n error,\n loading,\n theme: t,\n}: {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n theme: Theme;\n}) {\n let label = '—';\n let color = t.muted;\n if (error) {\n label = 'error';\n color = '#ef4444';\n } else if (loading && !data) {\n label = 'loading';\n } else if (data?.state === 'awaiting_reveal') {\n label = 'awaiting reveal';\n color = t.accent;\n } else if (data?.state === 'tallied') {\n const isPast = data.deadline ? Date.parse(data.deadline) < Date.now() : false;\n label = isPast ? 'closed' : 'open';\n color = isPast ? t.muted : '#10b981';\n }\n return (\n <span\n style={{\n padding: '2px 8px',\n border: `1px solid ${color}`,\n color,\n borderRadius: 4,\n fontSize: 10,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n {label}\n </span>\n );\n}\n\nfunction fmt(n: number): string {\n return n.toLocaleString('en-US');\n}\n\nexport const OcPoll = memo(OcPollImpl);\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/use-tally.ts","../src/poll.tsx"],"names":["useState","useRef","useCallback","useEffect","useMemo","jsxs","jsx","memo"],"mappings":"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","file":"poll.js","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\n/** A poll's own content: what it asks and when it closes. Not a count, so it\n * outlives a failed refresh of the same poll. */\nexport interface PollContent {\n question?: string;\n deadline?: string;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n /** The last question and deadline read for this pollId, kept after a failed refresh. */\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\n/** What the hook holds, always for one pollId: nothing of another poll is ever returned. */\ninterface Entry {\n pollId: string;\n data: TallyResponse | null;\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n}\n\nfunction contentOf(d: TallyResponse | null | undefined): PollContent | null {\n if (!d || (d.question === undefined && d.deadline === undefined)) return null;\n return {\n ...(d.question !== undefined ? { question: d.question } : {}),\n ...(d.deadline !== undefined ? { deadline: d.deadline } : {}),\n };\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [entry, setEntry] = useState<Entry>(() => ({\n pollId,\n data: opts.initialData ?? null,\n poll: contentOf(opts.initialData),\n error: null,\n loading: !opts.initialData,\n }));\n\n // Only the latest request writes: an answer that lands after a newer request\n // (an earlier refresh, or the poll before a pollId change) is dropped.\n const latest = useRef(0);\n\n const fetchOnce = useCallback(async () => {\n // The poll this fetch is for; a later pollId keeps an answer for this one off screen.\n const pid = pollId;\n const seq = ++latest.current;\n const kept = (prev: Entry) => (prev.pollId === pid ? prev.poll : null);\n if (!POLL_ID_RE.test(pid)) {\n setEntry({ pollId: pid, data: null, poll: null, error: new Error('pollId must be 64 hex chars'), loading: false });\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n if (seq !== latest.current) return;\n setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));\n } catch (err) {\n if (seq !== latest.current) return;\n // The last tally is no longer the answer: never show it beside the error.\n const error = err instanceof Error ? err : new Error(String(err));\n setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));\n }\n }, [baseUrl, pollId]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n // Until this pollId's first answer, nothing of the last poll shows.\n const mine = entry.pollId === pollId;\n return {\n data: mine ? entry.data : null,\n poll: mine ? entry.poll : null,\n error: mine ? entry.error : null,\n loading: mine ? entry.loading : true,\n refetch: fetchOnce,\n };\n}\n","'use client';\n\n/**\n * <OcPoll pollId=\"…\" />\n *\n * Full poll card: question, options as bars, turnout, status pill, \"cast a\n * ballot\" CTA that opens vote.ochk.io/p/<id> in a new tab.\n *\n * Read-only — the component doesn't sign anything. Voting requires a Bitcoin\n * wallet; we delegate that to the full web app at vote.ochk.io. Embed this\n * card in your own site / Discord webhook preview / docs page as the\n * canonical reference view.\n */\n\nimport type { CSSProperties } from 'react';\nimport { memo, useMemo } from 'react';\n\nimport { THEMES, type Theme, type TallyResponse } from './types.js';\nimport { useTally } from './use-tally.js';\n\nexport interface OcPollProps {\n /** 64-hex poll id. */\n pollId: string;\n /** Theme. Default `light`. */\n theme?: 'light' | 'dark';\n /** Override /api/tally origin. Default `https://vote.ochk.io`. */\n baseUrl?: string;\n /** Auto-refresh interval in ms. Default 60000. Set to 0 to disable. */\n refreshMs?: number;\n /** SSR-hydrated initial data. */\n initialData?: TallyResponse;\n /** Extra class on the outer element. */\n className?: string;\n /** Inline style override. */\n style?: CSSProperties;\n /** Hide the \"cast a ballot\" CTA (read-only mode). */\n hideCta?: boolean;\n}\n\nconst OcPollImpl = ({\n pollId,\n theme = 'light',\n baseUrl,\n refreshMs,\n initialData,\n className,\n style,\n hideCta = false,\n}: OcPollProps) => {\n const { data, poll, error, loading } = useTally(pollId, {\n ...(baseUrl !== undefined ? { baseUrl } : {}),\n ...(refreshMs !== undefined ? { refreshMs } : {}),\n ...(initialData !== undefined ? { initialData } : {}),\n });\n const t = THEMES[theme];\n const resolvedBase = baseUrl ?? 'https://vote.ochk.io';\n // The question and deadline are the poll's content, not a count: they stay\n // after a failed refresh, while the tallies and turnout do not.\n const deadline = data?.deadline ?? poll?.deadline;\n const isPast = deadline ? Date.parse(deadline) < Date.now() : false;\n\n const total = data?.turnout?.weight ?? 0;\n const sorted = useMemo(() => {\n if (!data?.tallies) return [] as Array<[string, number]>;\n return Object.entries(data.tallies).sort((a, b) => b[1] - a[1]);\n }, [data]);\n\n const outerStyle: CSSProperties = {\n border: `1px solid ${t.border}`,\n borderRadius: 8,\n background: t.bg,\n color: t.text,\n fontFamily:\n 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',\n fontSize: 14,\n lineHeight: 1.5,\n padding: 16,\n maxWidth: 520,\n ...style,\n };\n const monoStyle: CSSProperties = {\n fontFamily: 'ui-monospace, Menlo, monospace',\n fontSize: 11,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n color: t.muted,\n };\n\n return (\n <div className={className} style={outerStyle} data-slot=\"oc-poll\">\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>\n <div style={monoStyle}>oc vote · {data?.weight_mode?.replace('_', ' ') ?? '—'}</div>\n <StatusPill data={data} error={error} loading={loading} theme={t} />\n </div>\n <div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }}>\n {data?.question ?? poll?.question ?? (loading ? 'loading poll…' : pollId.slice(0, 16) + '…')}\n </div>\n\n {data?.state === 'awaiting_reveal' ? (\n <div\n style={{\n padding: 10,\n border: `1px solid ${t.border}`,\n background: t.accentSoft,\n color: t.text,\n fontSize: 13,\n }}\n >\n <strong style={{ color: t.accent }}>awaiting reveal</strong> — the creator\n of this secret-mode poll has not yet published reveal_sk.{' '}\n <span style={{ color: t.muted }}>\n ballots observed: {data.ballot_count ?? 0}\n </span>\n </div>\n ) : data?.state === 'tallied' ? (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n {sorted.map(([opt, weight]) => {\n const pct = total > 0 ? (weight / total) * 100 : 0;\n return (\n <div key={opt}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: 13,\n marginBottom: 4,\n }}\n >\n <span style={{ fontWeight: 600 }}>{opt}</span>\n <span style={{ color: t.muted, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12 }}>\n {fmt(weight)} · {pct.toFixed(1)}%\n </span>\n </div>\n <div\n style={{\n height: 6,\n background: t.border,\n borderRadius: 3,\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${Math.max(2, pct)}%`,\n background: t.accent,\n }}\n />\n </div>\n </div>\n );\n })}\n {sorted.length === 0 && (\n <div style={{ color: t.muted, fontSize: 13 }}>no ballots yet</div>\n )}\n </div>\n ) : (\n <div style={{ color: t.muted, fontSize: 13 }}>\n {loading ? 'loading…' : error?.message ?? 'no data'}\n </div>\n )}\n\n <div\n style={{\n marginTop: 14,\n paddingTop: 10,\n borderTop: `1px solid ${t.border}`,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 8,\n fontSize: 12,\n color: t.muted,\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n <span>\n {data && `${data.turnout?.voters ?? 0} voters`}\n {data && deadline && ' · '}\n {deadline && (isPast ? 'closed' : 'closes')}\n {deadline && ' ' + deadline.slice(0, 16).replace('T', ' ')}\n </span>\n {!hideCta && (data || deadline) && !isPast && (\n <a\n href={`${resolvedBase}/p/${pollId}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{\n padding: '5px 10px',\n background: t.accent,\n color: '#0a0a0a',\n textDecoration: 'none',\n borderRadius: 4,\n fontWeight: 700,\n fontSize: 11,\n letterSpacing: '0.05em',\n textTransform: 'uppercase',\n }}\n >\n cast a ballot →\n </a>\n )}\n </div>\n </div>\n );\n};\n\nfunction StatusPill({\n data,\n error,\n loading,\n theme: t,\n}: {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n theme: Theme;\n}) {\n let label = '—';\n let color = t.muted;\n if (error) {\n label = 'error';\n color = '#ef4444';\n } else if (loading && !data) {\n label = 'loading';\n } else if (data?.state === 'awaiting_reveal') {\n label = 'awaiting reveal';\n color = t.accent;\n } else if (data?.state === 'tallied') {\n const isPast = data.deadline ? Date.parse(data.deadline) < Date.now() : false;\n label = isPast ? 'closed' : 'open';\n color = isPast ? t.muted : '#10b981';\n }\n return (\n <span\n {...(error ? { title: error.message, 'aria-label': `error: ${error.message}` } : {})}\n style={{\n padding: '2px 8px',\n border: `1px solid ${color}`,\n color,\n borderRadius: 4,\n fontSize: 10,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n {label}\n </span>\n );\n}\n\nfunction fmt(n: number): string {\n return n.toLocaleString('en-US');\n}\n\nexport const OcPoll = memo(OcPollImpl);\n"]}
package/dist/poll.mjs CHANGED
@@ -22,40 +22,58 @@ var THEMES = {
22
22
  };
23
23
  var DEFAULT_API_BASE = "https://vote.ochk.io";
24
24
  var POLL_ID_RE = /^[0-9a-f]{64}$/;
25
+ function contentOf(d) {
26
+ if (!d || d.question === void 0 && d.deadline === void 0) return null;
27
+ return {
28
+ ...d.question !== void 0 ? { question: d.question } : {},
29
+ ...d.deadline !== void 0 ? { deadline: d.deadline } : {}
30
+ };
31
+ }
25
32
  function useTally(pollId, opts = {}) {
26
33
  const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;
27
34
  const refreshMs = opts.refreshMs ?? 6e4;
28
- const [data, setData] = useState(opts.initialData ?? null);
29
- const [error, setError] = useState(null);
30
- const [loading, setLoading] = useState(!opts.initialData);
31
- const pollIdRef = useRef(pollId);
32
- pollIdRef.current = pollId;
35
+ const [entry, setEntry] = useState(() => ({
36
+ pollId,
37
+ data: opts.initialData ?? null,
38
+ poll: contentOf(opts.initialData),
39
+ error: null,
40
+ loading: !opts.initialData
41
+ }));
42
+ const latest = useRef(0);
33
43
  const fetchOnce = useCallback(async () => {
34
- const pid = pollIdRef.current;
44
+ const pid = pollId;
45
+ const seq = ++latest.current;
46
+ const kept = (prev) => prev.pollId === pid ? prev.poll : null;
35
47
  if (!POLL_ID_RE.test(pid)) {
36
- setError(new Error("pollId must be 64 hex chars"));
37
- setLoading(false);
48
+ setEntry({ pollId: pid, data: null, poll: null, error: new Error("pollId must be 64 hex chars"), loading: false });
38
49
  return;
39
50
  }
40
51
  try {
41
52
  const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);
42
53
  const body = await res.json();
43
54
  if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);
44
- setData(body);
45
- setError(null);
55
+ if (seq !== latest.current) return;
56
+ setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));
46
57
  } catch (err) {
47
- setError(err instanceof Error ? err : new Error(String(err)));
48
- } finally {
49
- setLoading(false);
58
+ if (seq !== latest.current) return;
59
+ const error = err instanceof Error ? err : new Error(String(err));
60
+ setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));
50
61
  }
51
- }, [baseUrl]);
62
+ }, [baseUrl, pollId]);
52
63
  useEffect(() => {
53
64
  void fetchOnce();
54
65
  if (refreshMs <= 0) return;
55
66
  const timer = setInterval(() => void fetchOnce(), refreshMs);
56
67
  return () => clearInterval(timer);
57
68
  }, [fetchOnce, refreshMs]);
58
- return { data, error, loading, refetch: fetchOnce };
69
+ const mine = entry.pollId === pollId;
70
+ return {
71
+ data: mine ? entry.data : null,
72
+ poll: mine ? entry.poll : null,
73
+ error: mine ? entry.error : null,
74
+ loading: mine ? entry.loading : true,
75
+ refetch: fetchOnce
76
+ };
59
77
  }
60
78
  var OcPollImpl = ({
61
79
  pollId,
@@ -67,14 +85,15 @@ var OcPollImpl = ({
67
85
  style,
68
86
  hideCta = false
69
87
  }) => {
70
- const { data, error, loading } = useTally(pollId, {
88
+ const { data, poll, error, loading } = useTally(pollId, {
71
89
  ...baseUrl !== void 0 ? { baseUrl } : {},
72
90
  ...refreshMs !== void 0 ? { refreshMs } : {},
73
91
  ...initialData !== void 0 ? { initialData } : {}
74
92
  });
75
93
  const t = THEMES[theme];
76
94
  const resolvedBase = baseUrl ?? "https://vote.ochk.io";
77
- const isPast = data?.deadline ? Date.parse(data.deadline) < Date.now() : false;
95
+ const deadline = data?.deadline ?? poll?.deadline;
96
+ const isPast = deadline ? Date.parse(deadline) < Date.now() : false;
78
97
  const total = data?.turnout?.weight ?? 0;
79
98
  const sorted = useMemo(() => {
80
99
  if (!data?.tallies) return [];
@@ -107,7 +126,7 @@ var OcPollImpl = ({
107
126
  ] }),
108
127
  /* @__PURE__ */ jsx(StatusPill, { data, error, loading, theme: t })
109
128
  ] }),
110
- /* @__PURE__ */ jsx("div", { style: { fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }, children: data?.question ?? (loading ? "loading poll\u2026" : error ? error.message : pollId.slice(0, 16) + "\u2026") }),
129
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }, children: data?.question ?? poll?.question ?? (loading ? "loading poll\u2026" : pollId.slice(0, 16) + "\u2026") }),
111
130
  data?.state === "awaiting_reveal" ? /* @__PURE__ */ jsxs(
112
131
  "div",
113
132
  {
@@ -194,13 +213,12 @@ var OcPollImpl = ({
194
213
  },
195
214
  children: [
196
215
  /* @__PURE__ */ jsxs("span", { children: [
197
- data?.turnout?.voters ?? 0,
198
- " voters",
199
- data?.deadline && " \xB7 ",
200
- data?.deadline && (isPast ? "closed" : "closes"),
201
- data?.deadline && " " + data.deadline.slice(0, 16).replace("T", " ")
216
+ data && `${data.turnout?.voters ?? 0} voters`,
217
+ data && deadline && " \xB7 ",
218
+ deadline && (isPast ? "closed" : "closes"),
219
+ deadline && " " + deadline.slice(0, 16).replace("T", " ")
202
220
  ] }),
203
- !hideCta && !isPast && /* @__PURE__ */ jsx(
221
+ !hideCta && (data || deadline) && !isPast && /* @__PURE__ */ jsx(
204
222
  "a",
205
223
  {
206
224
  href: `${resolvedBase}/p/${pollId}`,
@@ -249,6 +267,7 @@ function StatusPill({
249
267
  return /* @__PURE__ */ jsx(
250
268
  "span",
251
269
  {
270
+ ...error ? { title: error.message, "aria-label": `error: ${error.message}` } : {},
252
271
  style: {
253
272
  padding: "2px 8px",
254
273
  border: `1px solid ${color}`,
package/dist/poll.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/use-tally.ts","../src/poll.tsx"],"names":[],"mappings":"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","file":"poll.mjs","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [data, setData] = useState<TallyResponse | null>(opts.initialData ?? null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(!opts.initialData);\n const pollIdRef = useRef(pollId);\n pollIdRef.current = pollId;\n\n const fetchOnce = useCallback(async () => {\n const pid = pollIdRef.current;\n if (!POLL_ID_RE.test(pid)) {\n setError(new Error('pollId must be 64 hex chars'));\n setLoading(false);\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n setData(body);\n setError(null);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n setLoading(false);\n }\n }, [baseUrl]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n return { data, error, loading, refetch: fetchOnce };\n}\n","'use client';\n\n/**\n * <OcPoll pollId=\"…\" />\n *\n * Full poll card: question, options as bars, turnout, status pill, \"cast a\n * ballot\" CTA that opens vote.ochk.io/p/<id> in a new tab.\n *\n * Read-only — the component doesn't sign anything. Voting requires a Bitcoin\n * wallet; we delegate that to the full web app at vote.ochk.io. Embed this\n * card in your own site / Discord webhook preview / docs page as the\n * canonical reference view.\n */\n\nimport type { CSSProperties } from 'react';\nimport { memo, useMemo } from 'react';\n\nimport { THEMES, type Theme, type TallyResponse } from './types.js';\nimport { useTally } from './use-tally.js';\n\nexport interface OcPollProps {\n /** 64-hex poll id. */\n pollId: string;\n /** Theme. Default `light`. */\n theme?: 'light' | 'dark';\n /** Override /api/tally origin. Default `https://vote.ochk.io`. */\n baseUrl?: string;\n /** Auto-refresh interval in ms. Default 60000. Set to 0 to disable. */\n refreshMs?: number;\n /** SSR-hydrated initial data. */\n initialData?: TallyResponse;\n /** Extra class on the outer element. */\n className?: string;\n /** Inline style override. */\n style?: CSSProperties;\n /** Hide the \"cast a ballot\" CTA (read-only mode). */\n hideCta?: boolean;\n}\n\nconst OcPollImpl = ({\n pollId,\n theme = 'light',\n baseUrl,\n refreshMs,\n initialData,\n className,\n style,\n hideCta = false,\n}: OcPollProps) => {\n const { data, error, loading } = useTally(pollId, {\n ...(baseUrl !== undefined ? { baseUrl } : {}),\n ...(refreshMs !== undefined ? { refreshMs } : {}),\n ...(initialData !== undefined ? { initialData } : {}),\n });\n const t = THEMES[theme];\n const resolvedBase = baseUrl ?? 'https://vote.ochk.io';\n const isPast = data?.deadline ? Date.parse(data.deadline) < Date.now() : false;\n\n const total = data?.turnout?.weight ?? 0;\n const sorted = useMemo(() => {\n if (!data?.tallies) return [] as Array<[string, number]>;\n return Object.entries(data.tallies).sort((a, b) => b[1] - a[1]);\n }, [data]);\n\n const outerStyle: CSSProperties = {\n border: `1px solid ${t.border}`,\n borderRadius: 8,\n background: t.bg,\n color: t.text,\n fontFamily:\n 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',\n fontSize: 14,\n lineHeight: 1.5,\n padding: 16,\n maxWidth: 520,\n ...style,\n };\n const monoStyle: CSSProperties = {\n fontFamily: 'ui-monospace, Menlo, monospace',\n fontSize: 11,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n color: t.muted,\n };\n\n return (\n <div className={className} style={outerStyle} data-slot=\"oc-poll\">\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>\n <div style={monoStyle}>oc vote · {data?.weight_mode?.replace('_', ' ') ?? '—'}</div>\n <StatusPill data={data} error={error} loading={loading} theme={t} />\n </div>\n <div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }}>\n {data?.question ?? (loading ? 'loading poll…' : error ? error.message : pollId.slice(0, 16) + '…')}\n </div>\n\n {data?.state === 'awaiting_reveal' ? (\n <div\n style={{\n padding: 10,\n border: `1px solid ${t.border}`,\n background: t.accentSoft,\n color: t.text,\n fontSize: 13,\n }}\n >\n <strong style={{ color: t.accent }}>awaiting reveal</strong> — the creator\n of this secret-mode poll has not yet published reveal_sk.{' '}\n <span style={{ color: t.muted }}>\n ballots observed: {data.ballot_count ?? 0}\n </span>\n </div>\n ) : data?.state === 'tallied' ? (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n {sorted.map(([opt, weight]) => {\n const pct = total > 0 ? (weight / total) * 100 : 0;\n return (\n <div key={opt}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: 13,\n marginBottom: 4,\n }}\n >\n <span style={{ fontWeight: 600 }}>{opt}</span>\n <span style={{ color: t.muted, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12 }}>\n {fmt(weight)} · {pct.toFixed(1)}%\n </span>\n </div>\n <div\n style={{\n height: 6,\n background: t.border,\n borderRadius: 3,\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${Math.max(2, pct)}%`,\n background: t.accent,\n }}\n />\n </div>\n </div>\n );\n })}\n {sorted.length === 0 && (\n <div style={{ color: t.muted, fontSize: 13 }}>no ballots yet</div>\n )}\n </div>\n ) : (\n <div style={{ color: t.muted, fontSize: 13 }}>\n {loading ? 'loading…' : error?.message ?? 'no data'}\n </div>\n )}\n\n <div\n style={{\n marginTop: 14,\n paddingTop: 10,\n borderTop: `1px solid ${t.border}`,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 8,\n fontSize: 12,\n color: t.muted,\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n <span>\n {data?.turnout?.voters ?? 0} voters\n {data?.deadline && ' · '}\n {data?.deadline && (isPast ? 'closed' : 'closes')}\n {data?.deadline && ' ' + data.deadline.slice(0, 16).replace('T', ' ')}\n </span>\n {!hideCta && !isPast && (\n <a\n href={`${resolvedBase}/p/${pollId}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{\n padding: '5px 10px',\n background: t.accent,\n color: '#0a0a0a',\n textDecoration: 'none',\n borderRadius: 4,\n fontWeight: 700,\n fontSize: 11,\n letterSpacing: '0.05em',\n textTransform: 'uppercase',\n }}\n >\n cast a ballot →\n </a>\n )}\n </div>\n </div>\n );\n};\n\nfunction StatusPill({\n data,\n error,\n loading,\n theme: t,\n}: {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n theme: Theme;\n}) {\n let label = '—';\n let color = t.muted;\n if (error) {\n label = 'error';\n color = '#ef4444';\n } else if (loading && !data) {\n label = 'loading';\n } else if (data?.state === 'awaiting_reveal') {\n label = 'awaiting reveal';\n color = t.accent;\n } else if (data?.state === 'tallied') {\n const isPast = data.deadline ? Date.parse(data.deadline) < Date.now() : false;\n label = isPast ? 'closed' : 'open';\n color = isPast ? t.muted : '#10b981';\n }\n return (\n <span\n style={{\n padding: '2px 8px',\n border: `1px solid ${color}`,\n color,\n borderRadius: 4,\n fontSize: 10,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n {label}\n </span>\n );\n}\n\nfunction fmt(n: number): string {\n return n.toLocaleString('en-US');\n}\n\nexport const OcPoll = memo(OcPollImpl);\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/use-tally.ts","../src/poll.tsx"],"names":[],"mappings":";;;;AAkCO,IAAM,MAAA,GAA0C;AAAA,EACnD,KAAA,EAAO;AAAA,IACH,EAAA,EAAI,SAAA;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,MAAA,EAAQ,SAAA;AAAA,IACR,MAAA,EAAQ,SAAA;AAAA,IACR,UAAA,EAAY;AAAA,GAChB;AAAA,EACA,IAAA,EAAM;AAAA,IACF,EAAA,EAAI,SAAA;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,KAAA,EAAO,SAAA;AAAA,IACP,MAAA,EAAQ,SAAA;AAAA,IACR,MAAA,EAAQ,SAAA;AAAA,IACR,UAAA,EAAY;AAAA;AAEpB,CAAA;AAEO,IAAM,gBAAA,GAAmB,sBAAA;AAEzB,IAAM,UAAA,GAAa,gBAAA;ACJ1B,SAAS,UAAU,CAAA,EAAyD;AACxE,EAAA,IAAI,CAAC,KAAM,CAAA,CAAE,QAAA,KAAa,UAAa,CAAA,CAAE,QAAA,KAAa,QAAY,OAAO,IAAA;AACzE,EAAA,OAAO;AAAA,IACH,GAAI,EAAE,QAAA,KAAa,MAAA,GAAY,EAAE,QAAA,EAAU,CAAA,CAAE,QAAA,EAAS,GAAI,EAAC;AAAA,IAC3D,GAAI,EAAE,QAAA,KAAa,MAAA,GAAY,EAAE,QAAA,EAAU,CAAA,CAAE,QAAA,EAAS,GAAI;AAAC,GAC/D;AACJ;AAEO,SAAS,QAAA,CACZ,MAAA,EACA,IAAA,GAAwB,EAAC,EACZ;AACb,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,gBAAA;AAChC,EAAA,MAAM,SAAA,GAAY,KAAK,SAAA,IAAa,GAAA;AAEpC,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAgB,OAAO;AAAA,IAC7C,MAAA;AAAA,IACA,IAAA,EAAM,KAAK,WAAA,IAAe,IAAA;AAAA,IAC1B,IAAA,EAAM,SAAA,CAAU,IAAA,CAAK,WAAW,CAAA;AAAA,IAChC,KAAA,EAAO,IAAA;AAAA,IACP,OAAA,EAAS,CAAC,IAAA,CAAK;AAAA,GACnB,CAAE,CAAA;AAIF,EAAA,MAAM,MAAA,GAAS,OAAO,CAAC,CAAA;AAEvB,EAAA,MAAM,SAAA,GAAY,YAAY,YAAY;AAEtC,IAAA,MAAM,GAAA,GAAM,MAAA;AACZ,IAAA,MAAM,GAAA,GAAM,EAAE,MAAA,CAAO,OAAA;AACrB,IAAA,MAAM,OAAO,CAAC,IAAA,KAAiB,KAAK,MAAA,KAAW,GAAA,GAAM,KAAK,IAAA,GAAO,IAAA;AACjE,IAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,GAAG,CAAA,EAAG;AACvB,MAAA,QAAA,CAAS,EAAE,MAAA,EAAQ,GAAA,EAAK,IAAA,EAAM,MAAM,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,IAAI,KAAA,CAAM,6BAA6B,CAAA,EAAG,OAAA,EAAS,OAAO,CAAA;AACjH,MAAA;AAAA,IACJ;AACA,IAAA,IAAI;AACA,MAAA,MAAM,MAAM,MAAM,KAAA,CAAM,GAAG,OAAO,CAAA,gBAAA,EAAmB,GAAG,CAAA,CAAE,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,MAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAK,KAAA,IAAS,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC/D,MAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,MAAA,QAAA,CAAS,CAAC,IAAA,MAAU,EAAE,QAAQ,GAAA,EAAK,IAAA,EAAM,MAAM,IAAA,EAAM,SAAA,CAAU,IAAI,CAAA,IAAK,KAAK,IAAI,CAAA,EAAG,OAAO,IAAA,EAAM,OAAA,EAAS,OAAM,CAAE,CAAA;AAAA,IACtH,SAAS,GAAA,EAAK;AACV,MAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAE5B,MAAA,MAAM,KAAA,GAAQ,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAChE,MAAA,QAAA,CAAS,CAAC,IAAA,MAAU,EAAE,MAAA,EAAQ,KAAK,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,IAAA,CAAK,IAAI,CAAA,EAAG,KAAA,EAAO,OAAA,EAAS,OAAM,CAAE,CAAA;AAAA,IAC7F;AAAA,EACJ,CAAA,EAAG,CAAC,OAAA,EAAS,MAAM,CAAC,CAAA;AAEpB,EAAA,SAAA,CAAU,MAAM;AACZ,IAAA,KAAK,SAAA,EAAU;AACf,IAAA,IAAI,aAAa,CAAA,EAAG;AACpB,IAAA,MAAM,QAAQ,WAAA,CAAY,MAAM,KAAK,SAAA,IAAa,SAAS,CAAA;AAC3D,IAAA,OAAO,MAAM,cAAc,KAAK,CAAA;AAAA,EACpC,CAAA,EAAG,CAAC,SAAA,EAAW,SAAS,CAAC,CAAA;AAGzB,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,KAAW,MAAA;AAC9B,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,IAAA,GAAO,KAAA,CAAM,IAAA,GAAO,IAAA;AAAA,IAC1B,IAAA,EAAM,IAAA,GAAO,KAAA,CAAM,IAAA,GAAO,IAAA;AAAA,IAC1B,KAAA,EAAO,IAAA,GAAO,KAAA,CAAM,KAAA,GAAQ,IAAA;AAAA,IAC5B,OAAA,EAAS,IAAA,GAAO,KAAA,CAAM,OAAA,GAAU,IAAA;AAAA,IAChC,OAAA,EAAS;AAAA,GACb;AACJ;AC9EA,IAAM,aAAa,CAAC;AAAA,EAChB,MAAA;AAAA,EACA,KAAA,GAAQ,OAAA;AAAA,EACR,OAAA;AAAA,EACA,SAAA;AAAA,EACA,WAAA;AAAA,EACA,SAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA,GAAU;AACd,CAAA,KAAmB;AACf,EAAA,MAAM,EAAE,IAAA,EAAM,IAAA,EAAM,OAAO,OAAA,EAAQ,GAAI,SAAS,MAAA,EAAQ;AAAA,IACpD,GAAI,OAAA,KAAY,MAAA,GAAY,EAAE,OAAA,KAAY,EAAC;AAAA,IAC3C,GAAI,SAAA,KAAc,MAAA,GAAY,EAAE,SAAA,KAAc,EAAC;AAAA,IAC/C,GAAI,WAAA,KAAgB,MAAA,GAAY,EAAE,WAAA,KAAgB;AAAC,GACtD,CAAA;AACD,EAAA,MAAM,CAAA,GAAI,OAAO,KAAK,CAAA;AACtB,EAAA,MAAM,eAAe,OAAA,IAAW,sBAAA;AAGhC,EAAA,MAAM,QAAA,GAAW,IAAA,EAAM,QAAA,IAAY,IAAA,EAAM,QAAA;AACzC,EAAA,MAAM,MAAA,GAAS,WAAW,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA,GAAI,IAAA,CAAK,KAAI,GAAI,KAAA;AAE9D,EAAA,MAAM,KAAA,GAAQ,IAAA,EAAM,OAAA,EAAS,MAAA,IAAU,CAAA;AACvC,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAM;AACzB,IAAA,IAAI,CAAC,IAAA,EAAM,OAAA,EAAS,OAAO,EAAC;AAC5B,IAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,OAAO,EAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,CAAC,CAAA,GAAI,CAAA,CAAE,CAAC,CAAC,CAAA;AAAA,EAClE,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAA,MAAM,UAAA,GAA4B;AAAA,IAC9B,MAAA,EAAQ,CAAA,UAAA,EAAa,CAAA,CAAE,MAAM,CAAA,CAAA;AAAA,IAC7B,YAAA,EAAc,CAAA;AAAA,IACd,YAAY,CAAA,CAAE,EAAA;AAAA,IACd,OAAO,CAAA,CAAE,IAAA;AAAA,IACT,UAAA,EACI,yFAAA;AAAA,IACJ,QAAA,EAAU,EAAA;AAAA,IACV,UAAA,EAAY,GAAA;AAAA,IACZ,OAAA,EAAS,EAAA;AAAA,IACT,QAAA,EAAU,GAAA;AAAA,IACV,GAAG;AAAA,GACP;AACA,EAAA,MAAM,SAAA,GAA2B;AAAA,IAC7B,UAAA,EAAY,gCAAA;AAAA,IACZ,QAAA,EAAU,EAAA;AAAA,IACV,aAAA,EAAe,QAAA;AAAA,IACf,aAAA,EAAe,WAAA;AAAA,IACf,OAAO,CAAA,CAAE;AAAA,GACb;AAEA,EAAA,4BACK,KAAA,EAAA,EAAI,SAAA,EAAsB,KAAA,EAAO,UAAA,EAAY,aAAU,SAAA,EACpD,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAU,cAAA,EAAgB,eAAA,EAAiB,GAAA,EAAK,CAAA,EAAG,YAAA,EAAc,GAAE,EAC1G,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,OAAO,SAAA,EAAW,QAAA,EAAA;AAAA,QAAA,eAAA;AAAA,QAAW,IAAA,EAAM,WAAA,EAAa,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA,IAAK;AAAA,OAAA,EAAI,CAAA;AAAA,0BAC7E,UAAA,EAAA,EAAW,IAAA,EAAY,KAAA,EAAc,OAAA,EAAkB,OAAO,CAAA,EAAG;AAAA,KAAA,EACtE,CAAA;AAAA,oBACA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,EAAE,QAAA,EAAU,IAAI,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,IAAA,EAAM,YAAA,EAAc,EAAA,IACxE,QAAA,EAAA,IAAA,EAAM,QAAA,IAAY,IAAA,EAAM,QAAA,KAAa,OAAA,GAAU,oBAAA,GAAkB,OAAO,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,QAAA,CAAA,EAC5F,CAAA;AAAA,IAEC,IAAA,EAAM,UAAU,iBAAA,mBACb,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,KAAA,EAAO;AAAA,UACH,OAAA,EAAS,EAAA;AAAA,UACT,MAAA,EAAQ,CAAA,UAAA,EAAa,CAAA,CAAE,MAAM,CAAA,CAAA;AAAA,UAC7B,YAAY,CAAA,CAAE,UAAA;AAAA,UACd,OAAO,CAAA,CAAE,IAAA;AAAA,UACT,QAAA,EAAU;AAAA,SACd;AAAA,QAEA,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,YAAO,KAAA,EAAO,EAAE,OAAO,CAAA,CAAE,MAAA,IAAU,QAAA,EAAA,iBAAA,EAAe,CAAA;AAAA,UAAS,+EAAA;AAAA,UACF,GAAA;AAAA,+BACzD,MAAA,EAAA,EAAK,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,CAAE,OAAM,EAAG,QAAA,EAAA;AAAA,YAAA,oBAAA;AAAA,YACV,KAAK,YAAA,IAAgB;AAAA,WAAA,EAC5C;AAAA;AAAA;AAAA,KACJ,GACA,IAAA,EAAM,KAAA,KAAU,SAAA,wBACf,KAAA,EAAA,EAAI,KAAA,EAAO,EAAE,OAAA,EAAS,MAAA,EAAQ,aAAA,EAAe,QAAA,EAAU,GAAA,EAAK,GAAE,EAC1D,QAAA,EAAA;AAAA,MAAA,MAAA,CAAO,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,MAAM,CAAA,KAAM;AAC3B,QAAA,MAAM,GAAA,GAAM,KAAA,GAAQ,CAAA,GAAK,MAAA,GAAS,QAAS,GAAA,GAAM,CAAA;AACjD,QAAA,4BACK,KAAA,EAAA,EACG,QAAA,EAAA;AAAA,0BAAA,IAAA;AAAA,YAAC,KAAA;AAAA,YAAA;AAAA,cACG,KAAA,EAAO;AAAA,gBACH,OAAA,EAAS,MAAA;AAAA,gBACT,cAAA,EAAgB,eAAA;AAAA,gBAChB,QAAA,EAAU,EAAA;AAAA,gBACV,YAAA,EAAc;AAAA,eAClB;AAAA,cAEA,QAAA,EAAA;AAAA,gCAAA,GAAA,CAAC,UAAK,KAAA,EAAO,EAAE,UAAA,EAAY,GAAA,IAAQ,QAAA,EAAA,GAAA,EAAI,CAAA;AAAA,gCACvC,IAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,CAAE,KAAA,EAAO,UAAA,EAAY,gCAAA,EAAkC,QAAA,EAAU,EAAA,EAAG,EACrF,QAAA,EAAA;AAAA,kBAAA,GAAA,CAAI,MAAM,CAAA;AAAA,kBAAE,QAAA;AAAA,kBAAI,GAAA,CAAI,QAAQ,CAAC,CAAA;AAAA,kBAAE;AAAA,iBAAA,EACpC;AAAA;AAAA;AAAA,WACJ;AAAA,0BACA,GAAA;AAAA,YAAC,KAAA;AAAA,YAAA;AAAA,cACG,KAAA,EAAO;AAAA,gBACH,MAAA,EAAQ,CAAA;AAAA,gBACR,YAAY,CAAA,CAAE,MAAA;AAAA,gBACd,YAAA,EAAc,CAAA;AAAA,gBACd,QAAA,EAAU;AAAA,eACd;AAAA,cAEA,QAAA,kBAAA,GAAA;AAAA,gBAAC,KAAA;AAAA,gBAAA;AAAA,kBACG,KAAA,EAAO;AAAA,oBACH,MAAA,EAAQ,MAAA;AAAA,oBACR,OAAO,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAG,CAAC,CAAA,CAAA,CAAA;AAAA,oBAC1B,YAAY,CAAA,CAAE;AAAA;AAClB;AAAA;AACJ;AAAA;AACJ,SAAA,EAAA,EA7BM,GA8BV,CAAA;AAAA,MAER,CAAC,CAAA;AAAA,MACA,MAAA,CAAO,MAAA,KAAW,CAAA,oBACf,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,CAAE,KAAA,EAAO,QAAA,EAAU,EAAA,IAAM,QAAA,EAAA,gBAAA,EAAc;AAAA,KAAA,EAEpE,CAAA,mBAEA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,EAAE,KAAA,EAAO,CAAA,CAAE,KAAA,EAAO,QAAA,EAAU,IAAG,EACtC,QAAA,EAAA,OAAA,GAAU,eAAA,GAAa,KAAA,EAAO,WAAW,SAAA,EAC9C,CAAA;AAAA,oBAGJ,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACG,KAAA,EAAO;AAAA,UACH,SAAA,EAAW,EAAA;AAAA,UACX,UAAA,EAAY,EAAA;AAAA,UACZ,SAAA,EAAW,CAAA,UAAA,EAAa,CAAA,CAAE,MAAM,CAAA,CAAA;AAAA,UAChC,OAAA,EAAS,MAAA;AAAA,UACT,UAAA,EAAY,QAAA;AAAA,UACZ,cAAA,EAAgB,eAAA;AAAA,UAChB,GAAA,EAAK,CAAA;AAAA,UACL,QAAA,EAAU,EAAA;AAAA,UACV,OAAO,CAAA,CAAE,KAAA;AAAA,UACT,UAAA,EAAY;AAAA,SAChB;AAAA,QAEA,QAAA,EAAA;AAAA,0BAAA,IAAA,CAAC,MAAA,EAAA,EACI,QAAA,EAAA;AAAA,YAAA,IAAA,IAAQ,CAAA,EAAG,IAAA,CAAK,OAAA,EAAS,MAAA,IAAU,CAAC,CAAA,OAAA,CAAA;AAAA,YACpC,QAAQ,QAAA,IAAY,QAAA;AAAA,YACpB,QAAA,KAAa,SAAS,QAAA,GAAW,QAAA,CAAA;AAAA,YACjC,QAAA,IAAY,MAAM,QAAA,CAAS,KAAA,CAAM,GAAG,EAAE,CAAA,CAAE,OAAA,CAAQ,GAAA,EAAK,GAAG;AAAA,WAAA,EAC7D,CAAA;AAAA,UACC,CAAC,OAAA,KAAY,IAAA,IAAQ,QAAA,CAAA,IAAa,CAAC,MAAA,oBAChC,GAAA;AAAA,YAAC,GAAA;AAAA,YAAA;AAAA,cACG,IAAA,EAAM,CAAA,EAAG,YAAY,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA;AAAA,cACjC,MAAA,EAAO,QAAA;AAAA,cACP,GAAA,EAAI,qBAAA;AAAA,cACJ,KAAA,EAAO;AAAA,gBACH,OAAA,EAAS,UAAA;AAAA,gBACT,YAAY,CAAA,CAAE,MAAA;AAAA,gBACd,KAAA,EAAO,SAAA;AAAA,gBACP,cAAA,EAAgB,MAAA;AAAA,gBAChB,YAAA,EAAc,CAAA;AAAA,gBACd,UAAA,EAAY,GAAA;AAAA,gBACZ,QAAA,EAAU,EAAA;AAAA,gBACV,aAAA,EAAe,QAAA;AAAA,gBACf,aAAA,EAAe;AAAA,eACnB;AAAA,cACH,QAAA,EAAA;AAAA;AAAA;AAED;AAAA;AAAA;AAER,GAAA,EACJ,CAAA;AAER,CAAA;AAEA,SAAS,UAAA,CAAW;AAAA,EAChB,IAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA,EAAO;AACX,CAAA,EAKG;AACC,EAAA,IAAI,KAAA,GAAQ,QAAA;AACZ,EAAA,IAAI,QAAQ,CAAA,CAAE,KAAA;AACd,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,KAAA,GAAQ,OAAA;AACR,IAAA,KAAA,GAAQ,SAAA;AAAA,EACZ,CAAA,MAAA,IAAW,OAAA,IAAW,CAAC,IAAA,EAAM;AACzB,IAAA,KAAA,GAAQ,SAAA;AAAA,EACZ,CAAA,MAAA,IAAW,IAAA,EAAM,KAAA,KAAU,iBAAA,EAAmB;AAC1C,IAAA,KAAA,GAAQ,iBAAA;AACR,IAAA,KAAA,GAAQ,CAAA,CAAE,MAAA;AAAA,EACd,CAAA,MAAA,IAAW,IAAA,EAAM,KAAA,KAAU,SAAA,EAAW;AAClC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,KAAK,QAAQ,CAAA,GAAI,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA;AACxE,IAAA,KAAA,GAAQ,SAAS,QAAA,GAAW,MAAA;AAC5B,IAAA,KAAA,GAAQ,MAAA,GAAS,EAAE,KAAA,GAAQ,SAAA;AAAA,EAC/B;AACA,EAAA,uBACI,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACI,GAAI,KAAA,GAAQ,EAAE,KAAA,EAAO,KAAA,CAAM,OAAA,EAAS,YAAA,EAAc,CAAA,OAAA,EAAU,KAAA,CAAM,OAAO,CAAA,CAAA,EAAG,GAAI,EAAC;AAAA,MAClF,KAAA,EAAO;AAAA,QACH,OAAA,EAAS,SAAA;AAAA,QACT,MAAA,EAAQ,aAAa,KAAK,CAAA,CAAA;AAAA,QAC1B,KAAA;AAAA,QACA,YAAA,EAAc,CAAA;AAAA,QACd,QAAA,EAAU,EAAA;AAAA,QACV,aAAA,EAAe,QAAA;AAAA,QACf,aAAA,EAAe,WAAA;AAAA,QACf,UAAA,EAAY;AAAA,OAChB;AAAA,MAEC,QAAA,EAAA;AAAA;AAAA,GACL;AAER;AAEA,SAAS,IAAI,CAAA,EAAmB;AAC5B,EAAA,OAAO,CAAA,CAAE,eAAe,OAAO,CAAA;AACnC;AAEO,IAAM,MAAA,GAAS,KAAK,UAAU","file":"poll.mjs","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\n/** A poll's own content: what it asks and when it closes. Not a count, so it\n * outlives a failed refresh of the same poll. */\nexport interface PollContent {\n question?: string;\n deadline?: string;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n /** The last question and deadline read for this pollId, kept after a failed refresh. */\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\n/** What the hook holds, always for one pollId: nothing of another poll is ever returned. */\ninterface Entry {\n pollId: string;\n data: TallyResponse | null;\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n}\n\nfunction contentOf(d: TallyResponse | null | undefined): PollContent | null {\n if (!d || (d.question === undefined && d.deadline === undefined)) return null;\n return {\n ...(d.question !== undefined ? { question: d.question } : {}),\n ...(d.deadline !== undefined ? { deadline: d.deadline } : {}),\n };\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [entry, setEntry] = useState<Entry>(() => ({\n pollId,\n data: opts.initialData ?? null,\n poll: contentOf(opts.initialData),\n error: null,\n loading: !opts.initialData,\n }));\n\n // Only the latest request writes: an answer that lands after a newer request\n // (an earlier refresh, or the poll before a pollId change) is dropped.\n const latest = useRef(0);\n\n const fetchOnce = useCallback(async () => {\n // The poll this fetch is for; a later pollId keeps an answer for this one off screen.\n const pid = pollId;\n const seq = ++latest.current;\n const kept = (prev: Entry) => (prev.pollId === pid ? prev.poll : null);\n if (!POLL_ID_RE.test(pid)) {\n setEntry({ pollId: pid, data: null, poll: null, error: new Error('pollId must be 64 hex chars'), loading: false });\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n if (seq !== latest.current) return;\n setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));\n } catch (err) {\n if (seq !== latest.current) return;\n // The last tally is no longer the answer: never show it beside the error.\n const error = err instanceof Error ? err : new Error(String(err));\n setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));\n }\n }, [baseUrl, pollId]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n // Until this pollId's first answer, nothing of the last poll shows.\n const mine = entry.pollId === pollId;\n return {\n data: mine ? entry.data : null,\n poll: mine ? entry.poll : null,\n error: mine ? entry.error : null,\n loading: mine ? entry.loading : true,\n refetch: fetchOnce,\n };\n}\n","'use client';\n\n/**\n * <OcPoll pollId=\"…\" />\n *\n * Full poll card: question, options as bars, turnout, status pill, \"cast a\n * ballot\" CTA that opens vote.ochk.io/p/<id> in a new tab.\n *\n * Read-only — the component doesn't sign anything. Voting requires a Bitcoin\n * wallet; we delegate that to the full web app at vote.ochk.io. Embed this\n * card in your own site / Discord webhook preview / docs page as the\n * canonical reference view.\n */\n\nimport type { CSSProperties } from 'react';\nimport { memo, useMemo } from 'react';\n\nimport { THEMES, type Theme, type TallyResponse } from './types.js';\nimport { useTally } from './use-tally.js';\n\nexport interface OcPollProps {\n /** 64-hex poll id. */\n pollId: string;\n /** Theme. Default `light`. */\n theme?: 'light' | 'dark';\n /** Override /api/tally origin. Default `https://vote.ochk.io`. */\n baseUrl?: string;\n /** Auto-refresh interval in ms. Default 60000. Set to 0 to disable. */\n refreshMs?: number;\n /** SSR-hydrated initial data. */\n initialData?: TallyResponse;\n /** Extra class on the outer element. */\n className?: string;\n /** Inline style override. */\n style?: CSSProperties;\n /** Hide the \"cast a ballot\" CTA (read-only mode). */\n hideCta?: boolean;\n}\n\nconst OcPollImpl = ({\n pollId,\n theme = 'light',\n baseUrl,\n refreshMs,\n initialData,\n className,\n style,\n hideCta = false,\n}: OcPollProps) => {\n const { data, poll, error, loading } = useTally(pollId, {\n ...(baseUrl !== undefined ? { baseUrl } : {}),\n ...(refreshMs !== undefined ? { refreshMs } : {}),\n ...(initialData !== undefined ? { initialData } : {}),\n });\n const t = THEMES[theme];\n const resolvedBase = baseUrl ?? 'https://vote.ochk.io';\n // The question and deadline are the poll's content, not a count: they stay\n // after a failed refresh, while the tallies and turnout do not.\n const deadline = data?.deadline ?? poll?.deadline;\n const isPast = deadline ? Date.parse(deadline) < Date.now() : false;\n\n const total = data?.turnout?.weight ?? 0;\n const sorted = useMemo(() => {\n if (!data?.tallies) return [] as Array<[string, number]>;\n return Object.entries(data.tallies).sort((a, b) => b[1] - a[1]);\n }, [data]);\n\n const outerStyle: CSSProperties = {\n border: `1px solid ${t.border}`,\n borderRadius: 8,\n background: t.bg,\n color: t.text,\n fontFamily:\n 'ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',\n fontSize: 14,\n lineHeight: 1.5,\n padding: 16,\n maxWidth: 520,\n ...style,\n };\n const monoStyle: CSSProperties = {\n fontFamily: 'ui-monospace, Menlo, monospace',\n fontSize: 11,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n color: t.muted,\n };\n\n return (\n <div className={className} style={outerStyle} data-slot=\"oc-poll\">\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>\n <div style={monoStyle}>oc vote · {data?.weight_mode?.replace('_', ' ') ?? '—'}</div>\n <StatusPill data={data} error={error} loading={loading} theme={t} />\n </div>\n <div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.25, marginBottom: 10 }}>\n {data?.question ?? poll?.question ?? (loading ? 'loading poll…' : pollId.slice(0, 16) + '…')}\n </div>\n\n {data?.state === 'awaiting_reveal' ? (\n <div\n style={{\n padding: 10,\n border: `1px solid ${t.border}`,\n background: t.accentSoft,\n color: t.text,\n fontSize: 13,\n }}\n >\n <strong style={{ color: t.accent }}>awaiting reveal</strong> — the creator\n of this secret-mode poll has not yet published reveal_sk.{' '}\n <span style={{ color: t.muted }}>\n ballots observed: {data.ballot_count ?? 0}\n </span>\n </div>\n ) : data?.state === 'tallied' ? (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>\n {sorted.map(([opt, weight]) => {\n const pct = total > 0 ? (weight / total) * 100 : 0;\n return (\n <div key={opt}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: 13,\n marginBottom: 4,\n }}\n >\n <span style={{ fontWeight: 600 }}>{opt}</span>\n <span style={{ color: t.muted, fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 12 }}>\n {fmt(weight)} · {pct.toFixed(1)}%\n </span>\n </div>\n <div\n style={{\n height: 6,\n background: t.border,\n borderRadius: 3,\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${Math.max(2, pct)}%`,\n background: t.accent,\n }}\n />\n </div>\n </div>\n );\n })}\n {sorted.length === 0 && (\n <div style={{ color: t.muted, fontSize: 13 }}>no ballots yet</div>\n )}\n </div>\n ) : (\n <div style={{ color: t.muted, fontSize: 13 }}>\n {loading ? 'loading…' : error?.message ?? 'no data'}\n </div>\n )}\n\n <div\n style={{\n marginTop: 14,\n paddingTop: 10,\n borderTop: `1px solid ${t.border}`,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 8,\n fontSize: 12,\n color: t.muted,\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n <span>\n {data && `${data.turnout?.voters ?? 0} voters`}\n {data && deadline && ' · '}\n {deadline && (isPast ? 'closed' : 'closes')}\n {deadline && ' ' + deadline.slice(0, 16).replace('T', ' ')}\n </span>\n {!hideCta && (data || deadline) && !isPast && (\n <a\n href={`${resolvedBase}/p/${pollId}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{\n padding: '5px 10px',\n background: t.accent,\n color: '#0a0a0a',\n textDecoration: 'none',\n borderRadius: 4,\n fontWeight: 700,\n fontSize: 11,\n letterSpacing: '0.05em',\n textTransform: 'uppercase',\n }}\n >\n cast a ballot →\n </a>\n )}\n </div>\n </div>\n );\n};\n\nfunction StatusPill({\n data,\n error,\n loading,\n theme: t,\n}: {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n theme: Theme;\n}) {\n let label = '—';\n let color = t.muted;\n if (error) {\n label = 'error';\n color = '#ef4444';\n } else if (loading && !data) {\n label = 'loading';\n } else if (data?.state === 'awaiting_reveal') {\n label = 'awaiting reveal';\n color = t.accent;\n } else if (data?.state === 'tallied') {\n const isPast = data.deadline ? Date.parse(data.deadline) < Date.now() : false;\n label = isPast ? 'closed' : 'open';\n color = isPast ? t.muted : '#10b981';\n }\n return (\n <span\n {...(error ? { title: error.message, 'aria-label': `error: ${error.message}` } : {})}\n style={{\n padding: '2px 8px',\n border: `1px solid ${color}`,\n color,\n borderRadius: 4,\n fontSize: 10,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n fontFamily: 'ui-monospace, Menlo, monospace',\n }}\n >\n {label}\n </span>\n );\n}\n\nfunction fmt(n: number): string {\n return n.toLocaleString('en-US');\n}\n\nexport const OcPoll = memo(OcPollImpl);\n"]}
@@ -5,12 +5,17 @@ interface UseTallyOptions {
5
5
  refreshMs?: number;
6
6
  initialData?: TallyResponse;
7
7
  }
8
+ interface PollContent {
9
+ question?: string;
10
+ deadline?: string;
11
+ }
8
12
  interface UseTallyState {
9
13
  data: TallyResponse | null;
14
+ poll: PollContent | null;
10
15
  error: Error | null;
11
16
  loading: boolean;
12
17
  refetch: () => void;
13
18
  }
14
19
  declare function useTally(pollId: string, opts?: UseTallyOptions): UseTallyState;
15
20
 
16
- export { type UseTallyOptions, type UseTallyState, useTally };
21
+ export { type PollContent, type UseTallyOptions, type UseTallyState, useTally };
@@ -5,12 +5,17 @@ interface UseTallyOptions {
5
5
  refreshMs?: number;
6
6
  initialData?: TallyResponse;
7
7
  }
8
+ interface PollContent {
9
+ question?: string;
10
+ deadline?: string;
11
+ }
8
12
  interface UseTallyState {
9
13
  data: TallyResponse | null;
14
+ poll: PollContent | null;
10
15
  error: Error | null;
11
16
  loading: boolean;
12
17
  refetch: () => void;
13
18
  }
14
19
  declare function useTally(pollId: string, opts?: UseTallyOptions): UseTallyState;
15
20
 
16
- export { type UseTallyOptions, type UseTallyState, useTally };
21
+ export { type PollContent, type UseTallyOptions, type UseTallyState, useTally };
package/dist/use-tally.js CHANGED
@@ -7,40 +7,58 @@ var DEFAULT_API_BASE = "https://vote.ochk.io";
7
7
  var POLL_ID_RE = /^[0-9a-f]{64}$/;
8
8
 
9
9
  // src/use-tally.ts
10
+ function contentOf(d) {
11
+ if (!d || d.question === void 0 && d.deadline === void 0) return null;
12
+ return {
13
+ ...d.question !== void 0 ? { question: d.question } : {},
14
+ ...d.deadline !== void 0 ? { deadline: d.deadline } : {}
15
+ };
16
+ }
10
17
  function useTally(pollId, opts = {}) {
11
18
  const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;
12
19
  const refreshMs = opts.refreshMs ?? 6e4;
13
- const [data, setData] = react.useState(opts.initialData ?? null);
14
- const [error, setError] = react.useState(null);
15
- const [loading, setLoading] = react.useState(!opts.initialData);
16
- const pollIdRef = react.useRef(pollId);
17
- pollIdRef.current = pollId;
20
+ const [entry, setEntry] = react.useState(() => ({
21
+ pollId,
22
+ data: opts.initialData ?? null,
23
+ poll: contentOf(opts.initialData),
24
+ error: null,
25
+ loading: !opts.initialData
26
+ }));
27
+ const latest = react.useRef(0);
18
28
  const fetchOnce = react.useCallback(async () => {
19
- const pid = pollIdRef.current;
29
+ const pid = pollId;
30
+ const seq = ++latest.current;
31
+ const kept = (prev) => prev.pollId === pid ? prev.poll : null;
20
32
  if (!POLL_ID_RE.test(pid)) {
21
- setError(new Error("pollId must be 64 hex chars"));
22
- setLoading(false);
33
+ setEntry({ pollId: pid, data: null, poll: null, error: new Error("pollId must be 64 hex chars"), loading: false });
23
34
  return;
24
35
  }
25
36
  try {
26
37
  const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);
27
38
  const body = await res.json();
28
39
  if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);
29
- setData(body);
30
- setError(null);
40
+ if (seq !== latest.current) return;
41
+ setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));
31
42
  } catch (err) {
32
- setError(err instanceof Error ? err : new Error(String(err)));
33
- } finally {
34
- setLoading(false);
43
+ if (seq !== latest.current) return;
44
+ const error = err instanceof Error ? err : new Error(String(err));
45
+ setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));
35
46
  }
36
- }, [baseUrl]);
47
+ }, [baseUrl, pollId]);
37
48
  react.useEffect(() => {
38
49
  void fetchOnce();
39
50
  if (refreshMs <= 0) return;
40
51
  const timer = setInterval(() => void fetchOnce(), refreshMs);
41
52
  return () => clearInterval(timer);
42
53
  }, [fetchOnce, refreshMs]);
43
- return { data, error, loading, refetch: fetchOnce };
54
+ const mine = entry.pollId === pollId;
55
+ return {
56
+ data: mine ? entry.data : null,
57
+ poll: mine ? entry.poll : null,
58
+ error: mine ? entry.error : null,
59
+ loading: mine ? entry.loading : true,
60
+ refetch: fetchOnce
61
+ };
44
62
  }
45
63
 
46
64
  exports.useTally = useTally;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/use-tally.ts"],"names":["useState","useRef","useCallback","useEffect"],"mappings":";;;;;AAqDO,IAAM,gBAAA,GAAmB,sBAAA;AAEzB,IAAM,UAAA,GAAa,gBAAA;;;ACtBnB,SAAS,QAAA,CACZ,MAAA,EACA,IAAA,GAAwB,EAAC,EACZ;AACb,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,gBAAA;AAChC,EAAA,MAAM,SAAA,GAAY,KAAK,SAAA,IAAa,GAAA;AAEpC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,IAAIA,cAAA,CAA+B,IAAA,CAAK,eAAe,IAAI,CAAA;AAC/E,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAuB,IAAI,CAAA;AACrD,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,IAAIA,cAAA,CAAS,CAAC,KAAK,WAAW,CAAA;AACxD,EAAA,MAAM,SAAA,GAAYC,aAAO,MAAM,CAAA;AAC/B,EAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAEpB,EAAA,MAAM,SAAA,GAAYC,kBAAY,YAAY;AACtC,IAAA,MAAM,MAAM,SAAA,CAAU,OAAA;AACtB,IAAA,IAAI,CAAC,UAAA,CAAW,IAAA,CAAK,GAAG,CAAA,EAAG;AACvB,MAAA,QAAA,CAAS,IAAI,KAAA,CAAM,6BAA6B,CAAC,CAAA;AACjD,MAAA,UAAA,CAAW,KAAK,CAAA;AAChB,MAAA;AAAA,IACJ;AACA,IAAA,IAAI;AACA,MAAA,MAAM,MAAM,MAAM,KAAA,CAAM,GAAG,OAAO,CAAA,gBAAA,EAAmB,GAAG,CAAA,CAAE,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,MAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAK,KAAA,IAAS,CAAA,KAAA,EAAQ,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAC/D,MAAA,OAAA,CAAQ,IAAI,CAAA;AACZ,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACjB,SAAS,GAAA,EAAK;AACV,MAAA,QAAA,CAAS,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAAA,IAChE,CAAA,SAAE;AACE,MAAA,UAAA,CAAW,KAAK,CAAA;AAAA,IACpB;AAAA,EACJ,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAAC,eAAA,CAAU,MAAM;AACZ,IAAA,KAAK,SAAA,EAAU;AACf,IAAA,IAAI,aAAa,CAAA,EAAG;AACpB,IAAA,MAAM,QAAQ,WAAA,CAAY,MAAM,KAAK,SAAA,IAAa,SAAS,CAAA;AAC3D,IAAA,OAAO,MAAM,cAAc,KAAK,CAAA;AAAA,EACpC,CAAA,EAAG,CAAC,SAAA,EAAW,SAAS,CAAC,CAAA;AAEzB,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,OAAA,EAAS,SAAS,SAAA,EAAU;AACtD","file":"use-tally.js","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [data, setData] = useState<TallyResponse | null>(opts.initialData ?? null);\n const [error, setError] = useState<Error | null>(null);\n const [loading, setLoading] = useState(!opts.initialData);\n const pollIdRef = useRef(pollId);\n pollIdRef.current = pollId;\n\n const fetchOnce = useCallback(async () => {\n const pid = pollIdRef.current;\n if (!POLL_ID_RE.test(pid)) {\n setError(new Error('pollId must be 64 hex chars'));\n setLoading(false);\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n setData(body);\n setError(null);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n setLoading(false);\n }\n }, [baseUrl]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n return { data, error, loading, refetch: fetchOnce };\n}\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/use-tally.ts"],"names":["useState","useRef","useCallback","useEffect"],"mappings":"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","file":"use-tally.js","sourcesContent":["// Wire types for the /api/tally response.\n// Deliberately a subset of what vote.ochk.io returns — we don't re-export\n// every field, just what the components render.\n\nexport interface TallyTurnout {\n voters: number;\n weight: number;\n}\n\nexport interface TallyResponse {\n poll_id: string;\n state: 'tallied' | 'awaiting_reveal';\n question?: string;\n creator?: string;\n deadline?: string;\n weight_mode?: string;\n mode?: 'public' | 'secret';\n snapshot_block?: number;\n turnout?: TallyTurnout;\n tallies?: Record<string, number>;\n ballot_count?: number;\n reveal_published?: boolean;\n error?: string;\n}\n\nexport interface Theme {\n bg: string;\n text: string;\n muted: string;\n border: string;\n accent: string;\n accentSoft: string;\n}\n\nexport const THEMES: Record<'light' | 'dark', Theme> = {\n light: {\n bg: '#ffffff',\n text: '#0a0a0a',\n muted: '#666666',\n border: '#e5e5e5',\n accent: '#f97316',\n accentSoft: '#fff4ec',\n },\n dark: {\n bg: '#0a0a0a',\n text: '#fafafa',\n muted: '#a0a0a0',\n border: '#262626',\n accent: '#fb923c',\n accentSoft: '#2a1a0a',\n },\n};\n\nexport const DEFAULT_API_BASE = 'https://vote.ochk.io';\n\nexport const POLL_ID_RE = /^[0-9a-f]{64}$/;\n","'use client';\n\n// useTally(pollId) — React hook that fetches + polls /api/tally.\n//\n// Defaults: fetch once on mount; refetch every 60s (matches the /api/tally\n// edge cache TTL so we never get stale data from our own server-side cache\n// but don't thrash the relay set either). Pass { refreshMs: 0 } for no\n// auto-refresh, or a custom interval.\n//\n// Consumers that want independent verification should NOT use this hook —\n// use @orangecheck/vote-core + @orangecheck/vote-cli with their own\n// node/bitcoin source. This hook trusts vote.ochk.io (or whatever baseUrl\n// you pass).\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { DEFAULT_API_BASE, POLL_ID_RE } from './types.js';\nimport type { TallyResponse } from './types.js';\n\nexport interface UseTallyOptions {\n baseUrl?: string;\n refreshMs?: number;\n /** If set, provides initial data (SSR hydration). */\n initialData?: TallyResponse;\n}\n\n/** A poll's own content: what it asks and when it closes. Not a count, so it\n * outlives a failed refresh of the same poll. */\nexport interface PollContent {\n question?: string;\n deadline?: string;\n}\n\nexport interface UseTallyState {\n data: TallyResponse | null;\n /** The last question and deadline read for this pollId, kept after a failed refresh. */\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n refetch: () => void;\n}\n\n/** What the hook holds, always for one pollId: nothing of another poll is ever returned. */\ninterface Entry {\n pollId: string;\n data: TallyResponse | null;\n poll: PollContent | null;\n error: Error | null;\n loading: boolean;\n}\n\nfunction contentOf(d: TallyResponse | null | undefined): PollContent | null {\n if (!d || (d.question === undefined && d.deadline === undefined)) return null;\n return {\n ...(d.question !== undefined ? { question: d.question } : {}),\n ...(d.deadline !== undefined ? { deadline: d.deadline } : {}),\n };\n}\n\nexport function useTally(\n pollId: string,\n opts: UseTallyOptions = {}\n): UseTallyState {\n const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;\n const refreshMs = opts.refreshMs ?? 60_000;\n\n const [entry, setEntry] = useState<Entry>(() => ({\n pollId,\n data: opts.initialData ?? null,\n poll: contentOf(opts.initialData),\n error: null,\n loading: !opts.initialData,\n }));\n\n // Only the latest request writes: an answer that lands after a newer request\n // (an earlier refresh, or the poll before a pollId change) is dropped.\n const latest = useRef(0);\n\n const fetchOnce = useCallback(async () => {\n // The poll this fetch is for; a later pollId keeps an answer for this one off screen.\n const pid = pollId;\n const seq = ++latest.current;\n const kept = (prev: Entry) => (prev.pollId === pid ? prev.poll : null);\n if (!POLL_ID_RE.test(pid)) {\n setEntry({ pollId: pid, data: null, poll: null, error: new Error('pollId must be 64 hex chars'), loading: false });\n return;\n }\n try {\n const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);\n const body = (await res.json()) as TallyResponse;\n if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);\n if (seq !== latest.current) return;\n setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));\n } catch (err) {\n if (seq !== latest.current) return;\n // The last tally is no longer the answer: never show it beside the error.\n const error = err instanceof Error ? err : new Error(String(err));\n setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));\n }\n }, [baseUrl, pollId]);\n\n useEffect(() => {\n void fetchOnce();\n if (refreshMs <= 0) return;\n const timer = setInterval(() => void fetchOnce(), refreshMs);\n return () => clearInterval(timer);\n }, [fetchOnce, refreshMs]);\n\n // Until this pollId's first answer, nothing of the last poll shows.\n const mine = entry.pollId === pollId;\n return {\n data: mine ? entry.data : null,\n poll: mine ? entry.poll : null,\n error: mine ? entry.error : null,\n loading: mine ? entry.loading : true,\n refetch: fetchOnce,\n };\n}\n"]}
@@ -5,40 +5,58 @@ var DEFAULT_API_BASE = "https://vote.ochk.io";
5
5
  var POLL_ID_RE = /^[0-9a-f]{64}$/;
6
6
 
7
7
  // src/use-tally.ts
8
+ function contentOf(d) {
9
+ if (!d || d.question === void 0 && d.deadline === void 0) return null;
10
+ return {
11
+ ...d.question !== void 0 ? { question: d.question } : {},
12
+ ...d.deadline !== void 0 ? { deadline: d.deadline } : {}
13
+ };
14
+ }
8
15
  function useTally(pollId, opts = {}) {
9
16
  const baseUrl = opts.baseUrl ?? DEFAULT_API_BASE;
10
17
  const refreshMs = opts.refreshMs ?? 6e4;
11
- const [data, setData] = useState(opts.initialData ?? null);
12
- const [error, setError] = useState(null);
13
- const [loading, setLoading] = useState(!opts.initialData);
14
- const pollIdRef = useRef(pollId);
15
- pollIdRef.current = pollId;
18
+ const [entry, setEntry] = useState(() => ({
19
+ pollId,
20
+ data: opts.initialData ?? null,
21
+ poll: contentOf(opts.initialData),
22
+ error: null,
23
+ loading: !opts.initialData
24
+ }));
25
+ const latest = useRef(0);
16
26
  const fetchOnce = useCallback(async () => {
17
- const pid = pollIdRef.current;
27
+ const pid = pollId;
28
+ const seq = ++latest.current;
29
+ const kept = (prev) => prev.pollId === pid ? prev.poll : null;
18
30
  if (!POLL_ID_RE.test(pid)) {
19
- setError(new Error("pollId must be 64 hex chars"));
20
- setLoading(false);
31
+ setEntry({ pollId: pid, data: null, poll: null, error: new Error("pollId must be 64 hex chars"), loading: false });
21
32
  return;
22
33
  }
23
34
  try {
24
35
  const res = await fetch(`${baseUrl}/api/tally?poll=${pid}`);
25
36
  const body = await res.json();
26
37
  if (!res.ok) throw new Error(body.error ?? `http ${res.status}`);
27
- setData(body);
28
- setError(null);
38
+ if (seq !== latest.current) return;
39
+ setEntry((prev) => ({ pollId: pid, data: body, poll: contentOf(body) ?? kept(prev), error: null, loading: false }));
29
40
  } catch (err) {
30
- setError(err instanceof Error ? err : new Error(String(err)));
31
- } finally {
32
- setLoading(false);
41
+ if (seq !== latest.current) return;
42
+ const error = err instanceof Error ? err : new Error(String(err));
43
+ setEntry((prev) => ({ pollId: pid, data: null, poll: kept(prev), error, loading: false }));
33
44
  }
34
- }, [baseUrl]);
45
+ }, [baseUrl, pollId]);
35
46
  useEffect(() => {
36
47
  void fetchOnce();
37
48
  if (refreshMs <= 0) return;
38
49
  const timer = setInterval(() => void fetchOnce(), refreshMs);
39
50
  return () => clearInterval(timer);
40
51
  }, [fetchOnce, refreshMs]);
41
- return { data, error, loading, refetch: fetchOnce };
52
+ const mine = entry.pollId === pollId;
53
+ return {
54
+ data: mine ? entry.data : null,
55
+ poll: mine ? entry.poll : null,
56
+ error: mine ? entry.error : null,
57
+ loading: mine ? entry.loading : true,
58
+ refetch: fetchOnce
59
+ };
42
60
  }
43
61
 
44
62
  export { useTally };