Files
study-bible-project/src/components/ReadingProgress.jsx
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

61 lines
3.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// "Your reading": how much of the Bible you have read (every chapter you spent time on in the Reader, plus the
// ones you finished in reading plans), a tile for each book that fills as you read it, and a calendar of the
// days you read in the last six months.
import { useMemo } from 'react';
import { getPlanSchedule } from '../utils/readingPlans.js';
import { heatmapWeeks, planChapterKeys, progressSummary } from '../utils/readingHistory.js';
const pct = (n) => `${Math.round(n * 100)}%`;
export default function ReadingProgress({ history, planProgress = [], activity = [], onOpenBook }) {
const summary = useMemo(() => progressSummary(history, planChapterKeys(planProgress, getPlanSchedule)), [history, planProgress]);
const days = useMemo(() => new Set([...(history.days ?? []), ...activity]), [history.days, activity]);
const weeks = useMemo(() => heatmapWeeks(days), [days]);
const daysInView = weeks.flat().filter((d) => d.on).length;
const group = (list) => (
<div className="grid grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] gap-1.5">
{list.map((b) => (
<button key={b.abbrev} type="button" onClick={() => onOpenBook?.(b.abbrev)} title={`${b.name}: ${b.read} of ${b.total} chapters read`}
aria-label={`${b.name}, ${b.read} of ${b.total} chapters read`}
className="relative overflow-hidden rounded-lg border border-[var(--r-line)] px-2 py-1.5 text-left text-xs hover:border-[var(--r-accent)]">
<span aria-hidden="true" className="absolute inset-y-0 left-0 bg-[var(--r-accent)] opacity-25" style={{ width: pct(b.pct) }} />
<span className="relative block truncate font-medium">{b.name}</span>
<span className="relative block text-[10px] text-[var(--r-sub)]">{b.read}/{b.total}</span>
</button>
))}
</div>
);
return (
<section aria-label="Your reading" className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h2 className="text-sm font-medium">Your reading</h2>
<p className="text-xs text-[var(--r-sub)]">
<span className="text-base font-medium text-[var(--r-text)]">{pct(summary.pct)}</span> of the Bible · {summary.read} of {summary.total} chapters
</p>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-[var(--r-line)]" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(summary.pct * 100)} aria-label="Bible read">
<div className="h-full bg-[var(--r-accent)]" style={{ width: pct(summary.pct) }} />
</div>
<p className="mt-1.5 text-[11px] text-[var(--r-sub)]">Old Testament {summary.ot.read}/{summary.ot.total} · New Testament {summary.nt.read}/{summary.nt.total}. A chapter counts once you’ve had it open for a bit in the Reader, or finished it in a plan.</p>
<h3 className="mb-1.5 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Old Testament</h3>
{group(summary.books.filter((b) => b.testament === 'ot'))}
<h3 className="mb-1.5 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">New Testament</h3>
{group(summary.books.filter((b) => b.testament === 'nt'))}
<h3 className="mb-1.5 mt-4 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Days you read · {daysInView} in the last 6 months</h3>
<div className="flex gap-[3px] overflow-x-auto pb-1" role="img" aria-label={`${daysInView} days read in the last six months`}>
{weeks.map((week) => (
<div key={week[0].date} className="flex flex-col gap-[3px]">
{week.map((d) => (
<span key={d.date} title={d.future ? '' : `${d.date}${d.on ? ': read' : ''}`}
className={`h-3 w-3 rounded-[3px] ${d.future ? 'opacity-0' : d.on ? 'bg-[var(--r-accent)]' : 'bg-[var(--r-line)]'}`} />
))}
</div>
))}
</div>
</section>
);
}