From 3fdf5016202c4d61d023c236d052de85efc7e390 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 29 Apr 2026 02:36:52 +0200 Subject: [PATCH] refactor(web): RegistrationPanel tab strip restyle + count badges --- .../RegistrationPanel.tsx | 81 ++++++++++++++++--- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/RegistrationPanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/RegistrationPanel.tsx index b8c48f19..76dce4fa 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/RegistrationPanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/RegistrationPanel.tsx @@ -1,6 +1,6 @@ import { createPortal } from 'react-dom'; import { useCallback, useEffect, useRef, useState } from 'react'; -import type { InviteLinkSummary, InviteRedemption } from '@backspace/shared'; +import type { InviteLinkSummary, InviteRedemption, InviteStatus } from '@backspace/shared'; import { api } from '../../../api/client'; import { useSettingsStore } from '../../../stores/settingsStore'; import { useUIStore } from '../../../stores/uiStore'; @@ -42,6 +42,33 @@ function formatExpiry(invite: InviteLinkSummary): string { return `Expires in ${hours}h`; } +function inviteStatusDotColor(status: InviteStatus): string { + switch (status) { + case 'active': return 'bg-status-online'; + case 'expired': return 'bg-accent-rose'; + case 'exhausted': return 'bg-accent-amber'; + case 'revoked': return 'bg-txt-tertiary'; + } +} + +function inviteStatusPillColor(status: InviteStatus): string { + switch (status) { + case 'expired': return 'bg-accent-rose/15 text-accent-rose'; + case 'exhausted': return 'bg-accent-amber/15 text-accent-amber'; + case 'revoked': return 'bg-white/5 text-txt-tertiary'; + case 'active': return ''; // never rendered for active + } +} + +function inviteStatusLabel(status: InviteStatus): string { + switch (status) { + case 'active': return 'Active'; + case 'expired': return 'Expired'; + case 'exhausted': return 'Exhausted'; + case 'revoked': return 'Revoked'; + } +} + type ExpiryPresetId = '1h' | '24h' | '7d' | '30d' | 'never' | 'custom'; /** Edit modal additionally supports a 'keep' option (don't change expiry on PATCH). */ @@ -1218,6 +1245,8 @@ export function RegistrationPanel() { const [tab, setTab] = useState<'active' | 'archived'>('active'); const [invites, setInvites] = useState([]); const [invitesLoading, setInvitesLoading] = useState(false); + const [activeCount, setActiveCount] = useState(0); + const [archivedCount, setArchivedCount] = useState(0); // Tracks the currently displayed tab so in-flight fetches can detect when // the user has switched tabs and discard their stale response. Without this @@ -1245,6 +1274,23 @@ export function RegistrationPanel() { [addToast], ); + const refreshCounts = useCallback(async () => { + try { + const [a, r] = await Promise.all([ + api.invites.list('active'), + api.invites.list('archived'), + ]); + setActiveCount(a.invites.length); + setArchivedCount(r.invites.length); + } catch { + // Leave previous counts on transient failure + } + }, []); + + useEffect(() => { + refreshCounts(); + }, [refreshCounts]); + useEffect(() => { if (instanceSettings) { setDraft({ @@ -1345,6 +1391,7 @@ export function RegistrationPanel() { {/* Invite Links */}
+ {/* Row 1: heading + create button */}
Invite Links
-
- {(['active', 'archived'] as const).map((t) => ( + {/* Row 2: tab strip (left) + filter placeholder (right — Commit 4) */} +
+
- ))} + +
+
{invitesLoading ? ( @@ -1382,7 +1437,7 @@ export function RegistrationPanel() { ) : (
{invites.map((inv) => ( - fetchInvites(tab)} /> + { fetchInvites(tab); refreshCounts(); }} /> ))}
)} @@ -1419,7 +1474,7 @@ export function RegistrationPanel() { {showCreate && ( setShowCreate(false)} - onCreated={() => fetchInvites('active')} + onCreated={() => { fetchInvites('active'); refreshCounts(); }} /> )}