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

+66
View File
@@ -0,0 +1,66 @@
// Share an outline: a read-only link anyone can open (no account), or the plain text.
import { useState } from 'react';
import Sheet from './reader/Sheet.jsx';
import { useToast } from './Toasts.jsx';
import { useOutlineShareLink } from '../hooks/useOutlineShareLink.js';
import { shareOutline } from '../utils/outlineExport.js';
const btn = 'rounded-xl border border-[var(--r-line)] px-3.5 py-2 text-sm hover:bg-black/5 disabled:opacity-50';
export default function ShareOutlineSheet({ open, onClose, projectId, outline }) {
const { toast } = useToast();
const link = useOutlineShareLink(projectId, open);
const [copied, setCopied] = useState(false);
const copyLink = async () => {
try { await navigator.clipboard.writeText(link.url); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch { toast('Couldn’t copy. Select the link and copy it.', { tone: 'error' }); }
};
// The phone/computer share sheet, with the link (falls back to copying it)
const shareLink = async () => {
if (navigator.share) {
try { await navigator.share({ title: `${outline.title} — Outline`, url: link.url }); return; } catch (err) { if (err?.name === 'AbortError') return; }
}
copyLink();
};
const shareText = 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' });
};
return (
<Sheet open={open} onClose={onClose} title="Share this outline">
<h3 className="text-sm font-medium">Read-only link</h3>
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">
Anyone with the link can read this study’s outlines (the whole-study one and each chunk’s), and print them. They can’t edit anything, and they can’t see your notes or scripts. No account needed.
</p>
{link.state === 'loading' && <p className="text-sm text-[var(--r-sub)]">Checking…</p>}
{link.state === 'unavailable' && (
<p className="rounded-xl border border-[var(--r-line)] px-3 py-2 text-sm text-[var(--r-sub)]">Links need your account and a connection to the server. You can still share the text below.</p>
)}
{link.state === 'ready' && (link.token ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<input readOnly aria-label="Outline link" value={link.url} onFocus={(e) => e.target.select()}
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={copyLink} className="shrink-0 rounded-lg bg-[var(--r-accent)] px-4 py-2 text-xs font-medium text-[var(--r-on)]">{copied ? 'Copied!' : 'Copy'}</button>
</div>
<div className="flex flex-wrap items-center gap-3">
<button type="button" onClick={shareLink} className={btn}>Share link…</button>
<button type="button" onClick={link.turnOff} disabled={link.busy} className="text-xs text-[var(--r-danger,#b42318)] hover:underline">{link.busy ? 'Turning off…' : 'Turn off link'}</button>
</div>
<p className="text-[11px] text-[var(--r-sub)]">People see the version that last synced to your account, and it updates as you edit. Turning the link off stops it working right away.</p>
</div>
) : (
<button type="button" onClick={link.create} disabled={link.busy} className="rounded-xl bg-[var(--r-accent)] px-4 py-2 text-sm font-medium text-[var(--r-on)] hover:opacity-90 disabled:opacity-50">{link.busy ? 'Creating…' : 'Create read-only link'}</button>
))}
{link.error && <p role="alert" className="mt-2 text-xs text-[var(--r-danger,#b42318)]">{link.error}</p>}
<div className="mt-5 border-t border-[var(--r-line)] pt-4">
<h3 className="text-sm font-medium">Or send the text</h3>
<p className="mb-2 mt-1 text-xs text-[var(--r-sub)]">The outline you’re looking at, as plain text, to paste into a message or email.</p>
<button type="button" onClick={shareText} className={btn}>Share as text</button>
</div>
</Sheet>
);
}