Implements a live, real-time participants feed on RafflePage with:
- Polling: Fetches new participants every 15 seconds
- Optimistic Updates: Instantly shows current user's purchase before API confirmation
- Animations: Smooth slide-in animations for new entries (respects
prefers-reduced-motion) - Capping: Displays max 20 participants, dropping oldest as new ones arrive
- Polling:
useEffectwith 15-second interval polling/raffles/:id/participants?since= - Optimistic Updates: Exposes
__addOptimisticParticipanton window for parent component - Animations: CSS keyframes
slideInDownwith staggered delays (respectsprefers-reduced-motion) - Avatar Generation: Deterministic color from address hash
- Capping: Slices to MAX_DISPLAYED (20) after each update
- Import: Added
useAuthhook andRecentParticipantscomponent - Handler:
handleTicketPurchasecalls__addOptimisticParticipantwith user address - Integration: Renders
<RecentParticipants>in left column after metadata section
- Tests for loading, polling, optimistic updates, animations, capping, and error handling
- New Endpoint:
GET /raffles/:id/participants?since= - Query Param:
since(unix timestamp in ms) for incremental updates - Response: Array of
{ address: string; timestamp: number }
- New Method:
getRecentParticipants(raffleId, sinceTimestamp) - Placeholder: Returns empty array (TODO: integrate with indexer for ticket purchase events)
-
New participants appear without page refresh (within 15 s)
- Polling interval: 15 seconds
- Fetches from
/raffles/:id/participants?since=<lastFetchTime>
-
New entries animate in from the top
- CSS keyframes:
slideInDown(300ms, ease-out) - Staggered delays: 50ms per entry
- Smooth opacity and transform transitions
- CSS keyframes:
-
Animation skipped when prefers-reduced-motion: reduce
- Detects
window.matchMedia("(prefers-reduced-motion: reduce)") - Applies empty animation style when true
- Detects
-
Optimistic self-entry appears instantly after purchase
handleTicketPurchasecalls__addOptimisticParticipant(address)- Prepends entry with
isOptimistic: trueflag - Shows green pulse indicator and "(pending)" tooltip
- Removed when API confirms (poll returns same address)
RafflePage
├─ handleTicketPurchase()
│ └─ __addOptimisticParticipant(userAddress)
│ └─ RecentParticipants state update
│
└─ <RecentParticipants raffleId={raffleId} currentUserAddress={address} />
├─ Initial load: GET /raffles/:id/participants
├─ Poll every 15s: GET /raffles/:id/participants?since=<timestamp>
└─ Render avatars with animations
interface Participant {
address: string;
timestamp: number;
isOptimistic?: boolean; // True for pending optimistic entries
}
// Max 20 displayed
const participants: Participant[] = [];- Initial Load: Fetch all participants, store
lastFetchTimeRef - Poll Loop (every 15s):
- Fetch participants since
lastFetchTimeRef - Remove optimistic entries that are now confirmed
- Prepend new participants
- Cap at 20, drop oldest
- Update
lastFetchTimeRef
- Fetch participants since
- User clicks "Buy Tickets"
handleTicketPurchase()calls__addOptimisticParticipant(address)- Component prepends
{ address, timestamp: now(), isOptimistic: true } - Renders with green pulse indicator
- Next poll removes optimistic flag when API confirms
Query Parameters:
since(optional): Unix timestamp in milliseconds. Returns participants since this time.
Response:
[
{
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5V3VQ",
"timestamp": 1716950000000
},
{
"address": "GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBY5V3VQ",
"timestamp": 1716950015000
}
]Status Codes:
200: Success404: Raffle not found
- Polling: Uses
setIntervalwith cleanup on unmount - Optimistic Updates: Exposed via
window.__addOptimisticParticipantfor parent component access - Animations: CSS keyframes with
prefers-reduced-motiondetection - Avatar Colors: Deterministic hash-based color generation for consistency
- Tooltips: Hover shows full address and "(pending)" for optimistic entries
- Placeholder:
getRecentParticipantsreturns empty array - TODO: Integrate with indexer to query ticket purchase events from blockchain
- Future: Consider caching recent participants in Redis for performance
- Loading state
- Participant display
- Polling interval (15s)
- Optimistic updates
- Animation respect for
prefers-reduced-motion - Capping at 20 participants
- Error handling
- Polling: Open RafflePage, wait 15s, verify new participants appear
- Optimistic: Click "Buy Tickets", verify address appears instantly with green pulse
- Animation: Observe smooth slide-in from top (disable in DevTools if needed)
- Reduced Motion: Enable
prefers-reduced-motion: reducein OS, verify no animation - Capping: Verify max 20 avatars displayed
- WebSocket: Replace polling with real-time WebSocket updates
- Indexer Integration: Query blockchain events for actual ticket purchases
- Pagination: "View All" link to show all participants
- Filtering: Filter by time range, address prefix
- Analytics: Track participant engagement metrics
- Notifications: Notify user when their purchase is confirmed
If issues arise:
- Frontend: Remove
<RecentParticipants>from RafflePage - Backend: Remove
/raffles/:id/participantsendpoint - Revert:
git revert <commit-hash>
- Issue: #486
- Component:
client/src/components/RecentParticipants.tsx - Page:
client/src/pages/RafflePage.tsx - Endpoint:
GET /raffles/:id/participants