Outline: share as a read-only link

- Share on the Outline page opens a sheet: create / copy / turn off a
  read-only link, share the link with the system share sheet, or send the
  outline as text
- The link has its own token (separate from the whole-study share link) and
  the public endpoint returns only the outlines, rebuilt field by field:
  no observations, interpretations, applications, scripts or word notes
- Opening the link shows the outline(s) with no sign-in, a switcher between
  the whole-study and per-chunk outlines, and Print / PDF
- New outline_share_token column (added automatically on start)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:02:40 -04:00
1 parent 2ca79730f1
commit d7a1601f80
11 files changed
+444 -10

No files matched your search

+5 -7
View File
@@ -10,7 +10,8 @@ import { formatChunkReference } from '../utils/bibleUtils.jsx';
import { buildChapterSummary } from '../utils/projectStorage.js';
import { compactPassages } from '../utils/passageSummary.js';
import { attachableFromChunk, fetchPassageText } from '../utils/passageText.js';
import { downloadOutlineDocx, downloadOutlineMarkdown, printOutline, shareOutline } from '../utils/outlineExport.js';
import ShareOutlineSheet from '../components/ShareOutlineSheet.jsx';
import { downloadOutlineDocx, downloadOutlineMarkdown, printOutline } from '../utils/outlineExport.js';
import {
KINDS, STUDY, addAfter, addChild, addRoot, chunkOf, countNodes, flatten, getNode, indent, insertAt, kindLabel, locate,
moveNode, newNode, outdent, outlineFor, outlineToText, removeNode, seedFromChunks, seedFromVerses, timeSummary, updateNode, withOutline,
@@ -44,6 +45,7 @@ export default function OutlinePage() {
const rows = flatten(tree);
const [pendingFocus, setPendingFocus] = useState(null);
const [metaOpen, setMetaOpen] = useState(() => new Set()); // points whose detail fields are showing
const [shareOpen, setShareOpen] = useState(false);
const [lookup, setLookup] = useState({ id: null, busy: false, error: '' });
// The one way every change is made, always from the latest outline (typing is fast).
@@ -107,11 +109,6 @@ export default function OutlinePage() {
const copy = async () => {
try { await navigator.clipboard.writeText(outlineToText(tree)); toast('Outline copied'); } catch { toast("Couldn't copy. Select the text and copy it instead.", { tone: 'error' }); }
};
const share = async () => {
const result = await shareOutline(outline);
if (result === 'copied') toast('Outline copied. Paste it into a message or email.');
else if (result === 'failed') toast("Couldn't share or copy the outline.", { tone: 'error' });
};
const print = () => { if (!printOutline(outline)) toast('Allow pop-ups for this site to print or save as PDF.', { tone: 'error' }); };
const word = async () => { try { await downloadOutlineDocx(outline); } catch { toast("Couldn't make the Word file.", { tone: 'error' }); } };
@@ -160,7 +157,7 @@ export default function OutlinePage() {
{seedLabel}{addable > 0 ? ` (${addable})` : ''}
</button>
<span className="mx-1 hidden h-6 w-px bg-[var(--r-line)] sm:block" aria-hidden="true" />
<button type="button" onClick={share} disabled={tree.length === 0} className={btn}>Share</button>
<button type="button" onClick={() => setShareOpen(true)} disabled={tree.length === 0} className={btn}>Share</button>
<button type="button" onClick={print} disabled={tree.length === 0} className={btn}>Print / PDF</button>
<details className="relative">
<summary className={`${btn} cursor-pointer list-none ${tree.length === 0 ? 'pointer-events-none opacity-50' : ''}`}>More ▾</summary>
@@ -330,6 +327,7 @@ export default function OutlinePage() {
</p>
)}
</main>
<ShareOutlineSheet open={shareOpen} onClose={() => setShareOpen(false)} projectId={project.id} outline={outline} />
</StudioShell>
);
}