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>
86 lines
5.1 KiB
React
86 lines
5.1 KiB
React
// 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>
|
||
);
|
||
}
|