UX safety: confirm destructive deletes, scope dice clear, back up sessionLog, add 404
- New themed useConfirm() hook (no native dialogs). World deletes (notes, quests, homebrew, maps) now confirm before removing. - Dice 'Clear history' is disabled with no active campaign (it used to wipe EVERY campaign's rolls) and now confirms; only ever clears the active campaign. - sessionLog is included in backup/restore and the campaign cascade-delete — it was silently dropped on backup and orphaned on campaign delete. - Unknown URLs render a themed 'Page not found' with a link home instead of a blank shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import { useEncounters } from '@/features/combat/hooks';
|
||||
import { Page, PageHeader, EmptyState, RequireCampaign } from '@/components/ui/Page';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input, Select } from '@/components/ui/Input';
|
||||
import { useConfirm } from '@/components/ui/useConfirm';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const STATUSES: Quest['status'][] = ['active', 'on-hold', 'completed', 'failed'];
|
||||
@@ -89,6 +90,7 @@ function Quests({ campaign }: { campaign: Campaign }) {
|
||||
function QuestCard({ quest }: { quest: Quest }) {
|
||||
const [q, setQ] = useState(quest);
|
||||
const [newObj, setNewObj] = useState('');
|
||||
const { confirm, confirmElement } = useConfirm();
|
||||
const save = useDebouncedCallback((next: Quest) => void questsRepo.update(next.id, {
|
||||
title: next.title, status: next.status, description: next.description, reward: next.reward, objectives: next.objectives,
|
||||
}), 350);
|
||||
@@ -110,8 +112,9 @@ function QuestCard({ quest }: { quest: Quest }) {
|
||||
<Select className={cn('w-auto py-1 text-xs font-medium', STATUS_COLOR[q.status])} value={q.status} onChange={(e) => update({ status: e.target.value as Quest['status'] })} aria-label="Quest status">
|
||||
{STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</Select>
|
||||
<Button size="icon" variant="ghost" className="text-danger" onClick={() => questsRepo.remove(quest.id)} aria-label={`Delete ${q.title}`}><X size={14} aria-hidden /></Button>
|
||||
<Button size="icon" variant="ghost" className="text-danger" onClick={async () => { if (await confirm({ title: 'Delete quest?', message: <>Delete <strong className="text-ink">{q.title || 'this quest'}</strong>? This can’t be undone.</> })) void questsRepo.remove(quest.id); }} aria-label={`Delete ${q.title}`}><X size={14} aria-hidden /></Button>
|
||||
</div>
|
||||
{confirmElement}
|
||||
|
||||
<textarea
|
||||
value={q.description}
|
||||
|
||||
Reference in New Issue
Block a user