import { useState } from 'react'; import type { Role } from '@reforger-panel/shared'; import { ROLE_LABELS } from '@reforger-panel/shared'; import { useCreateInvite, useDeleteInvite, useInvites } from '../api/hooks.js'; import { formatDateTime, formatRelativeTime } from '../lib/format.js'; import { Button, Card, EmptyState, RoleBadge, Spinner } from './ui.js'; const INVITABLE_ROLES: Role[] = ['server_admin', 'mission_lead', 'viewer']; const INVITE_DURATIONS = [ { label: 'Never expires', value: 'never', hours: null }, { label: '7 days', value: '168', hours: 168 }, { label: '30 days', value: '720', hours: 720 }, ] as const; function inviteLink(code: string): string { return `${window.location.origin}/?invite=${code}`; } function isEffectivelyPermanent(expiresAt: string): boolean { return new Date(expiresAt).getTime() - Date.now() > 20 * 365 * 24 * 60 * 60 * 1000; } export function InvitesCard() { const { data, isLoading } = useInvites(true); const createInvite = useCreateInvite(); const deleteInvite = useDeleteInvite(); const [role, setRole] = useState('mission_lead'); const [duration, setDuration] = useState<(typeof INVITE_DURATIONS)[number]['value']>('never'); const [copied, setCopied] = useState(null); const copy = async (code: string) => { try { await navigator.clipboard.writeText(inviteLink(code)); setCopied(code); setTimeout(() => setCopied(null), 2000); } catch { setCopied(null); } }; return ( } > {isLoading || !data ? ( ) : data.invites.length === 0 ? ( ) : (
    {data.invites.map((invite) => { const permanent = isEffectivelyPermanent(invite.expiresAt); const expired = !permanent && new Date(invite.expiresAt).getTime() < Date.now(); const state = invite.usedAt ? 'used' : expired ? 'expired' : 'active'; return (
  • {invite.code} {state === 'active' && active} {state === 'used' && ( used by {invite.usedBy} {formatRelativeTime(invite.usedAt)} )} {state === 'expired' && expired}

    {permanent ? 'never expires' : `expires ${formatDateTime(invite.expiresAt)}`} · created by {invite.createdBy ?? '—'}

    {state === 'active' && ( )}
  • ); })}
)}

Invite links are single-use and grant the selected role at login. Redeemed roles persist until you change them under Users & roles.

); }