feat: spectator reactions with floating emoji particles

POST /api/fights/:fightId/react endpoint accepts emoji reactions (fist,
fire, skull, 100, clown), aggregates counts, broadcasts via SSE.
Reaction bar added to FightViewer with floating emoji particles that
rise and fade. Live fight views receive reactions via SSE in real-time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 19:51:13 +00:00
co-authored by Claude Opus 4.6
parent baf153b115
commit fd22b126e7
3 changed files with 165 additions and 0 deletions
+32
View File
@@ -15,6 +15,10 @@ export const fightsRouter = new Hono()
// Track spectator counts per fight
const spectatorCounts = new Map<string, number>()
// Track reactions per fight
const VALID_REACTIONS = new Set(['fist', 'fire', 'skull', '100', 'clown'])
const fightReactions = new Map<string, Record<string, number>>()
export function getSpectatorCount(fightId: string): number {
return spectatorCounts.get(fightId) || 0
}
@@ -383,3 +387,31 @@ fightsRouter.get('/:id/stream', (c) => {
}
})
})
// React to a fight
fightsRouter.post('/:id/react', async (c) => {
const fightId = c.req.param('id')
const body = await c.req.json<{ emoji?: string }>()
const emoji = body?.emoji
if (!emoji || !VALID_REACTIONS.has(emoji)) {
return c.json({ error: 'Invalid reaction. Use: fist, fire, skull, 100, clown' }, 400)
}
// Aggregate
if (!fightReactions.has(fightId)) {
fightReactions.set(fightId, {})
}
const counts = fightReactions.get(fightId)!
counts[emoji] = (counts[emoji] || 0) + 1
// Broadcast via SSE
fightEvents.emit({
fightId,
type: 'reaction',
data: { emoji, counts: { ...counts } },
timestamp: new Date().toISOString(),
})
return c.json({ ok: true, counts })
})