Speed up the app, run production build, remove chunk ink, start App.jsx split
Performance: - Lazy-load pages, docx, and mammoth (initial JS 1,298 KB -> ~290 KB) - Gzip responses and cache hashed assets for a year - Memoize DrawCanvas strokes so ink isn't redrawn on every render - Precompute whole-Bible search index and defer the query while typing - Reload once if a lazy chunk is missing after a deploy Deployment: - setup.sh builds and runs one production server on APP_PORT (default 5173); --dev keeps the old Vite dev setup - Session cookie uses secure: 'auto' so login works behind any proxy Chunk editor: - Remove the Draw layout, Ink Notes panel, and ink in HTML export (existing inkStrokes data is left in saved projects) Refactor: - Move module-level helpers out of App.jsx into src/utils/* - Extract Bible reader state and logic into hooks/useBibleReader Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
00932dee4a
commit
ade7f1af6f
20 files changed
+1314
-1252
No files matched your search
@@ -0,0 +1,144 @@
|
||||
// Project persistence in localStorage, plus migrations from older data shapes.
|
||||
import {
|
||||
makeId,
|
||||
indexKey,
|
||||
projectKey,
|
||||
loadProjectIndex,
|
||||
} from './bibleUtils.jsx';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migration helpers (exported for testing)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function migrateChunk(chunk) {
|
||||
if (chunk.observation !== undefined) {
|
||||
return {
|
||||
...chunk,
|
||||
spilloverEndVerse: Number.isInteger(chunk.spilloverEndVerse) ? chunk.spilloverEndVerse : null,
|
||||
generalNotes: chunk.generalNotes ?? '',
|
||||
episodeNumber: chunk.episodeNumber ?? '',
|
||||
episodeTitle: chunk.episodeTitle ?? '',
|
||||
finalScript: chunk.finalScript ?? '',
|
||||
tags: chunk.tags ?? [],
|
||||
}; // already new format
|
||||
}
|
||||
return {
|
||||
...chunk,
|
||||
observation: chunk.notes ?? '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
crossReferences: [],
|
||||
tags: [],
|
||||
spilloverEndVerse: null,
|
||||
generalNotes: '',
|
||||
episodeNumber: '',
|
||||
episodeTitle: '',
|
||||
finalScript: '',
|
||||
notes: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function migrateProject(raw) {
|
||||
if (!raw) return null;
|
||||
// Already new format
|
||||
if (Array.isArray(raw.chapters)) {
|
||||
return {
|
||||
...raw,
|
||||
chapters: raw.chapters.map((ch) => ({
|
||||
...ch,
|
||||
chunks: ch.chunks.map(migrateChunk),
|
||||
})),
|
||||
};
|
||||
}
|
||||
// Old flat format → wrap in chapters array
|
||||
const id = raw.id ?? makeId();
|
||||
return {
|
||||
id,
|
||||
title: raw.title ?? '',
|
||||
translation: raw.translation ?? 'BSB',
|
||||
lastEdited: raw.lastEdited ?? Date.now(),
|
||||
selectedChunkId: raw.selectedChunkId ?? null,
|
||||
chapters: [
|
||||
{
|
||||
book: raw.book ?? '',
|
||||
bookAbbrev: raw.bookAbbrev ?? '',
|
||||
chapter: raw.chapter ?? '1',
|
||||
verses: raw.verses ?? [],
|
||||
chunks: (raw.chunks ?? []).map(migrateChunk),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Storage helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function saveProjectToStorage(project) {
|
||||
// Honour an already-stamped lastEdited (e.g. from autosave) so localStorage
|
||||
// and the server always receive the identical timestamp.
|
||||
const updated = { ...project, lastEdited: project.lastEdited ?? Date.now() };
|
||||
window.localStorage.setItem(projectKey(updated.id), JSON.stringify(updated));
|
||||
const index = loadProjectIndex();
|
||||
const existing = index.findIndex((e) => e.id === updated.id);
|
||||
const tags = Array.from(
|
||||
new Set(
|
||||
(updated.chapters ?? [])
|
||||
.flatMap((ch) => ch.chunks ?? [])
|
||||
.flatMap((chunk) => chunk.tags ?? []),
|
||||
),
|
||||
);
|
||||
const summary = {
|
||||
id: updated.id,
|
||||
title: updated.title,
|
||||
lastEdited: updated.lastEdited,
|
||||
chapterSummary: buildChapterSummary(updated),
|
||||
tags,
|
||||
};
|
||||
if (existing >= 0) {
|
||||
index[existing] = summary;
|
||||
} else {
|
||||
index.push(summary);
|
||||
}
|
||||
window.localStorage.setItem(indexKey(), JSON.stringify(index));
|
||||
return updated; // caller can use this exact object for the server PUT
|
||||
}
|
||||
|
||||
export function deleteProjectFromStorage(id) {
|
||||
window.localStorage.removeItem(projectKey(id));
|
||||
const index = loadProjectIndex().filter((e) => e.id !== id);
|
||||
window.localStorage.setItem(indexKey(), JSON.stringify(index));
|
||||
}
|
||||
|
||||
export function loadProjectById(id) {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(projectKey(id));
|
||||
return raw ? migrateProject(JSON.parse(raw)) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateOldStorageKeys() {
|
||||
const keys = Object.keys(window.localStorage);
|
||||
keys.forEach((key) => {
|
||||
if (!key.startsWith('bible-study-') || key.startsWith('bible-study-index') || key.startsWith('bible-study-project')) return;
|
||||
try {
|
||||
const raw = JSON.parse(window.localStorage.getItem(key));
|
||||
if (!raw || !raw.book) return;
|
||||
const migrated = migrateProject(raw);
|
||||
if (!migrated) return;
|
||||
saveProjectToStorage(migrated);
|
||||
window.localStorage.removeItem(key);
|
||||
} catch {
|
||||
// skip corrupt entries
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function buildChapterSummary(project) {
|
||||
if (!Array.isArray(project.chapters)) return '';
|
||||
return project.chapters
|
||||
.map((ch) => `${ch.book} ${ch.chapter}`)
|
||||
.join(', ');
|
||||
}
|
||||
Reference in new issue
Block a user