import { Link } from '@tanstack/react-router'; import { useLiveQuery } from 'dexie-react-hooks'; import { Brain, Users, Swords, ScrollText, VenetianMask, Target, Map, CalendarDays, Library, Hammer, Dice5, Monitor, ChevronRight, Shield, CheckCheck, Skull, Handshake, } from 'lucide-react'; import type { Campaign, Character, Npc, Quest, DiceRoll } from '@/lib/schemas'; import { diceRepo } from '@/lib/db/repositories'; import { getSystem } from '@/lib/rules'; import { cn } from '@/lib/cn'; import { useCharacters } from '@/features/characters/hooks'; import { useEncounters } from '@/features/combat/hooks'; import { useNotes, useNpcs, useQuests, useCalendar } from './hooks'; import { Page, RequireCampaign } from '@/components/ui/Page'; import { Meter, Badge, Avatar } from '@/components/ui/Codex'; export function DashboardPage() { return {(c) => }; } type NavIcon = React.ComponentType<{ size?: number; className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>; const LINKS: { to: string; label: string; icon: NavIcon }[] = [ { to: '/assistant', label: 'Assistant', icon: Brain }, { to: '/characters', label: 'Characters', icon: Users }, { to: '/combat', label: 'Combat', icon: Swords }, { to: '/notes', label: 'Notes & Wiki', icon: ScrollText }, { to: '/npcs', label: 'NPCs', icon: VenetianMask }, { to: '/quests', label: 'Quests', icon: Target }, { to: '/maps', label: 'Maps', icon: Map }, { to: '/calendar', label: 'Calendar', icon: CalendarDays }, { to: '/compendium', label: 'Compendium', icon: Library }, { to: '/homebrew', label: 'Homebrew', icon: Hammer }, { to: '/dice', label: 'Dice', icon: Dice5 }, { to: '/play', label: 'Player View', icon: Monitor }, ]; function Dashboard({ campaign }: { campaign: Campaign }) { const characters = useCharacters(campaign.id); const encounters = useEncounters(campaign.id); const notes = useNotes(campaign.id); const npcs = useNpcs(campaign.id); const quests = useQuests(campaign.id); const calendar = useCalendar(campaign.id); const rolls = useLiveQuery(() => diceRepo.recent(campaign.id, 8), [campaign.id], []); const sys = getSystem(campaign.system); const pcs = characters.filter((c) => c.kind === 'pc'); const activeQuests = quests.filter((q) => q.status === 'active').length; return ( {/* Editorial hero */} {sys.label} {calendar ? ` · in-world day ${calendar.currentDay}` : ''} {campaign.name} {campaign.description && ( {campaign.description} )} {/* Quick stats */} {/* Navigation cards */} Jump to {LINKS.map((l) => ( {l.label} ))} {/* Party overview */} Party Manage {pcs.length === 0 ? ( No player characters yet. ) : ( {pcs.map((c) => ( ))} )} {/* Cast & threats */} {npcs.length > 0 && ( <> Cast & Threats {npcs.slice(0, 6).map((n) => ( ))} > )} {/* Side column: quests + recent rolls */} ); } function PartyRow({ c, sys }: { c: Character; sys: ReturnType }) { const ac = sys.baseArmorClass({ level: c.level, abilities: c.abilities, armorBonus: c.armorBonus, ...(c.equippedArmor ? { equippedArmor: c.equippedArmor } : {}) }); const ratio = c.hp.max > 0 ? c.hp.current / c.hp.max : 0; const tone = ratio < 0.35 ? 'var(--app-danger)' : undefined; return ( {c.name} {c.hp.current}/{c.hp.max} Lv {c.level} {[c.ancestry, c.className].filter(Boolean).join(' ')} {ac} ); } const NPC_DISPOSITION: Record = { dead: { tone: 'ember', label: 'Hostile', Icon: Skull }, alive: { tone: 'verdigris', label: 'Friendly', Icon: Handshake }, unknown: { tone: 'default', label: 'Unknown', Icon: VenetianMask }, }; function ThreatRow({ n }: { n: Npc }) { const d = NPC_DISPOSITION[n.status]; const Icon = d.Icon; return ( {n.name} {[n.role, n.location].filter(Boolean).join(' · ') || '—'} {d.label} ); } function QuestRow({ q }: { q: Quest }) { const done = q.status === 'completed'; return ( {done ? ( ) : ( )} {q.title} {q.objectives.length > 0 && ( {q.objectives.slice(0, 6).map((o) => ( ))} )} ); } function RollRow({ r }: { r: DiceRoll }) { return ( {r.label ? `${r.label}: ` : ''} {r.expression} {r.total} ); } function Stat({ label, value }: { label: string; value: number }) { return ( {value} {label} ); }
{campaign.description}
No player characters yet.