Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ test-results/
# submodules
examples/CRISP/packages/crisp-contracts/lib/risc0-ethereum
templates/default/lib/risc0-ethereum

.claude/
.claude/settings.local.json
2 changes: 0 additions & 2 deletions examples/CRISP/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

<!-- {/* Open Graph / Facebook */} -->
<meta property="og:type" content="website" />
<meta property="og:url" content="" />
<meta property="og:title" content="CRISP" />
<meta
property="og:description"
Expand All @@ -29,7 +28,6 @@

<!-- {/* Twitter */} -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="" />
<meta property="twitter:title" content="CRISP" />
<meta
property="twitter:description"
Expand Down
3 changes: 2 additions & 1 deletion examples/CRISP/client/libs/crispWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ self.onmessage = async function (event) {
switch (type) {
case 'generate_proof':
try {
const { voteId, publicKey, address, signature } = data
const { voteId, publicKey, address, signature, previousCiphertext } = data

// voteId is either 0 or 1, so we need to encode the vote accordingly.
// We are adapting to the current CRISP application.
Expand All @@ -31,6 +31,7 @@ self.onmessage = async function (event) {
signature,
merkleLeaves,
balance,
previousCiphertext,
})
const encodedProof = encodeSolidityProof(proof)

Expand Down
2 changes: 2 additions & 0 deletions examples/CRISP/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import HistoricPoll from '@/pages/HistoricPoll/HistoricPoll'
import About from '@/pages/About/About'
import PollResult from '@/pages/PollResult/PollResult'
import RoundPoll from '@/pages/RoundPoll'
import useScrollToTop from '@/hooks/generic/useScrollToTop'
import { useVoteManagementContext } from '@/context/voteManagement'

Expand All @@ -26,7 +27,7 @@
await initialLoad()
}
loadWasm()
}, [])

Check warning on line 30 in examples/CRISP/client/src/App.tsx

View workflow job for this annotation

GitHub Actions / integration_prebuild

React Hook useEffect has a missing dependency: 'initialLoad'. Either include it or remove the dependency array

