Files
ttrpg_manager/src/features/world/CalendarPage.tsx
T
NilsBriggen dd694477b2 Polish sprint 1/2: restore crash fix, pf2e parity, full glyph purge
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>
2026-06-09 11:29:36 +02:00

67 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}