import { useCallback, useEffect, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import { dateTime } from '../../../lib/format.js' import { api } from '../../../api/client.js' // Admin email invites: send an invite at a chosen access level, see recent // invites and their status, revoke pending ones. When email delivery isn't // configured the create response hands back the accept link to copy manually. const ROLES = ['player', 'moderator', 'editor', 'admin'] const ROLE_BADGE = { admin: 'badge-admin', editor: 'badge-editor', moderator: 'badge-moderator', player: 'badge-player' } const STATUS_COLOR = { pending: 'var(--accent)', accepted: '#7fd0a4', revoked: 'var(--muted)' } function CopyLink({ url }) { const [copied, setCopied] = useState(false) async function copy() { try { await navigator.clipboard.writeText(url) setCopied(true) setTimeout(() => setCopied(false), 1800) } catch { /* clipboard blocked — the link is selectable in the box regardless */ } } return (
{ const r = document.createRange(); r.selectNodeContents(e.currentTarget); const s = window.getSelection(); s.removeAllRanges(); s.addRange(r) }} style={{ flex: 1, wordBreak: 'break-all', color: 'var(--head)', background: 'var(--panel-flat)', padding: '8px 10px', borderRadius: 6, border: '1px solid var(--line)', cursor: 'text', fontSize: '0.8rem' }} > {url}
) } function CreateInvite({ onCreated }) { const [email, setEmail] = useState('') const [role, setRole] = useState('player') const [sendEmail, setSendEmail] = useState(true) const [busy, setBusy] = useState(false) const [error, setError] = useState('') const [result, setResult] = useState(null) // fields emailed, acceptUrl, emailError async function submit(e) { e.preventDefault() setError(''); setResult(null) if (!email.trim()) return setError('Enter an email address.') setBusy(true) try { const res = await api.admin.createInvite(email.trim(), role, sendEmail) setResult(res) setEmail('') await onCreated() } catch (err) { setError(err.message || 'Could not create the invite.') } finally { setBusy(false) } } const submitLabel = sendEmail ? 'Create & email' : 'Create link' let resultText if (result?.emailed) { resultText = 'Invitation emailed. You can also share this single-use link:' } else { const emailNote = result?.emailError ? ` (email not sent: ${result.emailError})` : '' resultText = `Invite created${emailNote}. Share this single-use link:` } return (
Invite someone
{error &&

{error}

} {result && (

{resultText}

)}
) } export default function InvitesAdmin() { const [invites, setInvites] = useState(null) const [error, setError] = useState('') const load = useCallback(async () => { setError('') try { setInvites(await api.admin.listInvites()) } catch { setError('Could not load invites.') } }, []) useEffect(() => { load() }, [load]) async function revoke(id) { if (!window.confirm('Revoke this pending invitation?')) return try { await api.admin.revokeInvite(id) await load() } catch { /* surfaced by the row staying; keep it simple */ } } if (error) return return (
{!invites ? ( ) : (
{invites.length === 0 && ( )} {invites.map((iv) => { const status = iv.status === 'pending' && iv.expired ? 'expired' : iv.status return ( ) })}
Email Role Status Expires Created
No invites yet.
{iv.email} {iv.role} {status} {dateTime(iv.expiresAt)} {dateTime(iv.createdAt)} {iv.status === 'pending' && ( )}
)}
) }