'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 */}
General
Defaults
Branding
Locale
Communication
Email
Notifications
Digest
Features
AI
Tags
Analytics
{/* 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
Manage Expertise Tags
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
Manage Templates
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}
/>
)
}