'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 } export function SettingsContent({ initialSettings }: 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 Email Notif. Digest Security Audit AI Tags Analytics Storage
{/* Desktop: sidebar navigation */}
{/* Content area */}
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 Email Configuration Configure email settings for notifications and magic links Notification Email Settings Configure which notification types should also send email notifications File Storage Configure file upload limits and allowed types 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 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} />
) }