/** * Leaderboard Page * * Demonstrates: * - useQuery for fetching ordered records * - useMutations for CRUD operations * - useUser for role-aware actions */ import { useState } from 'react' import { BarChart3, Medal, Pencil, Plus, Trash2, TriangleAlert } from 'lucide-react' import { ROLES, useMutations, useQuery, useUser, type Role } from 'deepspace' import { Badge, Button, ConfirmModal, EmptyState, Input, Label, Modal, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, useToast, type BadgeProps, } from '@/components/ui' type BadgeVariant = BadgeProps['variant'] const LEADERBOARD_CATEGORY = { GENERAL: 'general', SPEED: 'speed', ACCURACY: 'accuracy', } as const type LeaderboardCategory = (typeof LEADERBOARD_CATEGORY)[keyof typeof LEADERBOARD_CATEGORY] const CATEGORY_CONFIG: Record = { [LEADERBOARD_CATEGORY.GENERAL]: { title: 'General', color: 'info' }, [LEADERBOARD_CATEGORY.SPEED]: { title: 'Speed', color: 'warning' }, [LEADERBOARD_CATEGORY.ACCURACY]: { title: 'Accuracy', color: 'success' }, } interface LeaderboardEntry { playerName: string score: number category: LeaderboardCategory playerId: string } type LeaderboardRecord = { recordId: string data: LeaderboardEntry } interface LeaderboardPageProps { className?: string } interface SubmitScoreModalProps { onClose: () => void onSubmit: (playerName: string, score: number, category: LeaderboardCategory) => Promise userName: string } function SubmitScoreModal({ onClose, onSubmit, userName }: SubmitScoreModalProps) { const [playerName, setPlayerName] = useState(userName) const [score, setScore] = useState('') const [category, setCategory] = useState(LEADERBOARD_CATEGORY.GENERAL) const [submitting, setSubmitting] = useState(false) const parsedScore = Number(score) const isValid = playerName.trim().length > 0 && score.trim().length > 0 && Number.isFinite(parsedScore) const handleSubmit = async () => { if (!isValid || submitting) return setSubmitting(true) try { const submitted = await onSubmit(playerName.trim(), parsedScore, category) if (!submitted) return onClose() } finally { setSubmitting(false) } } return ( undefined : onClose} size="sm"> Submit Score
setPlayerName(event.target.value)} placeholder="Your name" disabled={submitting} autoFocus />
setScore(event.target.value)} placeholder="Enter your score" disabled={submitting} />
) } interface EditScoreModalProps { entry: LeaderboardRecord onClose: () => void onSave: (entry: LeaderboardRecord, score: number) => Promise } function EditScoreModal({ entry, onClose, onSave }: EditScoreModalProps) { const [score, setScore] = useState(String(entry.data.score)) const [saving, setSaving] = useState(false) const parsedScore = Number(score) const isValid = score.trim().length > 0 && Number.isFinite(parsedScore) const handleSave = async () => { if (!isValid || saving) return setSaving(true) try { const saved = await onSave(entry, parsedScore) if (!saved) return onClose() } finally { setSaving(false) } } return ( undefined : onClose} size="sm"> Edit score for {entry.data.playerName} setScore(event.target.value)} disabled={saving} autoFocus /> ) } function RankBadge({ rank }: { rank: number }) { if (rank <= 3) { const label = rank === 1 ? '1st' : rank === 2 ? '2nd' : '3rd' const variant: BadgeVariant = rank === 1 ? 'warning' : rank === 2 ? 'secondary' : 'outline' return ( ) } return #{rank} } export default function LeaderboardPage({ className }: LeaderboardPageProps) { const { user } = useUser() const userRole = (user?.role ?? ROLES.VIEWER) as Role const isAdmin = userRole === ROLES.ADMIN const canSubmit = userRole === ROLES.MEMBER || isAdmin const toast = useToast() const [showSubmitModal, setShowSubmitModal] = useState(false) const [editingEntry, setEditingEntry] = useState(null) const [deleteTarget, setDeleteTarget] = useState(null) const [deleting, setDeleting] = useState(false) const { records: entries, status, error: queryError, } = useQuery('leaderboard', { orderBy: 'score', orderDir: 'desc', }) const { createConfirmed, putConfirmed, removeConfirmed } = useMutations('leaderboard') const handleSubmit = async ( playerName: string, score: number, category: LeaderboardCategory, ): Promise => { if (!user) { toast.error('Could not submit score', 'Sign in and try again.') return false } try { await createConfirmed({ playerName, score, category, playerId: user.id }) toast.success( 'Score submitted', `${score.toLocaleString()} points recorded for "${playerName}".`, ) return true } catch (error) { toast.error( 'Could not submit score', error instanceof Error ? error.message : 'Please try again.', ) return false } } const handleEditSave = async (entry: LeaderboardRecord, newScore: number): Promise => { try { await putConfirmed(entry.recordId, { score: newScore }) toast.success( 'Score updated', `"${entry.data.playerName}" now has ${newScore.toLocaleString()} points.`, ) return true } catch (error) { toast.error( 'Could not update score', error instanceof Error ? error.message : 'Please try again.', ) return false } } const handleDelete = async () => { if (!deleteTarget || deleting) return setDeleting(true) try { await removeConfirmed(deleteTarget.recordId) toast.success('Score deleted', `The score for "${deleteTarget.data.playerName}" was deleted.`) setDeleteTarget(null) } catch (error) { toast.error( 'Could not delete score', error instanceof Error ? error.message : 'Please try again.', ) } finally { setDeleting(false) } } const isLoading = status === 'loading' return (

Leaderboard

{isLoading ? 'Loading current rankings...' : canSubmit ? 'Submit a score and see where you rank.' : 'Scores are ranked from highest to lowest.'}

{canSubmit && ( )}
{isLoading ? (
) : status === 'error' ? (
{showSubmitModal && ( setShowSubmitModal(false)} onSubmit={handleSubmit} userName={user?.name ?? ''} /> )} {editingEntry && ( setEditingEntry(null)} onSave={handleEditSave} /> )} { if (!deleting) setDeleteTarget(null) }} onConfirm={() => void handleDelete()} title={ deleteTarget ? `Delete score for "${deleteTarget.data.playerName}"?` : 'Delete score?' } description={ deleteTarget ? `This permanently removes the ${deleteTarget.data.score.toLocaleString()}-point score.` : 'This cannot be undone.' } confirmText="Delete" loading={deleting} />
) }