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:
nmemmertandClaude Opus 5.5 committed 2026-09-22 14:34:13 -04:00
1 parent 00932dee4a
commit ade7f1af6f
20 files changed
+1314 -1252

No files matched your search

+144
View File
@@ -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(', ');
}