return (
<Fragment>
Expand All @@ -37,6 +38,7 @@
<Route path='/' element={<Landing />} />
<Route path='/about' element={<About />} />
<Route path='/current' element={<DailyPoll />} />
<Route path='/round/:roundId' element={<RoundPoll />} />
<Route path='/historic' element={<HistoricPoll />} />
<Route path='/result/:roundId/:type?' element={<PollResult />} />
<Route path='*' element={<Navigate to='/' replace />} />
Expand Down
42 changes: 32 additions & 10 deletions examples/CRISP/client/src/components/Cards/PollCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,40 @@ import { useVoteManagementContext } from '@/context/voteManagement'
const PollCard: React.FC<PollResult> = ({ roundId, options, totalVotes, date, endTime }) => {
const navigate = useNavigate()
const [results, setResults] = useState<PollOption[]>(options)
const { roundState, setPollResult } = useVoteManagementContext()
const [isActive, setIsActive] = useState(!hasPollEndedByTimestamp(endTime))
const { roundState, setPollResult, currentRoundId } = useVoteManagementContext()

const isActive = !hasPollEndedByTimestamp(endTime)
const activeTotalCount = roundState?.vote_count ?? 0
const isCurrentRound = roundId === currentRoundId
const displayVoteCount = isCurrentRound && isActive ? (roundState?.vote_count ?? totalVotes) : totalVotes

useEffect(() => {
if (!isActive) return

const checkPollStatus = () => {
const pollEnded = hasPollEndedByTimestamp(endTime)
if (pollEnded) {
setIsActive(false)
}
}

checkPollStatus()
const interval = setInterval(checkPollStatus, 1000)

return () => clearInterval(interval)
}, [endTime, isActive])

useEffect(() => {
const newPollOptions = markWinner(options)
setResults(newPollOptions)
}, [options])

const handleNavigation = () => {
if (isActive) {
if (isActive && isCurrentRound) {
return navigate('/current')
}
if (isActive && !isCurrentRound) {
return navigate(`/round/${roundId}`)
}
navigate(`/result/${roundId}`)
setPollResult({
roundId,
Expand All @@ -41,22 +61,24 @@ const PollCard: React.FC<PollResult> = ({ roundId, options, totalVotes, date, en

return (
<div
className='relative flex min-h-[248px] w-full cursor-pointer flex-col items-center justify-center space-y-4 rounded-3xl border-2 border-slate-600/20 bg-white/50 p-8 pt-2 shadow-lg md:max-w-[274px]'
className='relative flex min-h-[248px] w-full cursor-pointer flex-col items-center justify-center space-y-4 rounded-3xl border-2 border-slate-600/20 bg-white/50 p-8 pt-2 shadow-lg md:max-w-[274px] hover:border-slate-600/40 transition-colors'
onClick={handleNavigation}
>
<div className='external-icon absolute right-4 top-4' />
<div className='external-icon absolute right-4 top-4' />
<div className='text-xs font-bold text-slate-600'>{formatDate(date)}</div>
<div className='flex space-x-8 '>
<PollCardResult results={results} totalVotes={isActive ? activeTotalCount : totalVotes} isActive={isActive} />
<PollCardResult results={results} totalVotes={displayVoteCount} isActive={isActive} />
</div>
{isActive && (
<div className='flex items-center space-x-2 rounded-lg border-2 border-lime-600/80 bg-lime-400 px-2 py-1 text-center font-bold uppercase leading-none text-white'>
<div
className={`flex items-center space-x-2 rounded-lg border-2 ${isCurrentRound ? 'border-lime-600/80 bg-lime-400' : 'border-blue-600/80 bg-blue-400'} px-2 py-1 text-center font-bold uppercase leading-none text-white`}
>
<div className='h-1.5 w-1.5 animate-pulse rounded-full bg-white'></div>
<div>Active</div>
<div>{isCurrentRound ? 'Live' : 'Active'}</div>
</div>
)}
<div className='absolute bottom-[-1rem] left-1/2 -translate-x-1/2 transform '>
<VotesBadge totalVotes={isActive ? activeTotalCount : totalVotes} />
<VotesBadge totalVotes={displayVoteCount} />
</div>
</div>
)
Expand Down
71 changes: 51 additions & 20 deletions examples/CRISP/client/src/components/ToastAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,92 @@

// ToastAlert.tsx
import React, { useEffect } from 'react'
import { Link, X } from '@phosphor-icons/react'
import { Link, X, Warning, Info } from '@phosphor-icons/react'

type ToastAlertProps = {
type: 'success' | 'danger'
type: 'success' | 'danger' | 'warning' | 'info'
linkUrl?: string
message: string
onClose: () => void
persistent?: boolean
duration?: number
id?: string
}

const ToastAlert: React.FC<ToastAlertProps> = ({ message, type, linkUrl, onClose }) => {
const DEFAULT_DURATION = 5000

const ToastAlert: React.FC<ToastAlertProps> = ({ message, type, linkUrl, onClose, persistent = false, duration }) => {
useEffect(() => {
if (persistent) return

const timerDuration = duration || DEFAULT_DURATION
const timer = setTimeout(() => {
onClose()
}, 5000) // Toast will close after 5 seconds
}, timerDuration)

return () => clearTimeout(timer) // Clean up the timer
}, [onClose])
}, [onClose, persistent, duration])

const alertStyles = {
success: {
container: 'border-lime-600/80 shadow-button-outlined',
text: 'text-lime-600',
button: 'text-lime-600 hover:text-lime-700',
icon: null,
},
danger: {
container: 'border-red-600/80 shadow-danger',
text: 'text-red-600',
button: 'text-red-600 hover:text-red-700',
icon: null,
},
warning: {
container: 'border-amber-500/80 shadow-lg',
text: 'text-amber-600',
button: 'text-amber-600 hover:text-amber-700',
icon: Warning,
},
info: {
container: 'border-blue-500/80 shadow-lg',
text: 'text-blue-600',
button: 'text-blue-600 hover:text-blue-700',
icon: Info,
},
}

const currentAlertStyle = alertStyles[type]
const IconComponent = currentAlertStyle.icon

return (
<div className='toast-alert fixed bottom-8 left-8 z-[9999] transform transition-transform'>
<div className='toast-alert relative transform transition-transform animate-in slide-in-from-left-5 pointer-events-auto'>
<div
className={`shadow-toast w-min-[366px] flex h-[46px] items-center rounded-[16px] border-2 ${currentAlertStyle.container} bg-white px-6`}
className={`shadow-toast min-w-[366px] max-w-[500px] flex items-center rounded-[16px] border-2 ${currentAlertStyle.container} bg-white px-6 py-3`}
>
<div className='flex w-full items-center justify-between'>
{linkUrl && (
<a
href={linkUrl}
target='_blank'
className={`mr-6 flex items-center text-base font-extrabold uppercase leading-6 ${currentAlertStyle.text}`}
>
<Link size={16} weight='bold' className={`mr-2 ${currentAlertStyle.button}`} />
{message}
</a>
)}
{!linkUrl && <p className={`mr-3 text-base font-extrabold uppercase leading-6 ${currentAlertStyle.text}`}>{message}</p>}
<div className='flex w-full items-center justify-between gap-3'>
<div className='flex items-center gap-2 flex-1'>
{IconComponent && <IconComponent size={20} weight='bold' className={currentAlertStyle.text} />}
{linkUrl ? (
<a
href={linkUrl}
target='_blank'
rel='noopener noreferrer'
className={`flex items-center text-base font-extrabold uppercase leading-6 ${currentAlertStyle.text}`}
>
<Link size={16} weight='bold' className={`mr-2 ${currentAlertStyle.button}`} />
{message}
</a>
) : (
<p className={`text-base font-extrabold uppercase leading-6 ${currentAlertStyle.text}`}>{message}</p>
)}
</div>

<button onClick={onClose}>
<button onClick={onClose} className='flex-shrink-0 ml-2'>
<X weight='bold' size={16} className={currentAlertStyle.button} />
</button>
</div>
{persistent && (
<div className='absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse' title='Requires manual dismissal' />
)}
Comment thread
hmzakhalid marked this conversation as resolved.
</div>
</div>
)
Expand Down
105 changes: 105 additions & 0 deletions examples/CRISP/client/src/components/VotingStepIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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.

import React from 'react'
import { VotingStep } from '@/hooks/voting/useVoteCasting'
import { CheckIcon, CircleNotchIcon, WarningIcon, PencilSimpleIcon, LockIcon, BroadcastIcon, ShieldCheckIcon } from '@phosphor-icons/react'

type VotingStepIndicatorProps = {
step: VotingStep
message: string
lastActiveStep?: VotingStep | null
}

const steps: { key: VotingStep; label: string; icon: React.ElementType }[] = [
{ key: 'signing', label: 'Sign', icon: PencilSimpleIcon },
{ key: 'encrypting', label: 'Encrypt', icon: LockIcon },
{ key: 'generating_proof', label: 'Proof', icon: ShieldCheckIcon },
{ key: 'broadcasting', label: 'Broadcast', icon: BroadcastIcon },
]

const VotingStepIndicator: React.FC<VotingStepIndicatorProps> = ({ step, message, lastActiveStep }) => {
const getStepStatus = (stepKey: VotingStep) => {
const stepOrder = steps.map((s) => s.key)
const currentIndex = step === 'error' ? stepOrder.indexOf(lastActiveStep ?? 'signing') : stepOrder.indexOf(step)
const stepIndex = stepOrder.indexOf(stepKey)

if (step === 'complete') return 'complete'
if (step === 'error') return stepIndex <= currentIndex ? 'error' : 'pending'
if (stepIndex < currentIndex) return 'complete'
if (stepIndex === currentIndex) return 'active'
return 'pending'
}
Comment thread
hmzakhalid marked this conversation as resolved.

const getStepStyles = (status: string) => {
switch (status) {
case 'complete':
return {
circle: 'bg-lime-500 border-lime-500 text-white',
text: 'text-lime-600',
line: 'bg-lime-500',
}
case 'active':
return {
circle: 'bg-white border-lime-500 text-lime-500 animate-pulse',
text: 'text-lime-600 font-bold',
line: 'bg-slate-200',
}
case 'error':
return {
circle: 'bg-red-500 border-red-500 text-white',
text: 'text-red-600',
line: 'bg-red-300',
}
default:
return {
circle: 'bg-white border-slate-300 text-slate-400',
text: 'text-slate-400',
line: 'bg-slate-200',
}
}
}

return (
<div className='flex flex-col items-center justify-center space-y-4 py-4 w-full max-w-md'>
{/* Step indicators */}
<div className='flex items-center justify-between w-full px-4'>
{steps.map((s, index) => {
const status = getStepStatus(s.key)
const styles = getStepStyles(status)
const Icon = s.icon

return (
<React.Fragment key={s.key}>
<div className='flex flex-col items-center'>
<div className={`w-10 h-10 rounded-full border-2 flex items-center justify-center ${styles.circle}`}>
{status === 'complete' ? (
<CheckIcon size={20} weight='bold' />
) : status === 'active' ? (
<CircleNotchIcon size={20} weight='bold' className='animate-spin' />
) : status === 'error' ? (
<WarningIcon size={20} weight='bold' />
) : (
<Icon size={20} weight='bold' />
)}
</div>
<span className={`text-xs mt-1 ${styles.text}`}>{s.label}</span>
</div>
{index < steps.length - 1 && <div className={`flex-1 h-0.5 mx-2 ${styles.line}`} />}
</React.Fragment>
)
})}
</div>

{/* Current step message */}
<div className='text-center'>
<p className='text-base font-bold uppercase text-slate-600/70'>{message}</p>
</div>
</div>
)
}

export default VotingStepIndicator
Loading
Loading