Files
study-bible-project/src/components/CoTeachers.jsx
T
nmemmertandClaude Sonnet 5.5 0f1a181d87 Rename the Export stage to Finish
It now holds files, sharing, handouts, slides, importing notes, schedule,
co-teachers and earlier versions, not just exports.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 13:17:08 -04:00

86 lines
5.1 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.
// Finish → Co-teachers: let other accounts edit this study with you. You make a single-use invite link;
// whoever opens it (signed in) can edit the study, and you can remove them any time. Edits from both of you
// sync through the account; if you both change the same study, "Combine both" keeps everything.
import { useCallback, useEffect, useState } from 'react';
import { createEditInvite, getCollaborators, removeCollaborator } from '../syncService.js';
const btn = 'rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm hover:bg-black/5 disabled:opacity-50';
export default function CoTeachers({ projectId, sharedBy, onLeave }) {
const [state, setState] = useState({ status: 'loading', collaborators: [], pendingInvites: 0 });
const [invite, setInvite] = useState(null); // { url, expiresAt }
const [busy, setBusy] = useState(false);
const [copied, setCopied] = useState(false);
const [error, setError] = useState('');
const load = useCallback(async () => {
if (!projectId || sharedBy !== undefined) return;
const r = await getCollaborators(projectId);
setState(r.ok ? { status: 'ready', ...r.data } : { status: 'unavailable', collaborators: [], pendingInvites: 0 });
}, [projectId, sharedBy]);
useEffect(() => { setInvite(null); load(); }, [load]);
// Someone else's study that you were invited to edit
if (sharedBy !== undefined) {
return (
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Co-teachers">
<h2 className="text-sm font-medium">Shared with you</h2>
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">{sharedBy || 'Its owner'} invited you to edit this study. Your changes sync to them, and theirs to you.</p>
<button type="button" onClick={onLeave} className={btn}>Leave this study</button>
</section>
);
}
const makeInvite = async () => {
setBusy(true); setError('');
const r = await createEditInvite(projectId);
setBusy(false);
if (!r.ok) { setError(r.error ?? 'Could not make the link.'); return; }
setInvite({ url: `${window.location.origin}${window.location.pathname}?join=${r.data.token}`, expiresAt: r.data.expiresAt });
load();
};
const copy = async () => {
try { await navigator.clipboard.writeText(invite.url); setCopied(true); window.setTimeout(() => setCopied(false), 1800); } catch { setError('Couldn’t copy. Select the link and copy it.'); }
};
const remove = async (userId, email) => {
if (!window.confirm(`Remove ${email}? They will no longer be able to edit this study.`)) return;
const r = await removeCollaborator(projectId, userId);
if (r.ok) load(); else setError(r.error ?? 'Could not remove them.');
};
return (
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Co-teachers">
<h2 className="text-sm font-medium">Co-teachers</h2>
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">Let someone else edit this study with you. They need an account here. Each invite link works once and lasts 7 days.</p>
{state.status === 'unavailable' && <p className="text-xs text-[var(--r-sub)]">Invites need your account and a connection, and the study has to have synced once. Try again in a moment.</p>}
{state.status === 'ready' && (
<>
{state.collaborators.length > 0 && (
<ul className="mb-3 divide-y divide-[var(--r-line)] text-sm">
{state.collaborators.map((c) => (
<li key={c.userId} className="flex items-center justify-between gap-3 py-2">
<span>{c.email}</span>
<button type="button" onClick={() => remove(c.userId, c.email)} className="text-xs text-[var(--r-danger,#b42318)] hover:underline">Remove</button>
</li>
))}
</ul>
)}
{invite ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<input readOnly aria-label="Invite link" value={invite.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={copy} 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>
<p className="text-[11px] text-[var(--r-sub)]">Send it to one person. It stops working once they use it, or on {new Date(invite.expiresAt).toLocaleDateString()}.</p>
</div>
) : (
<button type="button" onClick={makeInvite} disabled={busy} className={btn}>{busy ? 'Making a link…' : 'Invite a co-teacher'}</button>
)}
{state.pendingInvites > 0 && !invite && <p className="mt-2 text-[11px] text-[var(--r-sub)]">{state.pendingInvites} invite{state.pendingInvites === 1 ? '' : 's'} waiting to be used.</p>}
</>
)}
{error && <p role="alert" className="mt-2 text-xs text-[var(--r-danger,#b42318)]">{error}</p>}
</section>
);
}