dd694477b2
Crash fix: - restoreBackup (file + cloud pull) now validates each row through its Zod schema, backfilling new fields — an older backup no longer lands characters missing feats/concentration/etc and crashing the sheet. + test. pf2e parity (closing feature gaps vs 5e): - +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist, Magus, Psychic, Summoner, Thaumaturge) with data + class tips. - the wizard now enriches pf2e classes with their caster ability, so pf2e spellcasters get the Spells step + "Caster" tag like 5e. - editable Perception rank and spellcasting proficiency on the pf2e sheet (fixes wrong initiative / spell DC at higher levels); rank-10 spell slots. - pf2e monster resistances/weaknesses/immunities now apply in combat (flat amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests. Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across Modal (every dialog), the sheet sections, dice, settings, player panels, world pages, map editor, roll tray, and session UI. Gate: 293 unit + e2e green; tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
67 lines
3.2 KiB
TypeScript
67 lines
3.2 KiB
TypeScript
import { useState } from 'react';
|
||
import { X } from 'lucide-react';
|
||
import type { Calendar, Campaign } from '@/lib/schemas';
|
||
import { calendarRepo } from '@/lib/db/repositories';
|
||
import { newId } from '@/lib/ids';
|
||
import { useCalendar } from './hooks';
|
||
import { Page, PageHeader, RequireCampaign } from '@/components/ui/Page';
|
||
import { Button } from '@/components/ui/Button';
|
||
import { Input } from '@/components/ui/Input';
|
||
|
||
export function CalendarPage() {
|
||
return <RequireCampaign>{(c) => <CalendarView campaign={c} />}</RequireCampaign>;
|
||
}
|
||
|
||
function CalendarView({ campaign }: { campaign: Campaign }) {
|
||
const stored = useCalendar(campaign.id);
|
||
const [eventTitle, setEventTitle] = useState('');
|
||
|
||
// The row is created on first save; until then we work with an in-memory default.
|
||
const calendar: Calendar = stored ?? { campaignId: campaign.id, currentDay: 0, events: [] };
|
||
|
||
const save = (next: Calendar) => void calendarRepo.save(next);
|
||
const advance = (delta: number) => save({ ...calendar, currentDay: calendar.currentDay + delta });
|
||
const addEvent = () => {
|
||
if (eventTitle.trim() === '') return;
|
||
save({ ...calendar, events: [...calendar.events, { id: newId(), day: calendar.currentDay, title: eventTitle.trim() }] });
|
||
setEventTitle('');
|
||
};
|
||
|
||
const upcoming = [...calendar.events].sort((a, b) => a.day - b.day);
|
||
|
||
return (
|
||
<Page>
|
||
<PageHeader title="Calendar" subtitle={`${campaign.name} · in-world timeline`} />
|
||
|
||
<div className="mb-6 flex items-center gap-3 rounded-lg border border-line bg-panel p-4">
|
||
<Button variant="secondary" onClick={() => advance(-1)} aria-label="Previous day">−1 day</Button>
|
||
<div className="text-center">
|
||
<div className="text-xs uppercase tracking-wide text-muted">Day</div>
|
||
<div className="font-display text-3xl font-bold text-accent">{calendar.currentDay}</div>
|
||
</div>
|
||
<Button variant="secondary" onClick={() => advance(1)} aria-label="Next day">+1 day</Button>
|
||
<Button variant="ghost" onClick={() => advance(7)}>+1 week</Button>
|
||
</div>
|
||
|
||
<div className="mb-3 flex gap-2">
|
||
<Input value={eventTitle} onChange={(e) => setEventTitle(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addEvent()} placeholder={`Add event on day ${calendar.currentDay}…`} aria-label="Event title" />
|
||
<Button variant="primary" onClick={addEvent}>Add event</Button>
|
||
</div>
|
||
|
||
{upcoming.length === 0 ? (
|
||
<p className="text-sm text-muted">No events yet.</p>
|
||
) : (
|
||
<ul className="space-y-1">
|
||
{upcoming.map((ev) => (
|
||
<li key={ev.id} className="flex items-center gap-3 rounded-md border border-line bg-panel px-3 py-2 text-sm">
|
||
<span className={'w-16 shrink-0 font-mono ' + (ev.day === calendar.currentDay ? 'text-accent' : 'text-muted')}>Day {ev.day}</span>
|
||
<span className="flex-1 text-ink">{ev.title}</span>
|
||
<button className="text-muted hover:text-danger" onClick={() => save({ ...calendar, events: calendar.events.filter((e) => e.id !== ev.id) })} aria-label={`Remove ${ev.title}`}><X size={13} aria-hidden /></button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</Page>
|
||
);
|
||
}
|