Teaching: hide-notes-until share, session length check, slides export
- A study share link can hide the owner's notes until a chosen time: the server sends only passages and discussion questions until then, with a banner in the shared view (new share_reveal column, migrated on start). - Outline: a session length with "N of M min planned, X min over" and a marker on the point where the plan runs past it. - Export > Slides: printable 16:9 slides (title, passage, outline points with their verse, discussion questions) for one session or all; light or dark. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
67645cf1df
commit
2c7c7145c4
14 files changed
+303
-33
No files matched your search
@@ -1,6 +1,7 @@
|
||||
// The Export stage: take the finished study out as a file, or share it read-only.
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { useShareLink } from '../hooks/useShareLink.js';
|
||||
import SlidesExport from '../components/SlidesExport.jsx';
|
||||
import ImportNotes from '../components/ImportNotes.jsx';
|
||||
import HandoutExport from '../components/HandoutExport.jsx';
|
||||
import SessionSchedule from '../components/SessionSchedule.jsx';
|
||||
@@ -53,7 +54,7 @@ export default function ExportPage() {
|
||||
className="min-w-0 flex-1 truncate rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-xs text-[var(--r-sub)]" />
|
||||
<button type="button" onClick={share.copy} className="shrink-0 rounded-lg bg-[var(--r-accent)] px-4 py-2 text-xs font-medium text-[var(--r-on)]">{share.shareCopied ? 'Copied!' : 'Copy'}</button>
|
||||
</div>
|
||||
<LinkRules expiresAt={share.expiresAt} hasPasscode={share.hasPasscode} busy={share.shareBusy} onSave={share.enable} />
|
||||
<LinkRules expiresAt={share.expiresAt} hasPasscode={share.hasPasscode} busy={share.shareBusy} onSave={share.enable} showReveal revealAt={share.revealAt} />
|
||||
{share.shareError && <p role="alert" className="text-xs text-[var(--r-danger,#b42318)]">{share.shareError}</p>}
|
||||
<button type="button" onClick={share.revoke} disabled={share.shareBusy} className="text-xs text-[var(--r-danger,#b42318)] hover:underline">{share.shareBusy ? 'Revoking…' : 'Revoke link'}</button>
|
||||
</div>
|
||||
@@ -64,6 +65,8 @@ export default function ExportPage() {
|
||||
|
||||
<HandoutExport project={project} />
|
||||
|
||||
<SlidesExport project={project} />
|
||||
|
||||
<ImportNotes />
|
||||
|
||||
<SessionSchedule project={project} />
|
||||
|
||||
@@ -11,12 +11,13 @@ import { buildChapterSummary } from '../utils/projectStorage.js';
|
||||
import { compactPassages } from '../utils/passageSummary.js';
|
||||
import { attachableFromChunk, fetchPassageText } from '../utils/passageText.js';
|
||||
import PresentMode from '../components/PresentMode.jsx';
|
||||
import { firstOverPoint, outlineSteps, planStatus } from '../utils/present.js';
|
||||
import IqOutlineAttach from '../components/iq/IqOutlineAttach.jsx';
|
||||
import ShareOutlineSheet from '../components/ShareOutlineSheet.jsx';
|
||||
import { downloadOutlineDocx, downloadOutlineMarkdown, printOutline } from '../utils/outlineExport.js';
|
||||
import {
|
||||
KINDS, STUDY, buildChunkFromNotes, buildFromNotes, notesBuildCount, addAfter, addChild, addRoot, chunkOf, countNodes, flatten, getNode, indent, insertAt, kindLabel, locate,
|
||||
moveNode, newNode, outdent, outlineFor, outlineToText, removeNode, seedFromChunks, seedFromVerses, timeSummary, updateNode, withOutline,
|
||||
moveNode, newNode, outdent, outlineFor, outlineToText, removeNode, seedFromChunks, seedFromVerses, timeSummary, totalMinutes, updateNode, withOutline,
|
||||
} from '../utils/outline.js';
|
||||
|
||||
const iconBtn = 'flex h-7 w-7 items-center justify-center rounded-md text-sm text-[var(--r-sub)] hover:bg-black/5 hover:text-[var(--r-text)] disabled:opacity-30 disabled:hover:bg-transparent';
|
||||
@@ -45,6 +46,11 @@ export default function OutlinePage() {
|
||||
const scopeChunk = scope === STUDY ? null : chunkOf(project, scope);
|
||||
const tree = outlineFor(project, scope);
|
||||
const rows = flatten(tree);
|
||||
// The time you have for a session, and how the planned minutes measure up to it
|
||||
const sessionMinutes = Number(project?.sessionMinutes) || 0;
|
||||
const plan = planStatus(totalMinutes(tree), sessionMinutes);
|
||||
const overPoint = firstOverPoint(outlineSteps(tree), sessionMinutes);
|
||||
const setSessionMinutes = (value) => updateProject((current) => ({ ...current, sessionMinutes: value === '' ? null : Math.max(0, Math.min(600, Math.round(Number(value)))) }));
|
||||
const [pendingFocus, setPendingFocus] = useState(null);
|
||||
const [metaOpen, setMetaOpen] = useState(() => new Set()); // points whose detail fields are showing
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
@@ -160,6 +166,11 @@ export default function OutlinePage() {
|
||||
</select>
|
||||
</label>
|
||||
{timeSummary(tree) && <span className="text-sm text-[var(--r-sub)]">{timeSummary(tree)}</span>}
|
||||
<label className="flex items-center gap-1.5 text-sm text-[var(--r-sub)]">Session length
|
||||
<input type="number" min="0" max="600" inputMode="numeric" aria-label="Session length in minutes" value={project?.sessionMinutes ?? ''} onChange={(e) => setSessionMinutes(e.target.value)}
|
||||
className="w-16 rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2 py-1 text-sm text-[var(--r-text)] focus:outline-none" /> min
|
||||
</label>
|
||||
{plan.state !== 'unset' && <span role="status" className={`text-sm ${plan.state === 'over' ? 'font-medium text-[var(--r-danger,#b42318)]' : 'text-[var(--r-sub)]'}`}>{plan.text}</span>}
|
||||
{scopeChunk && <button type="button" onClick={() => openChunk(scope)} className="text-sm text-[var(--r-accent)] hover:underline">Open this chunk in Study</button>}
|
||||
</div>
|
||||
|
||||
@@ -229,10 +240,11 @@ export default function OutlinePage() {
|
||||
/>
|
||||
|
||||
{/* What the point carries, at a glance */}
|
||||
{(node.kind || mins || node.ref || linked) && (
|
||||
{(node.kind || mins || node.ref || linked || overPoint?.id === node.id) && (
|
||||
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 px-2 text-xs text-[var(--r-sub)]">
|
||||
{node.kind && <span className={`${chip} border-[var(--r-accent)] text-[var(--r-accent)]`}>{kindLabel(node.kind)}</span>}
|
||||
{mins && <span>{node.minutes} min</span>}
|
||||
{overPoint?.id === node.id && <span className="font-medium text-[var(--r-danger,#b42318)]">Runs past {sessionMinutes} min here</span>}
|
||||
{node.ref && <span>{node.ref}</span>}
|
||||
{linked && node.chunkId !== scope && <button type="button" onClick={() => openChunk(node.chunkId)} className="text-[var(--r-accent)] hover:underline">Open chunk in Study</button>}
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user