'use client' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Skeleton } from '@/components/ui/skeleton' import { Bot, Palette, Mail, HardDrive, Shield, Settings as SettingsIcon, Bell, Tags, ExternalLink, Newspaper, BarChart3, ShieldAlert, Globe, Webhook, LayoutTemplate, } from 'lucide-react' import Link from 'next/link' import { Button } from '@/components/ui/button' import { AISettingsForm } from './ai-settings-form' import { AIUsageCard } from './ai-usage-card' import { BrandingSettingsForm } from './branding-settings-form' import { EmailSettingsForm } from './email-settings-form' import { StorageSettingsForm } from './storage-settings-form' import { SecuritySettingsForm } from './security-settings-form' import { DefaultsSettingsForm } from './defaults-settings-form' import { NotificationSettingsForm } from './notification-settings-form' function SettingsSkeleton() { return (
{[...Array(4)].map((_, i) => ( ))}
) } interface SettingsContentProps { initialSettings: Record isSuperAdmin?: boolean } export function SettingsContent({ initialSettings, isSuperAdmin = true }: SettingsContentProps) { // We use the initial settings passed from the server // Forms will refetch on mutation success // Helper to get settings by prefix const getSettingsByKeys = (keys: string[]) => { const result: Record = {} keys.forEach((key) => { if (initialSettings[key] !== undefined) { result[key] = initialSettings[key] } }) return result } const aiSettings = getSettingsByKeys([ 'ai_enabled', 'ai_provider', 'ai_model', 'ai_send_descriptions', 'openai_api_key', ]) const brandingSettings = getSettingsByKeys([ 'platform_name', 'primary_color', 'secondary_color', 'accent_color', ]) const emailSettings = getSettingsByKeys([ 'smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'email_from', ]) const storageSettings = getSettingsByKeys([ 'max_file_size_mb', 'allowed_file_types', ]) const securitySettings = getSettingsByKeys([ 'session_duration_hours', 'magic_link_expiry_minutes', 'rate_limit_requests_per_minute', ]) const defaultsSettings = getSettingsByKeys([ 'default_timezone', 'default_page_size', 'autosave_interval_seconds', 'display_project_names_uppercase', ]) const digestSettings = getSettingsByKeys([ 'digest_enabled', 'digest_default_frequency', 'digest_send_hour', 'digest_include_evaluations', 'digest_include_assignments', 'digest_include_deadlines', 'digest_include_announcements', ]) const analyticsSettings = getSettingsByKeys([ 'analytics_observer_scores_tab', 'analytics_observer_progress_tab', 'analytics_observer_juror_tab', 'analytics_observer_comparison_tab', 'analytics_pdf_enabled', 'analytics_pdf_sections', ]) const auditSecuritySettings = getSettingsByKeys([ 'audit_retention_days', 'anomaly_detection_enabled', 'anomaly_rapid_actions_threshold', 'anomaly_off_hours_start', 'anomaly_off_hours_end', ]) const localizationSettings = getSettingsByKeys([ 'localization_enabled_locales', 'localization_default_locale', ]) return ( <> {/* Mobile: horizontal scrollable tabs */} Defaults Branding Locale {isSuperAdmin && ( Email )} Notif. Digest {isSuperAdmin && ( Security )} Audit {isSuperAdmin && ( AI )} Tags Analytics {isSuperAdmin && ( Storage )}
{/* Desktop: sidebar navigation */}
{/* Content area */}
{isSuperAdmin && ( AI Configuration Configure AI-powered features like smart jury assignment )} Expertise Tags Manage tags used for jury expertise, project categorization, and AI-powered matching

Expertise tags are used across the platform to:

  • Categorize jury members by their areas of expertise
  • Tag projects for better organization and filtering
  • Power AI-based project tagging
  • Enable smart jury-project matching
Platform Branding Customize the look and feel of your platform {isSuperAdmin && ( Email Configuration Configure email settings for notifications and magic links )} Notification Email Settings Configure which notification types should also send email notifications {isSuperAdmin && ( File Storage Configure file upload limits and allowed types )} {isSuperAdmin && ( Security Settings Configure security and access control settings )} Default Settings Configure default values for the platform Digest Configuration Configure automated digest emails sent to users Analytics & Reports Configure observer dashboard visibility and PDF report settings Audit & Security Configure audit log retention and anomaly detection Localization Configure language and locale settings
{/* end content area */}
{/* end lg:flex */}
{/* Quick Links to sub-pages */}
Round Templates Create reusable round configuration templates {isSuperAdmin && ( Webhooks Configure webhook endpoints for platform events )}
) } export { SettingsSkeleton } // Inline settings sections for new tabs import { useState } from 'react' import { Switch } from '@/components/ui/switch' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Loader2 } from 'lucide-react' import { toast } from 'sonner' function useSettingsMutation() { const utils = trpc.useUtils() return trpc.settings.update.useMutation({ onSuccess: () => { utils.settings.invalidate() toast.success('Setting updated') }, onError: (e) => toast.error(e.message), }) } function SettingToggle({ label, description, settingKey, value, }: { label: string description?: string settingKey: string value: string }) { const mutation = useSettingsMutation() const isChecked = value === 'true' return (
{description && (

{description}

)}
mutation.mutate({ key: settingKey, value: String(checked) }) } />
) } function SettingInput({ label, description, settingKey, value, type = 'text', }: { label: string description?: string settingKey: string value: string type?: string }) { const [localValue, setLocalValue] = useState(value) const mutation = useSettingsMutation() const save = () => { if (localValue !== value) { mutation.mutate({ key: settingKey, value: localValue }) } } return (
{description && (

{description}

)}
setLocalValue(e.target.value)} onBlur={save} className="max-w-xs" /> {mutation.isPending && }
) } function SettingSelect({ label, description, settingKey, value, options, }: { label: string description?: string settingKey: string value: string options: Array<{ value: string; label: string }> }) { const mutation = useSettingsMutation() return (
{description && (

{description}

)}
) } function DigestSettingsSection({ settings }: { settings: Record }) { return (
) } function AnalyticsSettingsSection({ settings }: { settings: Record }) { return (

Choose which analytics tabs are visible to observers

) } function AuditSettingsSection({ settings }: { settings: Record }) { return (
) } function LocalizationSettingsSection({ settings }: { settings: Record }) { const mutation = useSettingsMutation() const enabledLocales = (settings.localization_enabled_locales || 'en').split(',') const toggleLocale = (locale: string) => { const current = new Set(enabledLocales) if (current.has(locale)) { if (current.size <= 1) { toast.error('At least one locale must be enabled') return } current.delete(locale) } else { current.add(locale) } mutation.mutate({ key: 'localization_enabled_locales', value: Array.from(current).join(','), }) } return (
EN English
toggleLocale('en')} disabled={mutation.isPending} />
FR Français
toggleLocale('fr')} disabled={mutation.isPending} />
) }