- {roundState.vote_count} votes
+ )}
+
+ {endTime && !isEnded && !busy && (
+
- 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..559121d3e3 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
-

-
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.
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- 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..18bb8bf2a4 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) => (
+
+ ))}
+
+
+
+
+
+
)
}
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 && (
+
+ )}
+
{
{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 &&
}
)}
-
-
+
+
)
}
From e8575adc26454ce3d04569d9c7a0d7a32bb6268b Mon Sep 17 00:00:00 2001
From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com>
Date: Thu, 21 May 2026 20:30:21 +0100
Subject: [PATCH 2/4] chore: pr comments
---
.../client/src/components/Cards/PollCard.tsx | 17 +++++++--
.../CRISP/client/src/components/Footer.tsx | 30 +++++++++-------
.../CRISP/client/src/design/Editorial.tsx | 3 +-
.../pages/Landing/components/DailyPoll.tsx | 35 ++++++++++++++-----
.../src/pages/Landing/components/Hero.tsx | 4 +--
.../src/pages/Landing/components/PastPoll.tsx | 4 +--
6 files changed, 63 insertions(+), 30 deletions(-)
diff --git a/examples/CRISP/client/src/components/Cards/PollCard.tsx b/examples/CRISP/client/src/components/Cards/PollCard.tsx
index aaac25bed4..8dc8b96752 100644
--- a/examples/CRISP/client/src/components/Cards/PollCard.tsx
+++ b/examples/CRISP/client/src/components/Cards/PollCard.tsx
@@ -59,9 +59,20 @@ const PollCard: React.FC = ({ roundId, options, totalVotes, date, en
}
return (
-
@@ -75,7 +86,7 @@ const PollCard: React.FC
= ({ roundId, options, totalVotes, date, en
{isActive && {isCurrentRound ? 'Live' : 'Active'}}
-
+
)
}
diff --git a/examples/CRISP/client/src/components/Footer.tsx b/examples/CRISP/client/src/components/Footer.tsx
index 3467f2788e..31816c6173 100644
--- a/examples/CRISP/client/src/components/Footer.tsx
+++ b/examples/CRISP/client/src/components/Footer.tsx
@@ -6,7 +6,6 @@
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 = () => {
@@ -16,25 +15,30 @@ const Footer: React.FC = () => {
© 2026 — Crisp Protocol
Secret-ballot voting with FHE + threshold MPC
diff --git a/examples/CRISP/client/src/design/Editorial.tsx b/examples/CRISP/client/src/design/Editorial.tsx
index 300886292c..8f5af20793 100644
--- a/examples/CRISP/client/src/design/Editorial.tsx
+++ b/examples/CRISP/client/src/design/Editorial.tsx
@@ -54,9 +54,10 @@ export function Cipher({
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 += blockSize) arr.push(raw.slice(i, i + blockSize))
+ for (let i = 0; i < length; i += step) arr.push(raw.slice(i, i + step))
return arr
}, [seed, length, blockSize])
return (
diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
index 65c1c4cdea..b06de5c812 100644
--- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
+++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
@@ -4,7 +4,7 @@
// 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'
@@ -57,17 +57,30 @@ const DailyPollSection: React.FC
= ({ loading, endTime, t
const { setOpen } = useModal()
const { castVoteWithProof, isVoting: isCastingVote, isMasking, votingStep, lastActiveStep, stepMessage } = useVoteCasting()
+ // Derived state (isEnded, tallyReady) is round-local. Tracking the round id
+ // lets us clear tallyReady when the round changes so a new active poll doesn't
+ // inherit the previous round's results state.
+ const trackedRoundId = useRef(roundState?.id)
+
useEffect(() => {
+ let cancelled = false
;(async () => {
- if (!client) return
- if (!roundState) return
+ if (!client || !roundState) return
- const block = await client.getBlock()
+ if (trackedRoundId.current !== roundState.id) {
+ trackedRoundId.current = roundState.id
+ setTallyReady(false)
+ }
- if (block.timestamp > roundState.end_time) {
- setIsEnded(true)
+ const block = await client.getBlock()
+ if (!cancelled) {
+ setIsEnded(block.timestamp > roundState.end_time)
}
})()
+
+ return () => {
+ cancelled = true
+ }
}, [roundState, client])
// Once the poll is over, poll the backend until the FHE tally is published.
@@ -76,9 +89,13 @@ const DailyPollSection: React.FC = ({ loading, endTime, t
let cancelled = false
const check = async () => {
- const result = await getWebResultByRound(roundState.id)
- if (!cancelled && result && Array.isArray(result.tally) && result.tally.length > 0) {
- setTallyReady(true)
+ 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.
}
}
diff --git a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx
index 559121d3e3..055b1696d0 100644
--- a/examples/CRISP/client/src/pages/Landing/components/Hero.tsx
+++ b/examples/CRISP/client/src/pages/Landing/components/Hero.tsx
@@ -67,8 +67,8 @@ const HeroSection: React.FC = () => {
-
-
+
+ Try the demo →
diff --git a/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx
index 18bb8bf2a4..aac5a56763 100644
--- a/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx
+++ b/examples/CRISP/client/src/pages/Landing/components/PastPoll.tsx
@@ -30,8 +30,8 @@ const PastPollSection: React.FC = ({ customLabel = 'Past p
))}
-
-
+
+ View all polls →
From fe7de43afd065cdc79246b65a8ea520e22fdf272 Mon Sep 17 00:00:00 2001
From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com>
Date: Thu, 21 May 2026 21:14:04 +0100
Subject: [PATCH 3/4] fix: tests
---
.../src/pages/Landing/components/DailyPoll.tsx | 1 +
examples/CRISP/test/crisp.spec.ts | 16 ++++++++--------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
index b06de5c812..0a3974d3dd 100644
--- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
+++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
@@ -70,6 +70,7 @@ const DailyPollSection: React.FC
= ({ loading, endTime, t
if (trackedRoundId.current !== roundState.id) {
trackedRoundId.current = roundState.id
setTallyReady(false)
+ setIsEnded(false)
}
const block = await client.getBlock()
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 ')
From a2edcb062db8aaa9140c92672afdc79c5c6cad55 Mon Sep 17 00:00:00 2001
From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com>
Date: Thu, 21 May 2026 21:39:04 +0100
Subject: [PATCH 4/4] chore: pr comments
---
examples/CRISP/client/src/design/editorial.css | 6 +++---
.../src/pages/Landing/components/DailyPoll.tsx | 18 ++++++++++++------
2 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/examples/CRISP/client/src/design/editorial.css b/examples/CRISP/client/src/design/editorial.css
index 9119293ef5..3802d14285 100644
--- a/examples/CRISP/client/src/design/editorial.css
+++ b/examples/CRISP/client/src/design/editorial.css
@@ -32,9 +32,9 @@
--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-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);
diff --git a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
index 0a3974d3dd..619b498c4a 100644
--- a/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
+++ b/examples/CRISP/client/src/pages/Landing/components/DailyPoll.tsx
@@ -57,9 +57,9 @@ const DailyPollSection: React.FC = ({ loading, endTime, t
const { setOpen } = useModal()
const { castVoteWithProof, isVoting: isCastingVote, isMasking, votingStep, lastActiveStep, stepMessage } = useVoteCasting()
- // Derived state (isEnded, tallyReady) is round-local. Tracking the round id
- // lets us clear tallyReady when the round changes so a new active poll doesn't
- // inherit the previous round's results state.
+ // 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(() => {
@@ -71,11 +71,17 @@ const DailyPollSection: React.FC = ({ loading, endTime, t
trackedRoundId.current = roundState.id
setTallyReady(false)
setIsEnded(false)
+ setPollSelected(null)
+ setNoPollSelected(true)
}
- const block = await client.getBlock()
- if (!cancelled) {
- setIsEnded(block.timestamp > roundState.end_time)
+ 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.
}
})()