diff --git a/apps/pwa/src/routes/AdminPage.tsx b/apps/pwa/src/routes/AdminPage.tsx index 7d4f823..14a8e5b 100644 --- a/apps/pwa/src/routes/AdminPage.tsx +++ b/apps/pwa/src/routes/AdminPage.tsx @@ -55,6 +55,22 @@ const sectionLabelStyle: React.CSSProperties = { export function AdminPage() { const queryClient = useQueryClient(); + // Phone detection for toast bottom offset + const phone = typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches; + + // Success toast state (D-08) + const [toast, setToast] = useState(null); + + // Auto-dismiss toast after 3000ms — mirrors SyncStateToast lines 72-79 + useEffect(() => { + if (!toast) return; + const timer = setTimeout(() => setToast(null), 3000); + return () => clearTimeout(timer); + }, [toast]); + + // Two-tab navigation state (D-10) + const [activeTab, setActiveTab] = useState<'members' | 'settings'>('members'); + // Credential sheet state const [sheetOpen, setSheetOpen] = useState(false); const [sheetMode, setSheetMode] = useState('admin-add'); @@ -185,6 +201,29 @@ export function AdminPage() { }, }); + // Roving tabindex keyboard handler for the two-tab strip (D-10) + function handleTabKeyDown(e: React.KeyboardEvent, current: 'members' | 'settings') { + if (e.key === 'ArrowRight') { + e.preventDefault(); + const next = current === 'members' ? 'settings' : 'members'; + setActiveTab(next); + ( + e.currentTarget.parentElement?.querySelector( + `[id="admin-tab-${next}"]`, + ) as HTMLElement | null + )?.focus(); + } else if (e.key === 'ArrowLeft') { + e.preventDefault(); + const prev = current === 'settings' ? 'members' : 'settings'; + setActiveTab(prev); + ( + e.currentTarget.parentElement?.querySelector( + `[id="admin-tab-${prev}"]`, + ) as HTMLElement | null + )?.focus(); + } + } + // Open credential sheet for a member function openSheet(member: AdminMember, buttonRef: React.RefObject) { // Capture the button so focus can return on close @@ -219,6 +258,7 @@ export function AdminPage() { setCreateError(null); void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] }); void queryClient.invalidateQueries({ queryKey: ['me'] }); + setToast('Member added.'); }, onError: (err) => { const msg = err instanceof Error ? err.message : 'server'; @@ -266,7 +306,7 @@ export function AdminPage() { {/* Page heading */}

- {/* ── MEMBERS section ─────────────────────────────────────────────── */} -
+ {/* ── Two-tab strip (D-10) ──────────────────────────────────────────── */} +
+ {(['members', 'settings'] as const).map((id) => ( + + ))} +
+ + {/* ── Tab panel: Members & Accounts ────────────────────────────────── */} + -
- + + + {/* end admin-panel-members */} + + {/* ── Tab panel: Settings ───────────────────────────────────────────── */} + {/* end admin-panel-settings */} + + {/* end centered content column */} + + {/* ── Success toast (D-08) ──────────────────────────────────────────────── */} + {toast && ( +
+
+ )} {/* Credential sheet — admin-rotate or admin-add */} {sheetMember && ( @@ -937,6 +1087,7 @@ export function AdminPage() { resetTriggerRef.current.focus(); } }} + onSuccess={() => setToast('Password reset.')} member={resetTargetMember} /> )} @@ -1189,10 +1340,11 @@ function CalendarRadioRow({ calendar, isSelected, onSelect }: CalendarRadioRowPr interface ResetPasswordSheetProps { isOpen: boolean; onClose: () => void; + onSuccess?: () => void; member: AdminMember; } -function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps) { +function ResetPasswordSheet({ isOpen, onClose, onSuccess, member }: ResetPasswordSheetProps) { const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(null); @@ -1222,6 +1374,10 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps onClose(); } + // Phone detection for desktop centering + const sheetPhone = + typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches; + const resetMutation = useMutation({ mutationFn: async () => { if (newPassword !== confirmPassword) throw new Error('mismatch'); @@ -1229,6 +1385,7 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps }, onSuccess: () => { handleClose(); + onSuccess?.(); }, onError: (err) => { const msg = err instanceof Error ? err.message : 'server'; @@ -1259,25 +1416,42 @@ function ResetPasswordSheet({ isOpen, onClose, member }: ResetPasswordSheetProps }} /> - {/* Sheet */} + {/* Sheet — phone: bottom-sheet / desktop: centered modal (D-09) */}