Add Insights, Observe-first, compare, handout layouts, richer search, backup nudge
- Commentary and study aids wait for 15 words of your own Observe notes. - Insights page: this week's own work, themes across studies, words you keep studying, what slipped in recalls, open follow-ups. - Recall can record what slipped; session notes (teach it back, who came, group notes, follow-ups); "Pray about this" from Apply. - Compare any two passages side by side. - Handout layouts: standard, questions only, large print, note-taking. - Search understands "in <book>", "last week/month/year" and a year. - Home shows the last backup age and nudges after two weeks. - Phone: the stage bar scrolls instead of clipping Script and Export. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ddc4e1ff6
commit
d8e522dd06
24 files changed
+540
-28
No files matched your search
@@ -14,6 +14,7 @@ import {
|
||||
formatRelativeDate,
|
||||
loadProjectIndex,
|
||||
} from '../utils/bibleUtils.jsx';
|
||||
import { backupAge, lastBackupAt } from '../utils/backup.js';
|
||||
import { loadProjectById } from '../utils/projectStorage.js';
|
||||
import { compactPassages } from '../utils/passageSummary.js';
|
||||
import { getPlan, getPlanSchedule, formatDayReadings, nextUnreadDayIndex, computeStreak } from '../utils/readingPlans.js';
|
||||
@@ -38,6 +39,8 @@ export default function HomePage() {
|
||||
audioState,
|
||||
audioProgress,
|
||||
openImportProject,
|
||||
setCurrentPage,
|
||||
openBackup,
|
||||
openNewProject,
|
||||
pullLatestFromServer,
|
||||
resumeProject,
|
||||
@@ -130,6 +133,7 @@ export default function HomePage() {
|
||||
active="studies"
|
||||
actions={(
|
||||
<>
|
||||
<button type="button" onClick={() => setCurrentPage('insights')} className={linkBtn} title="This week, themes, words, what slipped, follow-ups">Insights</button>
|
||||
<button type="button" onClick={openImportProject} className={linkBtn} title="Set up a whole series from a session list (.docx)">Import Session List</button>
|
||||
<button type="button" onClick={openNewProject} className="rounded-full bg-[var(--r-accent)] px-4 py-1.5 text-sm font-medium text-[var(--r-on)] hover:opacity-90">+ New Project</button>
|
||||
</>
|
||||
@@ -137,6 +141,15 @@ export default function HomePage() {
|
||||
>
|
||||
<main className="mx-auto grid max-w-6xl gap-6 px-4 py-6 lg:grid-cols-[minmax(0,1fr)_21rem]">
|
||||
<section className="min-w-0">
|
||||
{projectIndex.length > 0 && (() => {
|
||||
const age = backupAge(lastBackupAt());
|
||||
return (
|
||||
<p className={`mb-3 flex flex-wrap items-center gap-2 px-1 text-xs ${age.stale ? 'text-[var(--r-danger,#b42318)]' : 'text-[var(--r-sub)]'}`}>
|
||||
{age.text}.
|
||||
<button type="button" onClick={openBackup} className="text-[var(--r-accent)] hover:underline">Back up now</button>
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
<WeeklyRecall />
|
||||
<RecallHistory />
|
||||
<IqDailyCard />
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Insights: your studies seen together — this week's own work, themes (tags), words you keep studying,
|
||||
// what slipped in recalls, and follow-ups still open.
|
||||
import { useMemo } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import PageShell from '../components/study/PageShell.jsx';
|
||||
import { loadProjectById } from '../utils/projectStorage.js';
|
||||
import { followUpList, slippedList, themeIndex, weeklySummary, wordIndex } from '../utils/insights.js';
|
||||
|
||||
const card = 'rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5';
|
||||
const open = 'text-xs text-[var(--r-accent)] hover:underline';
|
||||
|
||||
function Stat({ label, value }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-4 py-3">
|
||||
<p className="text-2xl font-medium tabular-nums">{value}</p>
|
||||
<p className="text-xs text-[var(--r-sub)]">{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { projectIndex, openStudyChunk, goHome } = useApp();
|
||||
const data = useMemo(() => {
|
||||
const projects = projectIndex.map((e) => loadProjectById(e.id)).filter(Boolean);
|
||||
return { week: weeklySummary(projects), themes: themeIndex(projects), words: wordIndex(projects), slipped: slippedList(projects), followUps: followUpList(projects) };
|
||||
}, [projectIndex]);
|
||||
const { week, themes, words, slipped, followUps } = data;
|
||||
const link = (item) => <button type="button" onClick={() => openStudyChunk(item.projectId, item.chunkId)} className={open}>{item.reference} · {item.projectTitle}</button>;
|
||||
|
||||
return (
|
||||
<PageShell title="Insights" active="studies" onBack={goHome} maxWidth="max-w-4xl">
|
||||
<main className="mx-auto max-w-4xl space-y-5 px-4 pb-10">
|
||||
<section aria-label="This week" className={card}>
|
||||
<h2 className="text-sm font-medium">This week</h2>
|
||||
<p className="mb-3 mt-0.5 text-xs text-[var(--r-sub)]">Counted over the chunks you edited in the last 7 days. Pasted text isn’t counted as yours.</p>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<Stat label="Chunks worked on" value={week.chunksTouched} />
|
||||
<Stat label="Words of your own" value={week.ownWords} />
|
||||
<Stat label="Your own words" value={week.ownShare === null ? '–' : `${week.ownShare}%`} />
|
||||
<Stat label="Recalls done" value={week.recalls} />
|
||||
</div>
|
||||
{week.teachBacks > 0 && <p className="mt-2 text-xs text-[var(--r-sub)]">{week.teachBacks} teach-it-back summar{week.teachBacks === 1 ? 'y' : 'ies'} written.</p>}
|
||||
</section>
|
||||
|
||||
<section aria-label="Open follow-ups" className={card}>
|
||||
<h2 className="text-sm font-medium">Follow-ups</h2>
|
||||
{followUps.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">None yet. Add them under “More” in the Study stage.</p> : (
|
||||
<ul className="mt-2 space-y-1.5 text-sm">
|
||||
{followUps.map((f) => (
|
||||
<li key={f.id} className="flex flex-wrap items-baseline gap-x-3">
|
||||
<span className={f.done ? 'text-[var(--r-sub)] line-through' : ''}>{f.text}</span>
|
||||
{link(f)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section aria-label="What slipped" className={card}>
|
||||
<h2 className="text-sm font-medium">What slipped in recalls</h2>
|
||||
{slipped.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Nothing yet. After a weekly recall, note what you missed.</p> : (
|
||||
<ul className="mt-2 space-y-3 text-sm">
|
||||
{slipped.map((r) => (
|
||||
<li key={`${r.chunkId}-${r.at}`}>
|
||||
<p className="text-xs text-[var(--r-sub)]">{new Date(r.at).toLocaleDateString()} · {link(r)}</p>
|
||||
<ul className="ml-4 list-disc">{r.items.map((i) => <li key={i}>{i}</li>)}</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section aria-label="Themes" className={card}>
|
||||
<h2 className="text-sm font-medium">Themes</h2>
|
||||
{themes.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Tag a chunk (under “More”) and it appears here with every other chunk on the same theme.</p> : (
|
||||
<div className="mt-2 space-y-3">
|
||||
{themes.map((t) => (
|
||||
<details key={t.tag} className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2">
|
||||
<summary className="cursor-pointer text-sm"><span className="font-medium">{t.tag}</span> <span className="text-xs text-[var(--r-sub)]">· {t.count} chunk{t.count === 1 ? '' : 's'}</span></summary>
|
||||
<ul className="mt-2 space-y-2">
|
||||
{t.items.map((i) => (
|
||||
<li key={`${i.chunkId}`} className="text-sm">{link(i)}{i.snippet && <p className="font-serif text-[13px] text-[var(--r-sub)]">{i.snippet}</p>}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section aria-label="Words you keep studying" className={card}>
|
||||
<h2 className="text-sm font-medium">Words you keep studying</h2>
|
||||
{words.length === 0 ? <p className="mt-1 text-xs text-[var(--r-sub)]">Words you study in two or more places show up here.</p> : (
|
||||
<ul className="mt-2 space-y-2 text-sm">
|
||||
{words.map((w) => (
|
||||
<li key={w.key}>
|
||||
<span className="font-serif text-base">{w.lexeme}</span> <span className="text-xs text-[var(--r-sub)]">{[w.transliteration, w.strong, w.gloss].filter(Boolean).join(' · ')} · {w.count} places</span>
|
||||
<span className="mt-0.5 flex flex-wrap gap-x-3">{w.places.map((p) => <span key={p.chunkId}>{link(p)}</span>)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -13,10 +13,11 @@ import OutlineRail from '../components/study/OutlineRail.jsx';
|
||||
import ScripturePane from '../components/study/ScripturePane.jsx';
|
||||
import NotesPane from '../components/study/NotesPane.jsx';
|
||||
import ResourcePane, { useResourceTab } from '../components/study/ResourcePane.jsx';
|
||||
import CompareSheet from '../components/study/CompareSheet.jsx';
|
||||
import SuggestWordsSheet from '../components/study/SuggestWordsSheet.jsx';
|
||||
|
||||
export default function StudyPage() {
|
||||
const { project, allChunks, projectIndex, updateProject, updateChunk, setCurrentPage, readerCommentary, setReaderCommentary, saveUserTemplate } = useApp();
|
||||
const { project, allChunks, projectIndex, updateProject, updateChunk, setCurrentPage, readerCommentary, setReaderCommentary, saveUserTemplate, startPrayerFor } = useApp();
|
||||
|
||||
// Short-lived status line (word suggestions, lookups, …)
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
@@ -41,6 +42,7 @@ export default function StudyPage() {
|
||||
|
||||
const [tab, setTab] = useResourceTab();
|
||||
const [openWordId, setOpenWordId] = useState(null);
|
||||
const [comparing, setComparing] = useState(false);
|
||||
useEffect(() => { setOpenWordId(null); }, [selectedChunk?.id]);
|
||||
|
||||
const resources = useStudyResources({
|
||||
@@ -103,6 +105,7 @@ export default function StudyPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-1.5">
|
||||
<button type="button" onClick={() => setComparing(true)} className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5" title="Set another passage beside this one">Compare…</button>
|
||||
<button type="button" onClick={goPrev} disabled={globalIndex <= 0} aria-label="Previous chunk"
|
||||
className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-sm hover:bg-black/5 disabled:opacity-40">‹</button>
|
||||
<button type="button" onClick={goNext} disabled={globalIndex >= allChunks.length - 1} aria-label="Next chunk"
|
||||
@@ -132,7 +135,8 @@ export default function StudyPage() {
|
||||
<section aria-label="Notes" className="flex min-h-[22rem] flex-col overflow-hidden rounded-2xl border border-[var(--r-line)] bg-[var(--r-bg)] lg:min-h-0 lg:flex-[11]">
|
||||
<h2 className="shrink-0 border-b border-[var(--r-line)] px-4 py-2.5 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Your notes</h2>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">
|
||||
<NotesPane chunk={selectedChunk} updateChunk={updateChunk} allTags={allTags} templateId={project?.templateDef ?? project?.templateId} saveTemplate={saveUserTemplate} notify={notify} />
|
||||
<NotesPane chunk={selectedChunk} updateChunk={updateChunk} allTags={allTags} templateId={project?.templateDef ?? project?.templateId} saveTemplate={saveUserTemplate} notify={notify}
|
||||
onPray={() => startPrayerFor(formatChunkReference(project, chapterIndex, selectedChunk, '–'))} />
|
||||
</div>
|
||||
</section>
|
||||
<section aria-label="Resources" className="flex min-h-[24rem] flex-col overflow-hidden rounded-2xl border border-[var(--r-line)] bg-[var(--r-bg)] lg:min-h-0 lg:flex-[10]">
|
||||
@@ -148,6 +152,7 @@ export default function StudyPage() {
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{comparing && selectedChunk && <CompareSheet currentChunkId={selectedChunk.id} onClose={() => setComparing(false)} />}
|
||||
<SuggestWordsSheet wordStudy={wordStudy} />
|
||||
{statusMessage && (
|
||||
<div role="status" className="fixed bottom-6 left-1/2 z-[60] -translate-x-1/2 rounded-full bg-[var(--r-text)] px-4 py-2 text-sm text-[var(--r-bg)] shadow-lg">
|
||||
|
||||
Reference in new issue
Block a user