event-chips.tsx44 lines · main
| 1 | 'use client'; |
| 2 | |
| 3 | import { motion } from 'motion/react'; |
| 4 | |
| 5 | import { CountUp } from '../_components/stat-card'; |
| 6 | |
| 7 | /** |
| 8 | * Animated event-type count chips for the email-events page. Each chip |
| 9 | * fades/slides in with a small stagger and its count counts up. Counts |
| 10 | * are computed server-side from the REAL fetched events — this component |
| 11 | * only animates what it's given (honest-data rule). |
| 12 | */ |
| 13 | |
| 14 | export interface EventChip { |
| 15 | type: string; |
| 16 | count: number; |
| 17 | severity: 'ok' | 'warn' | 'fail'; |
| 18 | } |
| 19 | |
| 20 | const SEVERITY_CLASS: Record<EventChip['severity'], string> = { |
| 21 | ok: 'border-[var(--color-border-primary)] bg-[var(--color-primary-subtle)] text-[var(--color-primary)]', |
| 22 | fail: 'border-red-500/40 bg-red-500/10 text-red-400', |
| 23 | warn: 'border-yellow-500/40 bg-yellow-500/10 text-yellow-300', |
| 24 | }; |
| 25 | |
| 26 | export function EventChips({ chips }: { chips: readonly EventChip[] }) { |
| 27 | if (chips.length === 0) return null; |
| 28 | return ( |
| 29 | <div className="flex flex-wrap gap-3"> |
| 30 | {chips.map((c, i) => ( |
| 31 | <motion.span |
| 32 | key={c.type} |
| 33 | initial={{ opacity: 0, y: 8 }} |
| 34 | animate={{ opacity: 1, y: 0 }} |
| 35 | transition={{ duration: 0.35, delay: i * 0.05, ease: 'easeOut' }} |
| 36 | className={`inline-flex items-baseline gap-2 rounded-full border px-3 py-1.5 font-mono text-xs ${SEVERITY_CLASS[c.severity]}`} |
| 37 | > |
| 38 | {c.type} |
| 39 | <CountUp value={c.count} className="font-mono text-xs" /> |
| 40 | </motion.span> |
| 41 | ))} |
| 42 | </div> |
| 43 | ); |
| 44 | } |