Skip to content

Commit 18e6824

Browse files
fix: harden IndexedDB open, remove broken in-preview editing (#18)
1 parent e49bfe3 commit 18e6824

8 files changed

Lines changed: 88 additions & 236 deletions

File tree

src/app/new/NewDeckGallery.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,22 @@ export function NewDeckGallery() {
6969
const router = useRouter();
7070

7171
const create = async (preset: TemplatePreset) => {
72-
const deck = await createDeck({
73-
source: preset.seed,
74-
theme: {
75-
styleId: preset.styleId,
76-
paletteId: preset.paletteId,
77-
density: preset.density,
78-
mode: preset.mode,
79-
},
80-
templateName: preset.id === 'blank' ? undefined : preset.name,
81-
});
82-
router.push(`/d/${deck.id}/edit`);
72+
try {
73+
const deck = await createDeck({
74+
source: preset.seed,
75+
theme: {
76+
styleId: preset.styleId,
77+
paletteId: preset.paletteId,
78+
density: preset.density,
79+
mode: preset.mode,
80+
},
81+
templateName: preset.id === 'blank' ? undefined : preset.name,
82+
});
83+
router.push(`/d/${deck.id}/edit`);
84+
} catch (err) {
85+
const message = err instanceof Error ? err.message : 'Could not create deck';
86+
window.alert(`Could not create the deck: ${message}`);
87+
}
8388
};
8489

8590
const totalChoices = TEMPLATE_PRESETS.length + 1;

src/app/templates/TemplatesGallery.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,22 @@ export function TemplatesGallery() {
2424
const router = useRouter();
2525

2626
const applyTemplate = async (preset: TemplatePreset) => {
27-
const deck = await createDeck({
28-
source: preset.seed,
29-
theme: {
30-
styleId: preset.styleId,
31-
paletteId: preset.paletteId,
32-
density: preset.density,
33-
mode: preset.mode,
34-
},
35-
templateName: preset.name,
36-
});
37-
router.push(`/d/${deck.id}/edit`);
27+
try {
28+
const deck = await createDeck({
29+
source: preset.seed,
30+
theme: {
31+
styleId: preset.styleId,
32+
paletteId: preset.paletteId,
33+
density: preset.density,
34+
mode: preset.mode,
35+
},
36+
templateName: preset.name,
37+
});
38+
router.push(`/d/${deck.id}/edit`);
39+
} catch (err) {
40+
const message = err instanceof Error ? err.message : 'Could not create deck';
41+
window.alert(`Could not create the deck: ${message}`);
42+
}
3843
};
3944

4045
return (

src/editor/Editor.tsx

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
77
import { ParseError, parseDeck } from '@/ir/parse';
88
import { planDeck } from '@/ir/plan';
99
import { reorderSlide } from '@/ir/source-edit';
10-
import { replaceHeadingOccurrence, type EditableKind } from '@/ir/text-edit';
1110
import { lintColors } from '@/render/lint';
1211
import { resolveTheme } from '@/render/theme-resolver';
1312
import type { Brand, Deck, Density, Mode, ThemeRef } from '@/ir/schema';
@@ -184,10 +183,6 @@ export function Editor({ deckId }: Props) {
184183
setSelectedSlide(to);
185184
}, []);
186185

187-
const handleHeadingEdit = useCallback((kind: EditableKind, index: number, nextText: string) => {
188-
setSource((s) => replaceHeadingOccurrence(s, kind, index, nextText));
189-
}, []);
190-
191186
const handleInsert = useCallback((snippet: string) => {
192187
insertRef.current?.(snippet);
193188
}, []);
@@ -358,7 +353,6 @@ export function Editor({ deckId }: Props) {
358353
selectedSlide={selectedSlide}
359354
onSelectSlide={handleSelectSlide}
360355
onReorderSlide={handleReorderSlide}
361-
onHeadingEdit={handleHeadingEdit}
362356
/>
363357
) : (
364358
<div className="editor__error">
@@ -471,13 +465,11 @@ function PreviewStage({
471465
selectedSlide,
472466
onSelectSlide,
473467
onReorderSlide,
474-
onHeadingEdit,
475468
}: {
476469
deck: Deck;
477470
selectedSlide: number;
478471
onSelectSlide: (i: number) => void;
479472
onReorderSlide: (from: number, to: number) => void;
480-
onHeadingEdit: (kind: EditableKind, index: number, nextText: string) => void;
481473
}) {
482474
const total = deck.slides.length;
483475
const safeIndex = Math.min(Math.max(selectedSlide, 0), Math.max(total - 1, 0));
@@ -502,88 +494,10 @@ function PreviewStage({
502494
return () => window.removeEventListener('keydown', onKey);
503495
}, [safeIndex, total, onSelectSlide]);
504496

505-
const slideRef = useRef<HTMLDivElement>(null);
506-
507-
const beginEdit = useCallback(
508-
(el: HTMLElement) => {
509-
const tag = el.tagName.toLowerCase();
510-
if (!/^h[1-4]$/.test(tag)) return;
511-
// Compute occurrence index across the FULL deck, not just the visible
512-
// slide. We render slides individually, so we have to look through deck.
513-
const kind = tag as EditableKind;
514-
const targetText = el.textContent ?? '';
515-
let occurrence = 0;
516-
const targetLevel = Number(kind.slice(1));
517-
outer: for (let s = 0; s < deck.slides.length; s++) {
518-
const blocks = deck.slides[s].blocks;
519-
const stack: typeof blocks = [...blocks];
520-
while (stack.length > 0) {
521-
const b = stack.shift()!;
522-
if (b.type === 'heading') {
523-
if (b.level === targetLevel) {
524-
if (s === safeIndex && b.text === targetText) {
525-
break outer;
526-
}
527-
occurrence++;
528-
}
529-
} else if (b.type === 'box') {
530-
stack.unshift(...b.children);
531-
} else if (b.type === 'columns') {
532-
stack.unshift(...b.columns.flat());
533-
} else if (b.type === 'grid' || b.type === 'cell') {
534-
stack.unshift(...b.children);
535-
}
536-
}
537-
}
538-
el.contentEditable = 'true';
539-
el.classList.add('preview-editable');
540-
el.focus();
541-
const range = document.createRange();
542-
range.selectNodeContents(el);
543-
const sel = window.getSelection();
544-
sel?.removeAllRanges();
545-
sel?.addRange(range);
546-
547-
const finish = () => {
548-
el.removeEventListener('blur', finish);
549-
el.removeEventListener('keydown', onKey);
550-
el.contentEditable = 'false';
551-
el.classList.remove('preview-editable');
552-
const next = (el.textContent ?? '').trim();
553-
if (next && next !== targetText) onHeadingEdit(kind, occurrence, next);
554-
};
555-
const onKey = (ke: KeyboardEvent) => {
556-
if (ke.key === 'Enter' && !ke.shiftKey) {
557-
ke.preventDefault();
558-
el.blur();
559-
} else if (ke.key === 'Escape') {
560-
ke.preventDefault();
561-
el.textContent = targetText;
562-
el.blur();
563-
}
564-
};
565-
el.addEventListener('blur', finish);
566-
el.addEventListener('keydown', onKey);
567-
},
568-
[deck, safeIndex, onHeadingEdit],
569-
);
570-
571-
const onDoubleClick = useCallback(
572-
(e: React.MouseEvent<HTMLDivElement>) => {
573-
const t = e.target as HTMLElement;
574-
const heading = t.closest('h1, h2, h3, h4') as HTMLElement | null;
575-
if (heading && slideRef.current?.contains(heading)) {
576-
e.preventDefault();
577-
beginEdit(heading);
578-
}
579-
},
580-
[beginEdit],
581-
);
582-
583497
return (
584498
<div className="stage">
585499
<div className="stage__viewport">
586-
<div className="stage__slide" ref={slideRef} onDoubleClick={onDoubleClick}>
500+
<div className="stage__slide">
587501
<DeckRenderer deck={visibleDeck} />
588502
</div>
589503
</div>

src/editor/editor.css

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,22 +1425,6 @@
14251425
/* Print
14261426
--------------------------------------------------------------------------*/
14271427

1428-
/* ─── In-preview editing ──────────────────────────────────────────────── */
1429-
1430-
.stage__slide h1,
1431-
.stage__slide h2,
1432-
.stage__slide h3,
1433-
.stage__slide h4 {
1434-
cursor: text;
1435-
}
1436-
1437-
.preview-editable {
1438-
outline: 2px solid var(--accent, #6ee7b7);
1439-
outline-offset: 4px;
1440-
border-radius: 4px;
1441-
background: rgba(110, 231, 183, 0.08);
1442-
}
1443-
14441428
/* ─── Export PDF menu ────────────────────────────────────────────────── */
14451429

14461430
.export-pdf {

src/ir/text-edit.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/library/DeckLibrary.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,30 @@ export function DeckLibrary() {
3434
const [query, setQuery] = useState('');
3535
const [sort, setSort] = useState<SortKey>('recent');
3636

37+
const [loadError, setLoadError] = useState<string | null>(null);
38+
3739
useEffect(() => {
38-
listDecks().then(setDecks);
40+
listDecks()
41+
.then((d) => {
42+
setDecks(d);
43+
setLoadError(null);
44+
})
45+
.catch((err: unknown) => {
46+
const message = err instanceof Error ? err.message : 'Could not load decks';
47+
setLoadError(message);
48+
setDecks([]);
49+
});
3950
}, []);
4051

41-
const refresh = async () => setDecks(await listDecks());
52+
const refresh = async () => {
53+
try {
54+
setDecks(await listDecks());
55+
setLoadError(null);
56+
} catch (err) {
57+
const message = err instanceof Error ? err.message : 'Could not load decks';
58+
setLoadError(message);
59+
}
60+
};
4261

4362
const visibleDecks = useMemo(() => {
4463
if (!decks) return null;
@@ -83,7 +102,11 @@ export function DeckLibrary() {
83102
/>
84103

85104
<PageMain className="library__main">
86-
{decks === null ? (
105+
{loadError ? (
106+
<Mono as="div" className="library__loading" role="alert">
107+
{loadError}
108+
</Mono>
109+
) : decks === null ? (
87110
<Mono as="div" className="library__loading">
88111
Loading decks
89112
</Mono>

src/storage/db.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,18 @@ let dbPromise: Promise<IDBDatabase> | null = null;
1212

1313
function openDb(): Promise<IDBDatabase> {
1414
if (dbPromise) return dbPromise;
15-
dbPromise = new Promise((resolve, reject) => {
16-
const req = indexedDB.open(DB_NAME, DB_VERSION);
15+
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
16+
if (typeof indexedDB === 'undefined') {
17+
reject(new Error('IndexedDB is not available in this environment'));
18+
return;
19+
}
20+
let req: IDBOpenDBRequest;
21+
try {
22+
req = indexedDB.open(DB_NAME, DB_VERSION);
23+
} catch (err) {
24+
reject(err as Error);
25+
return;
26+
}
1727
req.onupgradeneeded = () => {
1828
const db = req.result;
1929
if (!db.objectStoreNames.contains(STORE_DECKS)) {
@@ -31,8 +41,23 @@ function openDb(): Promise<IDBDatabase> {
3141
store.createIndex('createdAt', 'createdAt');
3242
}
3343
};
34-
req.onsuccess = () => resolve(req.result);
35-
req.onerror = () => reject(req.error);
44+
req.onsuccess = () => {
45+
const db = req.result;
46+
// Let other tabs upgrade us out by closing our connection on demand.
47+
db.onversionchange = () => db.close();
48+
resolve(db);
49+
};
50+
req.onerror = () => reject(req.error ?? new Error('IndexedDB open failed'));
51+
req.onblocked = () =>
52+
reject(
53+
new Error(
54+
'IndexedDB upgrade blocked by another open tab. Close other tabs of this app and reload.',
55+
),
56+
);
57+
});
58+
// Don't poison future calls if this one rejects.
59+
dbPromise.catch(() => {
60+
dbPromise = null;
3661
});
3762
return dbPromise;
3863
}

0 commit comments

Comments
 (0)