'use client'; import { useState, useEffect, useRef } from 'react'; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { toast } from 'sonner'; import ProfileSectionSkeleton from './ProfileSectionSkeleton'; interface EventKey { key: string; displayLabel: string; description: string | null; sortOrder: number; } interface SubscriptionData { eventKeys: EventKey[]; channelTypes: string[]; matrix: Record>; } export function ProfileNotificationMatrix() { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [matrix, setMatrix] = useState>>({}); // Track per-cell debounce timers keyed by "eventKey::channelType" const pendingTimers = useRef>>(new Map()); useEffect(() => { fetch('/api/me/notification-subscriptions') .then((r) => r.json()) .then((fetched: SubscriptionData) => { setData(fetched); setMatrix(fetched.matrix); setLoading(false); }) .catch(() => { setLoading(false); }); }, []); function handleToggle(eventKey: string, channelType: string, newValue: boolean) { // Optimistic update setMatrix((prev) => ({ ...prev, [eventKey]: { ...(prev[eventKey] ?? {}), [channelType]: newValue, }, })); const cellKey = `${eventKey}::${channelType}`; // Clear any existing debounce timer for this cell const existing = pendingTimers.current.get(cellKey); if (existing) clearTimeout(existing); const timer = setTimeout(async () => { pendingTimers.current.delete(cellKey); try { const res = await fetch('/api/me/notification-subscriptions', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_key: eventKey, channel_type: channelType, enabled: newValue, }), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } toast.success('Preference saved'); } catch { // Revert on error setMatrix((prev) => ({ ...prev, [eventKey]: { ...(prev[eventKey] ?? {}), [channelType]: !newValue, }, })); toast.error("Couldn't save preference"); } }, 400); pendingTimers.current.set(cellKey, timer); } if (loading) { return ; } return ( Notifications Choose which events trigger a personal notification. {!data || data.channelTypes.length === 0 ? (

Configure a Teams or ntfy channel below to enable personal notifications.

) : data.channelTypes.length === 1 ? ( /* Single channel type — simple list, no header row */
{data.eventKeys.map((ek) => { const channelType = data.channelTypes[0]; const checked = matrix[ek.key]?.[channelType] ?? true; return (
{ek.displayLabel} handleToggle(ek.key, channelType, val)} aria-label={ek.displayLabel} />
); })}
) : ( /* Multiple channel types — grid with header row */
{/* Header row */}
{data.channelTypes.map((ct) => ( {ct} ))}
{/* Per event-key rows */} {data.eventKeys.map((ek) => (
{ek.displayLabel} {data.channelTypes.map((ct) => { const checked = matrix[ek.key]?.[ct] ?? true; return (
handleToggle(ek.key, ct, val)} aria-label={`${ek.displayLabel} — ${ct}`} />
); })}
))}
)}
); }