diff --git a/examples/CRISP/client/index.html b/examples/CRISP/client/index.html index e1de9c6eb3..1ffd49cc87 100644 --- a/examples/CRISP/client/index.html +++ b/examples/CRISP/client/index.html @@ -40,6 +40,14 @@ /> + + + + +
diff --git a/examples/CRISP/client/src/components/Cards/Card.tsx b/examples/CRISP/client/src/components/Cards/Card.tsx index e5ded8c8fb..5754c43599 100644 --- a/examples/CRISP/client/src/components/Cards/Card.tsx +++ b/examples/CRISP/client/src/components/Cards/Card.tsx @@ -31,20 +31,8 @@ const Card: React.FC = ({ children, isActive, isDetails, checked, onC return (
{children} diff --git a/examples/CRISP/client/src/components/Cards/CardContent.tsx b/examples/CRISP/client/src/components/Cards/CardContent.tsx index bcc9649093..6a81a1595b 100644 --- a/examples/CRISP/client/src/components/Cards/CardContent.tsx +++ b/examples/CRISP/client/src/components/Cards/CardContent.tsx @@ -12,7 +12,7 @@ interface CardContentProps { const CardContent: React.FC = ({ children }) => { return ( -
+
{children}
) diff --git a/examples/CRISP/client/src/components/Cards/PollCard.tsx b/examples/CRISP/client/src/components/Cards/PollCard.tsx index 235ebc0951..8dc8b96752 100644 --- a/examples/CRISP/client/src/components/Cards/PollCard.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCard.tsx @@ -59,27 +59,34 @@ const PollCard: React.FC = ({ roundId, options, totalVotes, date, en } return ( -
-
-
{formatDate(date)}
-
- +
+ {formatDate(date)} + + View → +
- {isActive && ( -
-
-
{isCurrentRound ? 'Live' : 'Active'}
-
- )} -
+ +
+ {isActive && {isCurrentRound ? 'Live' : 'Active'}}
-
+ ) } diff --git a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx index 1acc4dfdad..666ba0cc01 100644 --- a/examples/CRISP/client/src/components/Cards/PollCardResult.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCardResult.tsx @@ -26,35 +26,42 @@ const PollCardResult: React.FC = ({ isResult, results, tota } return ( -
+
{results.map((poll) => ( -
-
+
+
-

{poll.label}

+ + {poll.label} +
{isActive && isResult && ( -
-
- -
vote encrypted
-
-

revealed when poll ends

+
+ + + vote encrypted + + revealed when poll ends
)} {!isActive && ( -
-

0 && poll.checked ? 'text-lime-400' : 'text-slate-600/50'}`} +
+
0 && poll.checked ? 'var(--accent)' : 'var(--ink-soft)', + }} > {totalVotes ? calculatePercentage(poll.votes) : 0}% -

-

- {poll.votes} votes -

+
+ + {poll.votes} {poll.votes === 1 ? 'vote' : 'votes'} +
)}
diff --git a/examples/CRISP/client/src/components/Footer.tsx b/examples/CRISP/client/src/components/Footer.tsx index 9ef672aa67..31816c6173 100644 --- a/examples/CRISP/client/src/components/Footer.tsx +++ b/examples/CRISP/client/src/components/Footer.tsx @@ -6,44 +6,42 @@ import React from 'react' import GnosisGuildLogo from '@/assets/icons/gg.svg' -import { Link } from 'react-router-dom' import { CastleTurret, GithubLogo, TelegramLogo, TwitterLogo } from '@phosphor-icons/react' const Footer: React.FC = () => { return ( - + +
) } diff --git a/examples/CRISP/client/src/components/Navbar.tsx b/examples/CRISP/client/src/components/Navbar.tsx index 1ebcd20b11..56a7e938f5 100644 --- a/examples/CRISP/client/src/components/Navbar.tsx +++ b/examples/CRISP/client/src/components/Navbar.tsx @@ -5,13 +5,16 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React from 'react' -import Logo from '@/assets/icons/logo.svg' import { Link } from 'react-router-dom' import NavMenu from '@/components/NavMenu' import { ConnectKitButton } from 'connectkit' import useToken from '@/hooks/generic/useMintToken' const PAGES = [ + { + label: 'Live Poll', + path: '/current', + }, { label: 'About', path: '/about', @@ -26,40 +29,30 @@ const Navbar: React.FC = () => { const { mintTokens, isMinting } = useToken() return ( - + +
) } diff --git a/examples/CRISP/client/src/components/VotesBadge.tsx b/examples/CRISP/client/src/components/VotesBadge.tsx index 8cf5cf42e7..99fefc93f8 100644 --- a/examples/CRISP/client/src/components/VotesBadge.tsx +++ b/examples/CRISP/client/src/components/VotesBadge.tsx @@ -12,11 +12,9 @@ type VotesBadgeProps = { const VotesBadge: React.FC = ({ totalVotes }) => { return ( -
- {totalVotes} votes -
+ + {totalVotes} {totalVotes === 1 ? 'vote' : 'votes'} + ) } diff --git a/examples/CRISP/client/src/design/Editorial.tsx b/examples/CRISP/client/src/design/Editorial.tsx new file mode 100644 index 0000000000..8f5af20793 --- /dev/null +++ b/examples/CRISP/client/src/design/Editorial.tsx @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +// +// CRISP — editorial component library, ported to TSX from the Claude Design +// handoff (crisp/project/components.jsx). Pure presentational primitives; +// no app/voting logic lives here. + +import { useEffect, useMemo, useState, type ReactNode, type CSSProperties } from 'react' + +/* ============================================================ + Numbered gutter — Nº 0X | LABEL (vertical rule) + ============================================================ */ +export function Gutter({ num, label }: { num: string; label: string }) { + return ( +
+
Nº {num}
+
+
{label}
+
+ ) +} + +/* ============================================================ + Ciphertext renderer — deterministic-looking hex blob whose + blocks animate in. Purely decorative. + ============================================================ */ +const HEX = '0123456789abcdef' +function makeHex(seed: number, len: number): string { + let s = seed + let out = '' + for (let i = 0; i < len; i++) { + s = (s * 16807 + 7) % 2147483647 + out += HEX[s & 15] + } + return out +} + +export function Cipher({ + seed = 42, + length = 96, + blockSize = 4, + highlight = false, + tight = false, + className = '', +}: { + seed?: number + length?: number + blockSize?: number + highlight?: boolean + tight?: boolean + className?: string +}) { + const blocks = useMemo(() => { + const step = Math.max(1, Math.floor(blockSize)) + const raw = makeHex(seed + length, length) + const arr: string[] = [] + for (let i = 0; i < length; i += step) arr.push(raw.slice(i, i + step)) + return arr + }, [seed, length, blockSize]) + return ( + + {blocks.map((b, i) => ( + + {b} + {i < blocks.length - 1 ? ' ' : ''} + + ))} + + ) +} + +/* ============================================================ + Threshold custodian seals + ============================================================ */ +type SealState = 'signed' | 'pending' | 'active' + +export function ThresholdSeal({ id, state }: { id: string; state: SealState }) { + const cls = state === 'signed' ? 'active' : state === 'pending' ? 'pending' : '' + return ( +
+
+
C—{id}
+
{state === 'signed' ? '✓' : state === 'pending' ? '·' : '○'}
+
+
+ ) +} + +export function ThresholdRow({ signed, total }: { signed: number; total: number }) { + return ( +
+ {Array.from({ length: total }).map((_, i) => ( + + ))} +
+ ) +} + +/* ============================================================ + Tally bar — horizontal segments with mono labels + ============================================================ */ +export interface TallySegment { + label?: string + count: number + color?: 'a' | 'b' +} + +export function TallyBar({ segments, total }: { segments: TallySegment[]; total: number }) { + return ( +
+ {segments.map((s, i) => { + const pct = total > 0 ? (s.count / total) * 100 : 0 + return ( +
+ {pct >= 8 ? `${Math.round(pct)}%` : ''} +
+ ) + })} +
+ ) +} + +/* ============================================================ + Section header — Nº NN | TITLE | meta on right + ============================================================ */ +export function SectionHeader({ + num, + kicker, + title, + meta, + children, +}: { + num: string + kicker: string + title: ReactNode + meta?: ReactNode + children?: ReactNode +}) { + return ( +
+
+
+ Nº {num} +
+
+ {kicker} +
+
+
{title}
+
{meta}
+ {children} +
+ ) +} + +/* ============================================================ + Hand-drawn markers (SVG strike / underline) + ============================================================ */ +export function MarkerStrike({ children, color = 'var(--accent)' }: { children: ReactNode; color?: string }) { + return ( + + {children} + + + + + ) +} + +export function MarkerUnderline({ children, color = 'var(--accent)' }: { children: ReactNode; color?: string }) { + return ( + + {children} + + + + + ) +} + +/* ============================================================ + Countdown — mono digits to a target epoch (ms) + ============================================================ */ +export function Countdown({ targetMs }: { targetMs: number }) { + const [now, setNow] = useState(() => Date.now()) + useEffect(() => { + const t = setInterval(() => setNow(Date.now()), 1000) + return () => clearInterval(t) + }, []) + const diff = Math.max(0, targetMs - now) + const d = Math.floor(diff / 86400000) + const h = Math.floor((diff / 3600000) % 24) + const m = Math.floor((diff / 60000) % 60) + const s = Math.floor((diff / 1000) % 60) + const seg = (n: number, lbl: string) => ( + + + {String(n).padStart(2, '0')} + + + {lbl} + + + ) + return ( +
+ {seg(d, 'd')} {seg(h, 'h')} {seg(m, 'm')} {seg(s, 's')} +
+ ) +} + +/* ============================================================ + EditorialShell — opt-in wrapper that scopes the design tokens + (palette / mode / density) for a page or subtree. + ============================================================ */ +export function EditorialShell({ + children, + palette = 'interfold', + mode = 'light', + density = 'comfortable', + className = '', + style, +}: { + children: ReactNode + palette?: string + mode?: 'light' | 'dark' + density?: 'comfortable' | 'compact' + className?: string + style?: CSSProperties +}) { + return ( +
+ {children} +
+ ) +} diff --git a/examples/CRISP/client/src/design/editorial.css b/examples/CRISP/client/src/design/editorial.css new file mode 100644 index 0000000000..3802d14285 --- /dev/null +++ b/examples/CRISP/client/src/design/editorial.css @@ -0,0 +1,844 @@ +/* SPDX-License-Identifier: LGPL-3.0-only + * + * This file is provided WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. + * + * CRISP — editorial design system, ported from the Claude Design handoff. + * Scoped under `.crisp-editorial` so it can be adopted page-by-page without + * disturbing the rest of the (Tailwind) app during migration. Palette / mode / + * density are set via data-attributes on the wrapper element. + */ + +/* ============================================================ + Design tokens — Interfold mint (default) + ============================================================ */ +.crisp-editorial { + --paper: #cff5dd; /* signature mint */ + --paper-2: #bfefd0; + --paper-soft: #c5f1d5; + --ink: #0a0b0a; /* near-pure black */ + --ink-2: #1a1c1a; + --ink-soft: #6a7770; /* muted sage-gray for captions */ + --rule: #0a0b0a1a; + --rule-strong: #0a0b0a33; + --accent: #0a0b0a; /* ink-as-accent for monochrome editorial */ + --accent-tint: #a8e5bd; + --accent-soft: #bcedcc; + --warm: #0f3d2a; /* deep forest as secondary emphasis */ + --warm-tint: #a8e5bd; + --warm-soft: #bcedcc; + --danger: #8b2a14; + --paper-card: #daf7e4; + --shadow-1: 0 1px 0 0 #0a0b0a0d; + + --f-serif: 'Source Serif 4', 'Source Serif Pro', 'Iowan Old Style', 'Georgia', serif; + --f-italic: 'Source Serif 4', 'Georgia', serif; + --f-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', 'Menlo', monospace; + --f-sans: 'Inter', system-ui, sans-serif; + + --pad-x: clamp(28px, 4vw, 72px); + --pad-y: clamp(24px, 3vw, 48px); + --gap-row: 28px; + + --r-0: 0px; + --r-1: 2px; + --r-2: 4px; + + background: var(--paper); + color: var(--ink); + font-family: var(--f-serif); + font-size: 17px; + line-height: 1.45; + font-synthesis-weight: none; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* ----- Palette presets (data-palette on the wrapper) ----- */ +.crisp-editorial[data-palette='moss'] { + --paper: #f4efe6; + --paper-2: #ece5d7; + --paper-soft: #efe9dc; + --paper-card: #faf6ee; + --ink: #15140f; + --ink-2: #2a2822; + --ink-soft: #6b6a60; + --accent: #2f5d3a; + --accent-tint: #c8d7be; + --accent-soft: #dde7d2; + --warm: #b7531f; + --warm-tint: #e8c9a8; + --warm-soft: #f0dcc0; +} +.crisp-editorial[data-palette='tomato'] { + --paper: #f8f0e3; + --paper-2: #f1e6d2; + --paper-soft: #f4ead7; + --paper-card: #fcf5e8; + --ink: #1a1410; + --ink-2: #2c221c; + --ink-soft: #75665a; + --accent: #c84231; + --accent-tint: #f2c5b9; + --accent-soft: #f6d7cc; + --warm: #e8a33b; + --warm-tint: #f6d9a7; + --warm-soft: #f8e2bc; +} +.crisp-editorial[data-palette='ink'] { + --paper: #ece7da; + --paper-2: #e2dccc; + --paper-soft: #e6e0d0; + --paper-card: #f5f0e2; + --ink: #0e1118; + --ink-2: #1f2330; + --ink-soft: #5c6171; + --accent: #2d3e8f; + --accent-tint: #bcc4e2; + --accent-soft: #d6dae9; + --warm: #b7531f; + --warm-tint: #e8c9a8; +} +.crisp-editorial[data-palette='clay'] { + --paper: #f2eae0; + --paper-2: #e8decf; + --paper-soft: #ece2d2; + --paper-card: #f8f1e5; + --ink: #211a12; + --ink-2: #322820; + --ink-soft: #6e6053; + --accent: #9d3b1a; + --accent-tint: #e8c9a8; + --accent-soft: #f0dcc0; + --warm: #2f5d3a; + --warm-tint: #c8d7be; +} +.crisp-editorial[data-palette='bone'] { + --paper: #eeeae0; + --paper-2: #e3ded1; + --paper-soft: #e8e2d4; + --paper-card: #f6f2e6; + --ink: #14140f; + --ink-2: #28281f; + --ink-soft: #6a695c; + --accent: #1b1b17; + --accent-tint: #bfb9a8; + --accent-soft: #d8d2c0; + --warm: #b7531f; +} + +.crisp-editorial[data-mode='dark'] { + --paper: #14130f; + --paper-2: #1c1a14; + --paper-soft: #1a1812; + --paper-card: #1e1c16; + --ink: #efe9da; + --ink-2: #c9c3b4; + --ink-soft: #807a6d; + --rule: #efe9da1f; + --rule-strong: #efe9da33; + --accent: #93b187; + --accent-tint: #2f5d3a; + --accent-soft: #1f3a26; + --warm: #e0a36f; + --warm-tint: #5c3919; + --warm-soft: #43290f; +} + +.crisp-editorial[data-density='compact'] { + --pad-x: clamp(20px, 2.5vw, 48px); + --pad-y: clamp(16px, 2vw, 32px); + --gap-row: 18px; +} + +/* ============================================================ + Base (scoped) + ============================================================ */ +.crisp-editorial * { + box-sizing: border-box; +} +.crisp-editorial a { + color: inherit; + text-decoration: none; +} +.crisp-editorial ::selection { + background: var(--accent); + color: var(--paper); +} + +/* ============================================================ + Type primitives + ============================================================ */ +.crisp-editorial .mono { + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + font-weight: 500; +} +.crisp-editorial .mono-sm { + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.12em; + text-transform: uppercase; +} +.crisp-editorial .mono-md { + font-family: var(--f-mono); + font-size: 13px; + letter-spacing: 0.08em; +} +.crisp-editorial .italic-serif { + font-style: italic; + font-feature-settings: 'ss01'; +} + +.crisp-editorial .display { + font-family: var(--f-serif); + font-weight: 400; + font-size: clamp(56px, 9vw, 144px); + line-height: 0.97; + letter-spacing: -0.02em; + margin: 0; +} +.crisp-editorial .display em { + font-style: italic; + color: var(--accent); + font-weight: 400; +} +.crisp-editorial .h1 { + font-family: var(--f-serif); + font-weight: 400; + font-size: clamp(36px, 5vw, 72px); + line-height: 1.02; + letter-spacing: -0.015em; + margin: 0; +} +.crisp-editorial .h2 { + font-family: var(--f-serif); + font-weight: 400; + font-size: clamp(28px, 3vw, 44px); + line-height: 1.08; + letter-spacing: -0.01em; + margin: 0; +} +.crisp-editorial .h3 { + font-family: var(--f-serif); + font-weight: 500; + font-size: 22px; + line-height: 1.18; + margin: 0; +} +.crisp-editorial .lede { + font-family: var(--f-serif); + font-size: 19px; + line-height: 1.5; + color: var(--ink-2); + max-width: 56ch; +} + +/* ============================================================ + Rules + shell + ============================================================ */ +.crisp-editorial .hr { + height: 1px; + background: var(--rule-strong); + width: 100%; +} +.crisp-editorial .hr-soft { + height: 1px; + background: var(--rule); + width: 100%; +} +.crisp-editorial .vrule { + width: 1px; + background: var(--rule-strong); +} + +/* ============================================================ + Header / TopBar + ============================================================ */ +.crisp-editorial .topbar { + display: flex; + align-items: center; + gap: 24px; + padding: 18px var(--pad-x); + background: var(--paper-2); + border-bottom: 1px solid var(--rule-strong); +} +.crisp-editorial .brand { + display: flex; + align-items: baseline; + gap: 10px; + font-family: var(--f-serif); + font-size: 28px; + letter-spacing: -0.01em; +} +.crisp-editorial .brand .glyph { + display: inline-block; + width: 14px; + height: 14px; + background: var(--accent); + border-radius: 50%; + transform: translateY(2px); +} +.crisp-editorial .brand-mono { + font-family: var(--f-mono); + font-size: 12px; + color: var(--ink-soft); + letter-spacing: 0.18em; + text-transform: uppercase; +} +.crisp-editorial .topnav { + display: flex; + gap: 0; + margin-left: 12px; +} +.crisp-editorial .topnav button, +.crisp-editorial .topnav a { + appearance: none; + background: transparent; + border: 0; + padding: 6px 4px; + margin-right: 22px; + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-soft); + cursor: pointer; + position: relative; + font-weight: 500; +} +.crisp-editorial .topnav button:hover, +.crisp-editorial .topnav a:hover { + color: var(--ink); +} +.crisp-editorial .topnav .active { + color: var(--ink); +} +.crisp-editorial .topnav .active::after { + content: ''; + position: absolute; + left: 0; + right: 0; + bottom: -19px; + height: 2px; + background: var(--ink); +} +.crisp-editorial .topbar-right { + margin-left: auto; + display: flex; + gap: 12px; + align-items: center; +} + +/* ============================================================ + Buttons + ============================================================ */ +.crisp-editorial .btn { + appearance: none; + font-family: var(--f-mono); + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 500; + padding: 13px 22px; + border: 1px solid var(--ink); + background: var(--ink); + color: var(--paper); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 10px; + border-radius: 0; + transition: + transform 0.15s, + background 0.15s, + color 0.15s; +} +.crisp-editorial .btn:hover { + transform: translate(-1px, -1px); + box-shadow: 3px 3px 0 0 var(--ink); +} +.crisp-editorial .btn:active { + transform: translate(0, 0); + box-shadow: 0 0 0 0 var(--ink); +} +.crisp-editorial .btn.ghost { + background: transparent; + color: var(--ink); +} +.crisp-editorial .btn.ghost:hover { + background: var(--ink); + color: var(--paper); + box-shadow: none; + transform: none; +} +.crisp-editorial .btn.accent { + background: var(--accent); + border-color: var(--accent); + color: var(--paper); +} +.crisp-editorial .btn.tint { + background: var(--accent-soft); + border-color: var(--accent-soft); + color: var(--ink); +} +.crisp-editorial .btn.warm { + background: var(--warm); + border-color: var(--warm); + color: var(--paper); +} +.crisp-editorial .btn.sm { + padding: 8px 14px; + font-size: 10px; +} +.crisp-editorial .btn.lg { + padding: 18px 32px; + font-size: 13px; +} +.crisp-editorial .btn:disabled { + cursor: not-allowed; +} + +.crisp-editorial .linkish { + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + border-bottom: 1px solid var(--ink); + padding-bottom: 2px; + cursor: pointer; + display: inline-block; +} +.crisp-editorial .linkish:hover { + color: var(--accent); + border-color: var(--accent); +} + +.crisp-editorial .wallet { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 7px 12px 7px 8px; + border: 1px solid var(--ink); + background: var(--paper-card); + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.08em; + cursor: pointer; +} +.crisp-editorial .wallet .blockie { + width: 18px; + height: 18px; + background: conic-gradient(from 12deg, #2f5d3a 0 25%, #b7531f 0 50%, #15140f 0 75%, #c8d7be 0 100%); + border: 1px solid var(--ink); +} +.crisp-editorial .pill-mint { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 7px 14px; + background: var(--accent-tint); + border: 1px solid var(--ink); + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; +} +.crisp-editorial .pill-mint:hover { + background: var(--accent); + color: var(--paper); +} + +/* ============================================================ + Section number gutter + ============================================================ */ +.crisp-editorial .nogutter { + display: grid; + grid-template-columns: 80px 1fr; + gap: 24px; +} +/* Two-column editorial split: editorial text on the left, a visual + (ciphertext / faceoff) on the right. Collapses to one column on narrow. */ +.crisp-editorial .split { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: clamp(32px, 5vw, 72px); + align-items: start; + width: 100%; +} +.crisp-editorial .split > .split-visual { + position: sticky; + top: 32px; +} +@media (max-width: 920px) { + .crisp-editorial .split { + grid-template-columns: 1fr; + } + .crisp-editorial .split > .split-visual { + position: static; + } +} +.crisp-editorial .nogutter .gut { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; +} +.crisp-editorial .nogutter .gut .num { + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; +} +.crisp-editorial .nogutter .gut .vrule { + width: 1px; + height: 110px; + background: var(--ink); +} +.crisp-editorial .nogutter .gut .tag { + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-soft); + max-width: 65px; + line-height: 1.5; +} + +/* ============================================================ + Cards + ============================================================ */ +.crisp-editorial .card { + background: var(--paper-card); + border: 1px solid var(--ink); + padding: 24px; + box-shadow: var(--shadow-1); +} +.crisp-editorial .bord-t { + border-top: 1px solid var(--rule-strong); +} +.crisp-editorial .bord-b { + border-bottom: 1px solid var(--rule-strong); +} + +/* ============================================================ + Tag / pill labels + ============================================================ */ +.crisp-editorial .tag { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.14em; + text-transform: uppercase; + padding: 4px 8px; + border: 1px solid var(--ink); + background: var(--paper); +} +.crisp-editorial .tag.dot::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--ink); +} +.crisp-editorial .tag.live { + background: var(--accent); + color: var(--paper); + border-color: var(--accent); +} +.crisp-editorial .tag.live.dot::before { + background: var(--paper); + animation: crisp-pulse 1.4s ease-in-out infinite; +} +.crisp-editorial .tag.tally { + background: var(--warm); + color: var(--paper); + border-color: var(--warm); +} +.crisp-editorial .tag.closed { + background: var(--paper-2); + color: var(--ink-soft); + border-color: var(--rule-strong); +} +@keyframes crisp-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +/* ============================================================ + Cryptography visuals + ============================================================ */ +.crisp-editorial .cipher { + font-family: var(--f-mono); + font-size: 10.5px; + line-height: 1.5; + color: var(--ink-soft); + word-break: break-all; + letter-spacing: 0; +} +.crisp-editorial .cipher.tight { + line-height: 1.3; + font-size: 9.5px; +} +.crisp-editorial .cipher b { + color: var(--accent); + font-weight: 500; +} +.crisp-editorial .cipher .blk { + display: inline-block; + padding: 0 2px; + animation: crisp-encrypt-pop 0.25s both; +} + +.crisp-editorial .seal { + width: 56px; + height: 56px; + border: 1px solid var(--ink); + background: var(--paper-card); + display: grid; + place-items: center; + position: relative; + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.12em; + flex-shrink: 0; +} +.crisp-editorial .seal::before { + content: ''; + position: absolute; + inset: 4px; + border: 1px solid var(--rule-strong); + pointer-events: none; +} +.crisp-editorial .seal.active { + background: var(--accent); + color: var(--paper); + border-color: var(--accent); +} +.crisp-editorial .seal.active::before { + border-color: #ffffff44; +} +.crisp-editorial .seal.pending { + background: var(--paper-2); + color: var(--ink-soft); +} +.crisp-editorial .threshold-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.crisp-editorial .tally-bar { + display: flex; + align-items: stretch; + height: 40px; + border: 1px solid var(--ink); + background: var(--paper-card); + overflow: hidden; +} +.crisp-editorial .tally-bar .seg { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 12px; + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.06em; + color: var(--paper); + transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} +.crisp-editorial .tally-bar .seg.a { + background: var(--accent); +} +.crisp-editorial .tally-bar .seg.b { + background: var(--warm); +} + +/* ============================================================ + Faceoff — two-option vote slots (emoji) with vs divider + ============================================================ */ +.crisp-editorial .faceoff { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 18px; + align-items: stretch; + max-width: 640px; +} +.crisp-editorial .faceoff-slot { + appearance: none; + position: relative; + display: grid; + place-items: center; + aspect-ratio: 1 / 1; + background: var(--paper-card); + border: 1px solid var(--ink); + cursor: pointer; + transition: + transform 0.15s, + box-shadow 0.15s, + background 0.15s; +} +.crisp-editorial .faceoff-slot:not(:disabled):hover { + transform: translate(-1px, -1px); + box-shadow: 4px 4px 0 0 var(--ink); +} +.crisp-editorial .faceoff-slot.selected { + background: var(--accent-soft); + box-shadow: inset 0 0 0 3px var(--accent); +} +.crisp-editorial .faceoff-slot:disabled { + cursor: default; +} +.crisp-editorial .faceoff-emoji { + font-size: clamp(64px, 14vw, 132px); + line-height: 1; + user-select: none; +} +.crisp-editorial .faceoff-corner { + position: absolute; + top: 10px; + left: 12px; +} +.crisp-editorial .faceoff-pick { + position: absolute; + bottom: 12px; +} +.crisp-editorial .faceoff-vs { + display: grid; + place-items: center; +} +.crisp-editorial .faceoff-vs .mono { + font-size: 13px; + color: var(--ink-soft); +} +@media (max-width: 560px) { + .crisp-editorial .faceoff { + grid-template-columns: 1fr; + } + .crisp-editorial .faceoff-slot { + aspect-ratio: 16 / 9; + } +} + +/* ============================================================ + Footer + ============================================================ */ +.crisp-editorial .footer { + padding: 28px var(--pad-x); + display: flex; + gap: 24px; + align-items: center; + border-top: 1px solid var(--rule-strong); + background: var(--paper-2); + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-soft); + flex-wrap: wrap; +} +.crisp-editorial .footer .links { + display: flex; + gap: 24px; + margin-left: auto; +} +.crisp-editorial .footer a:hover { + color: var(--ink); +} + +/* ============================================================ + Utility + ============================================================ */ +.crisp-editorial .row { + display: flex; + align-items: center; + gap: var(--gap-row); +} +.crisp-editorial .col { + display: flex; + flex-direction: column; + gap: var(--gap-row); +} +.crisp-editorial .gap-2 { + gap: 8px; +} +.crisp-editorial .gap-3 { + gap: 12px; +} +.crisp-editorial .gap-4 { + gap: 16px; +} +.crisp-editorial .gap-6 { + gap: 24px; +} +.crisp-editorial .gap-8 { + gap: 32px; +} +.crisp-editorial .flex-1 { + flex: 1; +} +.crisp-editorial .between { + display: flex; + justify-content: space-between; + align-items: center; +} +.crisp-editorial .pad-section { + padding: var(--pad-y) var(--pad-x); + width: 100%; + max-width: 1180px; + margin-inline: auto; +} +.crisp-editorial .muted { + color: var(--ink-soft); +} +.crisp-editorial .accent { + color: var(--accent); +} +.crisp-editorial .warm { + color: var(--warm); +} +.crisp-editorial .txt-right { + text-align: right; +} +.crisp-editorial .cap { + font-family: var(--f-mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ink-soft); +} + +@keyframes crisp-encrypt-pop { + 0% { + opacity: 0; + transform: translateY(4px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +@keyframes crisp-scan { + 0% { + background-position: 0 0; + } + 100% { + background-position: 200% 0; + } +} +.crisp-editorial .scan { + background-image: linear-gradient(90deg, transparent 0%, var(--accent-tint) 50%, transparent 100%); + background-size: 200% 100%; + animation: crisp-scan 2.2s linear infinite; +} diff --git a/examples/CRISP/client/src/globals.css b/examples/CRISP/client/src/globals.css index b2a237bccd..2a89849097 100644 --- a/examples/CRISP/client/src/globals.css +++ b/examples/CRISP/client/src/globals.css @@ -39,7 +39,7 @@ footer { /* Global Styles */ body { - @apply transition-element bg-slate-200 p-0 font-jakarta text-slate-600; + @apply transition-element bg-[#CFF5DD] p-0 font-jakarta text-slate-600; } img { diff --git a/examples/CRISP/client/src/main.tsx b/examples/CRISP/client/src/main.tsx index 5e5fdaed3e..9737e9e8ba 100644 --- a/examples/CRISP/client/src/main.tsx +++ b/examples/CRISP/client/src/main.tsx @@ -8,6 +8,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' import './globals.css' +import './design/editorial.css' import { HashRouter } from 'react-router-dom' import { VoteManagementProvider } from '@/context/voteManagement/index.ts' import { NotificationAlertProvider } from './context/NotificationAlert/NotificationAlert.context.tsx' diff --git a/examples/CRISP/client/src/pages/About/About.tsx b/examples/CRISP/client/src/pages/About/About.tsx index 53f08068e4..a05c477867 100644 --- a/examples/CRISP/client/src/pages/About/About.tsx +++ b/examples/CRISP/client/src/pages/About/About.tsx @@ -6,58 +6,42 @@ import React from 'react' import CardContent from '@/components/Cards/CardContent' -import CircularTiles from '@/components/CircularTiles' +import { EditorialShell } from '@/design/Editorial' + +const SECTIONS = [ + { + kicker: 'what is crisp?', + body: 'CRISP (Coercion-Resistant Impartial Selection Protocol) is a secure protocol for digital decision-making, leveraging fully homomorphic encryption (FHE) and distributed threshold cryptography (DTC) to enable verifiable secret ballots. Built with Enclave, CRISP safeguards democratic systems and decision-making applications against coercion, manipulation, and other vulnerabilities.', + }, + { + kicker: 'why is this important?', + body: 'Open ballots are known to produce suboptimal outcomes, exposing participants to bribery and coercion. CRISP mitigates these risks and other vulnerabilities with secret, receipt-free ballots, fostering secure and impartial decision-making environments.', + }, + { + kicker: 'Proof of Concept', + body: 'This application is a Proof of Concept (PoC), demonstrating the viability of Enclave as a network and CRISP as an application for secret ballots. Future iterations of this and other applications will be progressively more complete.', + }, +] const About: React.FC = () => { return ( -
-
- -
-
-

About CRISP

- -
-

what is crisp?

-
-

- CRISP (Coercion-Resistant Impartial Selection Protocol) is a secure protocol for digital decision-making, leveraging fully - homomorphic encryption (FHE) and distributed threshold cryptography (DTC) to enable verifiable secret ballots. Built with - Enclave, CRISP safeguards democratic systems and decision-making applications against coercion, manipulation, and other - vulnerabilities. -

- {/*
-

See what's happening under the hood

- -
*/} -
-
-
-

why is this important?

-

- Open ballots are known to produce suboptimal outcomes, exposing participants to bribery and coercion. CRISP mitigates these - risks and other vulnerabilities with secret, receipt-free ballots, fostering secure and impartial decision-making - environments. -

- {/*
-

See what's happening under the hood

- -
*/} -
-
-

Proof of Concept

-

- This application is a Proof of Concept (PoC), demonstrating the viability of Enclave as a network and CRISP as an application - for secret ballots. Future iterations of this and other applications will be progressively more complete. -

- {/*
-

See what's happening under the hood

- -
*/} -
-
-
-
+ +
+
+

About CRISP

+ + {SECTIONS.map(({ kicker, body }) => ( +
+

{kicker}

+

+ {body} +

+
+ ))} +
+
+
+
) } diff --git a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx index a17d3200f5..614e4f5714 100644 --- a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx +++ b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx @@ -20,22 +20,20 @@ const ConfirmVote: React.FC<{ confirmationUrl: string }> = ({ confirmationUrl }) return ( -
-

WHAT JUST HAPPENED?

-
-

- Your vote was encrypted and{' '} - - posted onchain - {' '} - by a relayer. When the poll is over, the results will be tallied using Fully Homomorphic Encryption (FHE) and the results - decrypted using threshold cryptography, without revealing your identity or choice. -

-
+
+

WHAT JUST HAPPENED?

+

+ Your vote was encrypted and{' '} + + posted onchain + {' '} + by a relayer. When the poll is over, the results will be tallied using Fully Homomorphic Encryption (FHE) and the results + decrypted using threshold cryptography, without revealing your identity or choice. +

-
-

WHAT DOES THIS MEAN?

-

+

+

WHAT DOES THIS MEAN?

+

Your participation has directly contributed to a transparent and fair decision-making process, showcasing the power of privacy-preserving technology in governance and beyond. The use of CRISP in this vote represents a significant step towards secure, anonymous, and tamper-proof digital elections and polls. This innovation ensures that every vote counts equally while diff --git a/examples/CRISP/client/src/pages/HistoricPoll/HistoricPoll.tsx b/examples/CRISP/client/src/pages/HistoricPoll/HistoricPoll.tsx index dc92d46721..fb0247a84b 100644 --- a/examples/CRISP/client/src/pages/HistoricPoll/HistoricPoll.tsx +++ b/examples/CRISP/client/src/pages/HistoricPoll/HistoricPoll.tsx @@ -9,7 +9,7 @@ import PollCard from '@/components/Cards/PollCard' import { PollResult } from '@/model/poll.model' import LoadingAnimation from '@/components/LoadingAnimation' import { useVoteManagementContext } from '@/context/voteManagement' -import CircularTiles from '@/components/CircularTiles' +import { EditorialShell } from '@/design/Editorial' import { debounce } from '@/utils/methods' const HistoricPoll: React.FC = () => { @@ -67,25 +67,25 @@ const HistoricPoll: React.FC = () => { }, [handleScroll]) return ( -

-
- -
-
-

Historic polls

+ +
+
+
Archive
+

Past polls

+
{isLoading && (
)} - {!pastPolls.length && !isLoading &&

There are no historic polls.

} + {!pastPolls.length && !isLoading &&

There are no historic polls.

} {visiblePolls.length > 0 && ( -
+
{visiblePolls.map((pollResult: PollResult, index: number) => { return (
@@ -99,8 +99,8 @@ const HistoricPoll: React.FC = () => {
)} -
-
+
+
) } diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx index a233c00750..619b498c4a 100644 --- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx @@ -4,10 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' +import { useNavigate } from 'react-router-dom' import { Poll } from '@/model/poll.model' -import Card from '@/components/Cards/Card' -import CircularTiles from '@/components/CircularTiles' import { useVoteManagementContext } from '@/context/voteManagement' import LoadingAnimation from '@/components/LoadingAnimation' @@ -16,6 +15,7 @@ import { useModal } from 'connectkit' import { useVoteCasting } from '@/hooks/voting/useVoteCasting' import VotingStepIndicator from '@/components/VotingStepIndicator' import { usePublicClient } from 'wagmi' +import { EditorialShell, Cipher } from '@/design/Editorial' type DailyPollSectionProps = { loading?: boolean @@ -23,28 +23,97 @@ type DailyPollSectionProps = { title?: string } -const DailyPollSection: React.FC = ({ loading, endTime, title = 'Daily Poll' }) => { - const { user, pollOptions, setPollOptions, roundState, hasVotedInCurrentRound, voteStatusLoading } = useVoteManagementContext() +const FaceoffSlot: React.FC<{ + poll?: Poll + side: 'A' | 'B' + disabled: boolean + onSelect: (poll: Poll) => void +}> = ({ poll, side, disabled, onSelect }) => { + if (!poll) return
+ return ( + + ) +} + +const DailyPollSection: React.FC = ({ loading, endTime, title = 'The Faceoff' }) => { + const { user, pollOptions, setPollOptions, roundState, hasVotedInCurrentRound, voteStatusLoading, isLoading, getWebResultByRound } = + useVoteManagementContext() + const navigate = useNavigate() const client = usePublicClient() const [isEnded, setIsEnded] = useState(false) + const [tallyReady, setTallyReady] = useState(false) const [pollSelected, setPollSelected] = useState(null) const [noPollSelected, setNoPollSelected] = useState(true) const { setOpen } = useModal() const { castVoteWithProof, isVoting: isCastingVote, isMasking, votingStep, lastActiveStep, stepMessage } = useVoteCasting() + // Derived and selection state are round-local. Tracking the round id lets us + // clear them when the round changes so a new active poll doesn't inherit the + // previous round's results state or vote selection. + const trackedRoundId = useRef(roundState?.id) + useEffect(() => { + let cancelled = false ;(async () => { - if (!client) return - if (!roundState) return - - const block = await client.getBlock() + if (!client || !roundState) return + + if (trackedRoundId.current !== roundState.id) { + trackedRoundId.current = roundState.id + setTallyReady(false) + setIsEnded(false) + setPollSelected(null) + setNoPollSelected(true) + } - if (block.timestamp > roundState.end_time) { - setIsEnded(true) + try { + const block = await client.getBlock() + if (!cancelled) { + setIsEnded(block.timestamp > roundState.end_time) + } + } catch { + // Transient RPC failure — leave isEnded untouched and retry on the next run. } })() + + return () => { + cancelled = true + } }, [roundState, client]) + // Once the poll is over, poll the backend until the FHE tally is published. + useEffect(() => { + if (!isEnded || !roundState || tallyReady) return + + let cancelled = false + const check = async () => { + try { + const result = await getWebResultByRound(roundState.id) + if (!cancelled && result && Array.isArray(result.tally) && result.tally.length > 0) { + setTallyReady(true) + } + } catch { + // Transient failure — keep polling on the next interval tick. + } + } + + check() + const interval = setInterval(check, 8000) + return () => { + cancelled = true + clearInterval(interval) + } + }, [isEnded, roundState, tallyReady, getWebResultByRound]) + const handleChecked = (selectedPoll: Poll) => { const isAlreadySelected = pollSelected?.value === selectedPoll.value @@ -73,89 +142,110 @@ const DailyPollSection: React.FC = ({ loading, endTime, t await castVoteWithProof(pollSelected, isMasking) } + const busy = isCastingVote || isMasking + const optionA = pollOptions[0] + const optionB = pollOptions[1] + const hasPoll = Boolean(roundState && optionA?.label && optionB?.label) + const slotDisabled = busy || isEnded || Boolean(loading) + return ( - <> -
-
- -
+ +
+
+ {/* Left — context + actions */} +
+
+
{title}
+ {hasPoll &&

Choose your favorite

} + {!roundState && !isLoading &&

No active poll found. Check back when the next round opens.

} +
-
-
-

{title}

-

Choose your favorite

- {!roundState &&

No active poll found.

} -
- {roundState && ( -
-
-
-
{!isEnded ? 'Live' : 'Ended'}
+ {roundState && ( +
+ {!isEnded && Live} + {isEnded && tallyReady && Closed} + {isEnded && !tallyReady && Over · Tallying…} + + {roundState.vote_count} {roundState.vote_count === 1 ? 'vote' : 'votes'} + + {hasVotedInCurrentRound && You voted} + {voteStatusLoading && Checking…}
-
- {roundState.vote_count} votes + )} + + {endTime && !isEnded && !busy && ( +
+
Closes in
+
- {hasVotedInCurrentRound && ( -
- You voted + )} + + {busy && } + {isLoading && !roundState && !busy && } + + {/* Active poll — voting actions */} + {roundState && !isEnded && ( +
+ {noPollSelected && ( +
+ {hasVotedInCurrentRound ? 'Select an option to update your vote' : 'Select your favorite'} +
+ )} +
+ +
- )} - {voteStatusLoading && ( -
- Checking... -
- )} -
- )} - - {endTime && !isEnded && !isCastingVote && ( -
- -
- )} - {(isCastingVote || isMasking) && } - {loading && !isCastingVote && !isMasking && } -
- {pollOptions.map((poll) => ( -
- handleChecked(poll)}> -

{poll.label}

-
- ))} + )} + + {/* Poll over — tallying / results, no more voting */} + {roundState && isEnded && ( +
+ {tallyReady ? ( + <> +
The threshold committee has decrypted the result.
+
+ +
+ + ) : ( +
+ Voting is closed. Ballots are being tallied under encryption — results will appear here once the committee publishes the + decrypted tally. +
+ )} +
+ )}
- {roundState && ( -
- {noPollSelected && !isEnded && ( -
- {hasVotedInCurrentRound ? 'Select an option to update your vote' : 'Select your favorite'} + + {/* Right — faceoff + ciphertext */} + {hasPoll && ( +
+
+ +
+ vs
- )} - - + +
+ +
+
+ {busy ? 'Encrypting your ballot…' : 'Your ballot will be encrypted before it leaves this page'} +
+ +
)}
-
- +
+
) } diff --git a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx index b35c32c0e4..055b1696d0 100644 --- a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx @@ -5,63 +5,92 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import React from 'react' -import Logo from '@/assets/icons/logo.svg' -import CircularTiles from '@/components/CircularTiles' import { Link } from 'react-router-dom' import { Keyhole, ListMagnifyingGlass, ShieldCheck } from '@phosphor-icons/react' +import { EditorialShell, Cipher, MarkerUnderline } from '@/design/Editorial' + +const PRINCIPLES = [ + { + icon: Keyhole, + label: 'Private', + body: 'Voter privacy through fully homomorphic encryption — ballots are encrypted before they ever leave your device.', + }, + { + icon: ListMagnifyingGlass, + label: 'Reliable', + body: 'Verifiable results while preserving confidentiality. The tally is computed on ciphertext and proven correct.', + }, + { + icon: ShieldCheck, + label: 'Equitable', + body: 'Robust safeguards against coercion and tampering, with a threshold committee that no single party controls.', + }, +] const HeroSection: React.FC = () => { return ( -
-
- -
-
-
-

Introducing

- CRISP Logo -

Coercion-Resistant Impartial Selection Protocol

-
-
    -
  • - -
    - Private. - Voter privacy through advanced encryption. + +
    +
    + {/* Left — editorial copy */} +
    +
    +
    Coercion-Resistant Impartial Selection Protocol
    +

    + Crisp +

    +

    + Secret-ballot voting you can actually verify. Cast an encrypted vote, let a threshold committee open only the final tally — + and nobody, not even the people running the election, learns how you voted. +

    -
  • -
  • - -
    - Reliable. - Verifiable results while preserving confidentiality. + +
      + {PRINCIPLES.map(({ icon: Icon, label, body }) => ( +
    • + +
      + + {label}. + + {body} +
      +
    • + ))} +
    + +
    +
    + A simple demonstration of CRISP technology. + + Learn more → + +
    +
    + + Try the demo → + +
    -
  • -
  • - -
    - Equitable. - Robust safeguards against coercion and tampering. +
    + + {/* Right — ciphertext visual */} +
    +
    +
    + Your ballot, encrypted + FHE +
    + +
    + + This is what a vote looks like on-chain — opaque to everyone, tallied without ever being decrypted. +
    -
  • -
-
-
-
This is a simple demonstration of CRISP technology.
- -
Learn more.
-
- - -
-
-
+ + ) } diff --git a/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx index a68019e987..aac5a56763 100644 --- a/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx +++ b/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx @@ -9,6 +9,7 @@ import PollCard from '@/components/Cards/PollCard' import { PollResult } from '@/model/poll.model' import { useVoteManagementContext } from '@/context/voteManagement' import { Link } from 'react-router-dom' +import { EditorialShell } from '@/design/Editorial' type PastPollSectionProps = { customLabel?: string @@ -21,17 +22,19 @@ const PastPollSection: React.FC = ({ customLabel = 'Past p const pollsToShow = limit ? pastPolls.slice(0, limit) : pastPolls return ( -
-

{customLabel}

-
- {pollsToShow.map((poll: PollResult) => ( - - ))} -
- - - -
+ +
+

{customLabel}

+
+ {pollsToShow.map((poll: PollResult) => ( + + ))} +
+ + View all polls → + +
+
) } diff --git a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx index 4c5db262f8..2dd44d2655 100644 --- a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx +++ b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx @@ -13,7 +13,7 @@ import PastPollSection from '@/pages/Landing/components/PastPoll' import { useParams } from 'react-router-dom' import LoadingAnimation from '@/components/LoadingAnimation' import { useVoteManagementContext } from '@/context/voteManagement' -import CircularTiles from '@/components/CircularTiles' +import { EditorialShell } from '@/design/Editorial' import CountdownTimer from '@/components/CountdownTime' import ConfirmVote from '../DailyPoll/components/ConfirmVote' @@ -56,11 +56,8 @@ const PollResult: React.FC = () => { }, [pollResult]) return ( -
-
- -
-
+ +
{loading && !pollResult && (
@@ -68,22 +65,19 @@ const PollResult: React.FC = () => { )} {!loading && pollResult && ( -
-
-
-

Poll {pollResult.roundId}

-

- {type === 'confirmation' ? 'Thanks for voting!' : 'Poll Results'} -

- {type !== 'confirmation' &&

{formatDate(pollResult.date)}

} -
- {type === 'confirmation' && roundEndDate && ( -
- -
- )} - +
+
+

Poll {pollResult.roundId}

+

{type === 'confirmation' ? 'Thanks for voting!' : 'Poll Results'}

+ {type !== 'confirmation' &&

{formatDate(pollResult.date)}

}
+ {type === 'confirmation' && roundEndDate && ( +
+
Closes in
+ +
+ )} + { {type === 'confirmation' && txUrl && } {type !== 'confirmation' && ( -
-

WHAT JUST HAPPENED?

-
-

- After casting your vote, CRISP securely processed your selection using a blend of Fully Homomorphic Encryption (FHE), - threshold cryptography, and zero-knowledge proofs (ZKPs), without revealing your identity or choice. Your vote was - encrypted and anonymously aggregated with others, ensuring the integrity of the voting process while strictly - maintaining confidentiality. The protocol's advanced cryptographic techniques guarantee that your vote contributes to - the final outcome without any risk of privacy breaches or undue influence. -

-
+
+

WHAT JUST HAPPENED?

+

+ After casting your vote, CRISP securely processed your selection using a blend of Fully Homomorphic Encryption (FHE), + threshold cryptography, and zero-knowledge proofs (ZKPs), without revealing your identity or choice. Your vote was + encrypted and anonymously aggregated with others, ensuring the integrity of the voting process while strictly + maintaining confidentiality. The protocol's advanced cryptographic techniques guarantee that your vote contributes to + the final outcome without any risk of privacy breaches or undue influence. +

-
-

WHAT DOES THIS MEAN?

-

+

+

WHAT DOES THIS MEAN?

+

Your participation has directly contributed to a transparent and fair decision-making process, showcasing the power of privacy-preserving technology in governance and beyond. The use of CRISP in this vote represents a significant step towards secure, anonymous, and tamper-proof digital elections and polls. This innovation ensures that every vote counts @@ -119,15 +111,11 @@ const PollResult: React.FC = () => {

)} - {pastPolls.length > 0 && ( -
- -
- )} + {pastPolls.length > 0 && } )} -
-
+
+
) } diff --git a/examples/CRISP/test/crisp.spec.ts b/examples/CRISP/test/crisp.spec.ts index 9a0ac312c5..cd385bf70c 100644 --- a/examples/CRISP/test/crisp.spec.ts +++ b/examples/CRISP/test/crisp.spec.ts @@ -68,7 +68,7 @@ const test = testWithSynpress(metaMaskFixtures(basicSetup)) const { expect } = test async function ensureHomePageLoaded(page: Page) { - return await expect(page.locator('h4')).toHaveText('Coercion-Resistant Impartial Selection Protocol') + return await expect(page.getByText('Coercion-Resistant Impartial Selection Protocol')).toBeVisible() } function log(msg: string) { @@ -133,7 +133,7 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => log(`connecting to dapp...`) await metamask.connectToDapp() log(`clicking try demo...`) - await page.locator('button:has-text("Try Demo")').click() + await page.locator('a:has-text("Try the demo")').click() log(`waiting for E3 Committee being published...`) await waitForE3Ready(e3id) @@ -143,22 +143,22 @@ test('CRISP smoke test', async ({ context, page, metamaskPage, extensionId }) => await page.reload() log(`clicking first vote card...`) - await page.locator("[data-test-id='poll-button-0'] > [data-test-id='card']").click() + await page.locator("[data-test-id='poll-button-0']").click() log(`clicking Cast Vote...`) - await page.locator('button:has-text("Cast Vote")').click() + await page.locator('button:has-text("Cast")').click() log(`confirming MetaMask signature request...`) await metamask.confirmSignature() const WAIT = E3_DURATION - DKG_DURATION + OUTPUT_DECRYPTION_WAIT log(`waiting ${WAIT}ms...`) await page.waitForTimeout(WAIT) log(`clicking historic polls button...`) - await page.locator('a:has-text("Historic polls")').click() + await page.locator('a:has-text("Historic Polls")').click() log(`asserting that Historic polls exists...`) - await expect(page.locator('h1')).toHaveText('Historic polls') + await expect(page.locator('h1')).toHaveText('Past polls') log(`asserting that result has 100% on the vote we clicked on...`) - await expect(page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] h3")).toHaveText('100%') + await expect(page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-0'] .h2")).toHaveText('100%') log(`asserting that result has 0% on the vote we did not click on...`) - await expect(page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] h3")).toHaveText('0%') + await expect(page.locator("[data-test-id='poll-0-0'] [data-test-id='poll-result-1'] .h2")).toHaveText('0%') log('============================================') log(' PLAYWRIGHT TEST IS COMPLETE ')