Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
182 lines
5.8 KiB
TypeScript
182 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
import { toast } from 'sonner'
|
|
import { Loader2, Settings } from 'lucide-react'
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
|
|
const COMMON_TIMEZONES = [
|
|
{ value: 'Europe/Monaco', label: 'Monaco (CET/CEST)' },
|
|
{ value: 'Europe/Paris', label: 'Paris (CET/CEST)' },
|
|
{ value: 'Europe/London', label: 'London (GMT/BST)' },
|
|
{ value: 'America/New_York', label: 'New York (EST/EDT)' },
|
|
{ value: 'America/Los_Angeles', label: 'Los Angeles (PST/PDT)' },
|
|
{ value: 'Asia/Tokyo', label: 'Tokyo (JST)' },
|
|
{ value: 'Asia/Singapore', label: 'Singapore (SGT)' },
|
|
{ value: 'Australia/Sydney', label: 'Sydney (AEST/AEDT)' },
|
|
{ value: 'UTC', label: 'UTC' },
|
|
]
|
|
|
|
const formSchema = z.object({
|
|
default_timezone: z.string().min(1, 'Timezone is required'),
|
|
default_page_size: z.string().regex(/^\d+$/, 'Must be a number'),
|
|
autosave_interval_seconds: z.string().regex(/^\d+$/, 'Must be a number'),
|
|
})
|
|
|
|
type FormValues = z.infer<typeof formSchema>
|
|
|
|
interface DefaultsSettingsFormProps {
|
|
settings: {
|
|
default_timezone?: string
|
|
default_page_size?: string
|
|
autosave_interval_seconds?: string
|
|
}
|
|
}
|
|
|
|
export function DefaultsSettingsForm({ settings }: DefaultsSettingsFormProps) {
|
|
const utils = trpc.useUtils()
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
default_timezone: settings.default_timezone || 'Europe/Monaco',
|
|
default_page_size: settings.default_page_size || '20',
|
|
autosave_interval_seconds: settings.autosave_interval_seconds || '30',
|
|
},
|
|
})
|
|
|
|
const updateSettings = trpc.settings.updateMultiple.useMutation({
|
|
onSuccess: () => {
|
|
toast.success('Default settings saved successfully')
|
|
utils.settings.getByCategory.invalidate({ category: 'DEFAULTS' })
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to save settings: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const onSubmit = (data: FormValues) => {
|
|
updateSettings.mutate({
|
|
settings: [
|
|
{ key: 'default_timezone', value: data.default_timezone },
|
|
{ key: 'default_page_size', value: data.default_page_size },
|
|
{ key: 'autosave_interval_seconds', value: data.autosave_interval_seconds },
|
|
],
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="default_timezone"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Default Timezone</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select timezone" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{COMMON_TIMEZONES.map((tz) => (
|
|
<SelectItem key={tz.value} value={tz.value}>
|
|
{tz.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Timezone used for displaying dates and deadlines across the platform
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="default_page_size"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Default Page Size</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select page size" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="10">10 items per page</SelectItem>
|
|
<SelectItem value="20">20 items per page</SelectItem>
|
|
<SelectItem value="50">50 items per page</SelectItem>
|
|
<SelectItem value="100">100 items per page</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Default number of items shown in lists and tables
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="autosave_interval_seconds"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Autosave Interval (seconds)</FormLabel>
|
|
<FormControl>
|
|
<Input type="number" min="10" max="120" placeholder="30" {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
How often evaluation forms are automatically saved while editing.
|
|
Lower values provide better data protection but increase server load.
|
|
Recommended: 30 seconds.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button type="submit" disabled={updateSettings.isPending}>
|
|
{updateSettings.isPending ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Saving...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
Save Default Settings
|
|
</>
|
|
)}
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
)
|
|
}
|