Files
ttrpg_manager/src/features/world/CalendarPage.tsx
T
NilsBriggen ed3d967526 Phase 5: campaign management & worldbuilding
- New entities (note/npc/quest/calendar): schemas, Dexie v4 tables, repos, hooks;
  campaign cascade-delete now covers them all
- Dashboard hub: campaign stats, navigation cards, party overview, recent rolls;
  opening a campaign lands here
- Notes/Wiki: tags, [[wiki-links]] with click-to-open/create, backlinks panel,
  full-text search, autosave
- NPC manager (role/location/faction/status) and Quest tracker (status +
  objectives checklist + reward)
- In-world Calendar: integer day counter + events (no real Date -> no timezone bug)
- Fix: calendarRepo.get is read-only (creating-on-read crashed inside liveQuery)

8 new unit tests (wikilinks + extended cascade), worldbuilding e2e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:44:47 +02:00

66 lines
3.1 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 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}`}></button>
</li>
))}
</ul>
)}
</Page>
);
}