'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 */}
General
Defaults
Branding
Locale
Communication
{isSuperAdmin && (
Email
)}
Notifications
Digest
Security
{isSuperAdmin && (
Security
)}
Audit
Features
{isSuperAdmin && (
AI
)}
Tags
Analytics
{isSuperAdmin && (
)}
{/* 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
Manage Expertise Tags
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
Manage Templates
{isSuperAdmin && (
Webhooks
Configure webhook endpoints for platform events
Manage Webhooks
)}
>
)
}
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 (
{label}
{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 (
{label}
{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 (
{label}
{description && (
{description}
)}
mutation.mutate({ key: settingKey, value: v })}
disabled={mutation.isPending}
>
{options.map((opt) => (
{opt.label}
))}
)
}
function DigestSettingsSection({ settings }: { settings: Record }) {
return (
)
}
function AnalyticsSettingsSection({ settings }: { settings: Record }) {
return (
Observer Tab Visibility
Choose which analytics tabs are visible to observers
PDF Reports
)
}
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 (
Enabled Languages
EN
English
toggleLocale('en')}
disabled={mutation.isPending}
/>
FR
Français
toggleLocale('fr')}
disabled={mutation.isPending}
/>
)
}