import { useMemo, useState } from 'react'; import type { Campaign, Note } from '@/lib/schemas'; import { notesRepo } from '@/lib/db/repositories'; import { useDebouncedCallback } from '@/lib/useDebouncedCallback'; import { extractWikiLinks, splitWikiLinks } from '@/lib/wikilinks'; import { useNotes } from './hooks'; import { Page, PageHeader, EmptyState, RequireCampaign } from '@/components/ui/Page'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { cn } from '@/lib/cn'; export function NotesPage() { return {(c) => }; } function Notes({ campaign }: { campaign: Campaign }) { const notes = useNotes(campaign.id); const [selectedId, setSelectedId] = useState(null); const [query, setQuery] = useState(''); const selected = notes.find((n) => n.id === selectedId) ?? null; const filtered = useMemo(() => { const q = query.trim().toLowerCase(); const list = q ? notes.filter((n) => n.title.toLowerCase().includes(q) || n.body.toLowerCase().includes(q) || n.tags.some((t) => t.toLowerCase().includes(q))) : notes; return [...list].sort((a, b) => a.title.localeCompare(b.title)); }, [notes, query]); const createNote = async (title = 'Untitled') => { const note = await notesRepo.create(campaign.id, title); setSelectedId(note.id); }; const openByTitle = async (title: string) => { const existing = notes.find((n) => n.title.toLowerCase() === title.toLowerCase()); if (existing) setSelectedId(existing.id); else await createNote(title); }; return ( createNote()}>+ New note} /> {notes.length === 0 ? ( createNote()}>+ New note} /> ) : ( setQuery(e.target.value)} placeholder="Search notes…" aria-label="Search notes" /> {filtered.map((n) => ( setSelectedId(n.id)} className={cn( 'w-full truncate rounded-md border px-3 py-2 text-left text-sm', selected?.id === n.id ? 'border-accent bg-elevated' : 'border-line bg-panel hover:border-accent/40', )} > {n.title} ))} {selected ? ( ) : ( Select a note to edit. )} )} ); } function NoteEditor({ note, allNotes, onOpenTitle, onSelect, }: { note: Note; allNotes: Note[]; onOpenTitle: (title: string) => void; onSelect: (id: string | null) => void; }) { const [title, setTitle] = useState(note.title); const [body, setBody] = useState(note.body); const [tags, setTags] = useState(note.tags.join(', ')); // Save the full snapshot so editing title + body quickly doesn't drop an edit. const save = useDebouncedCallback((next: { title: string; body: string; tags: string }) => { void notesRepo.update(note.id, { title: next.title, body: next.body, tags: next.tags.split(',').map((t) => t.trim()).filter(Boolean), }); }, 350); const persist = (over: Partial<{ title: string; body: string; tags: string }>) => save({ title, body, tags, ...over }); const backlinks = allNotes.filter( (n) => n.id !== note.id && extractWikiLinks(n.body).some((t) => t.toLowerCase() === note.title.toLowerCase()), ); const titlesLower = new Set(allNotes.map((n) => n.title.toLowerCase())); return ( { setTitle(e.target.value); persist({ title: e.target.value }); }} /> { await notesRepo.remove(note.id); onSelect(null); }} > Delete Tags (comma-separated) { setTags(e.target.value); persist({ tags: e.target.value }); }} placeholder="lore, barovia, npc" /> { setBody(e.target.value); persist({ body: e.target.value }); }} placeholder="Write here… link other notes with [[Note Title]]." className="min-h-48 w-full resize-y rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60" /> {/* Rendered preview with clickable links */} Preview {splitWikiLinks(body).map((part, i) => part.link ? ( onOpenTitle(part.link!)} className={cn('underline', titlesLower.has(part.link.toLowerCase()) ? 'text-accent' : 'text-warning')} title={titlesLower.has(part.link.toLowerCase()) ? 'Open note' : 'Create this note'} > {part.text} ) : ( {part.text} ), )} {backlinks.length > 0 && ( Linked from {backlinks.map((n) => ( onSelect(n.id)} className="rounded-md border border-line bg-surface px-2 py-1 text-xs text-ink hover:border-accent/40"> {n.title} ))} )} ); }
{splitWikiLinks(body).map((part, i) => part.link ? ( onOpenTitle(part.link!)} className={cn('underline', titlesLower.has(part.link.toLowerCase()) ? 'text-accent' : 'text-warning')} title={titlesLower.has(part.link.toLowerCase()) ? 'Open note' : 'Create this note'} > {part.text} ) : ( {part.text} ), )}