Files
study-bible-project/src/utils/projectStorage.js
T
nmemmertandClaude Sonnet 5.5 54cb1c217d
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
- Admin: sign-up Open / Invite only / Closed with single-use invite links
- Present mode for scripts (auto-scroll) and outlines (step through, pace vs planned minutes)
- Series: group studies on the dashboard, filter by series, calendar file for a study's sessions
- Lesson handout: printable page per session (passage, outline, questions, writing lines)
- Save a chunk's notes as your own study template (synced)
- Voice dictation for notes and scripts (browser speech recognition)
- Reading progress: map of the 66 books and a calendar of days read, synced and merged across devices
- Export highlights and bookmarks as Markdown or Word, grouped by named colour
- Verse of the day push to the ntfy topic (needs IQBIBLE_API_KEY)
- Sharing: co-teacher editing via single-use invite links; read-only links can expire and take a passcode

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-06 10:26:43 -04:00

146 lines
4.2 KiB
JavaScript

// 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),
series: (updated.series ?? '').trim(),
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(', ');
}