Improve projects UX, settings layout, uppercase names, per-page selector, and fix round deletion

- Fix round deletion FK constraint: add onDelete Cascade on Evaluation.form and SetNull on ProjectFile.round
- Add configurable per-page selector (10/20/50/100) to Pagination component, wired in projects page with URL sync
- Add display_project_names_uppercase setting in admin defaults, applied to project titles across desktop/mobile views
- Redesign admin settings page: vertical sidebar nav on desktop with grouped sections, horizontal scrollable tabs on mobile
- Polish projects page: responsive header with total count, search clear button with result count, status stats bar, submission date column, country display, mobile card file count

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 20:13:47 +01:00
parent 829acf8d4e
commit 5c4200158f
5 changed files with 323 additions and 97 deletions

View File

@@ -8,6 +8,7 @@ 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 { Switch } from '@/components/ui/switch'
import {
Form,
FormControl,
@@ -41,6 +42,7 @@ 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'),
display_project_names_uppercase: z.string().optional(),
})
type FormValues = z.infer<typeof formSchema>
@@ -50,6 +52,7 @@ interface DefaultsSettingsFormProps {
default_timezone?: string
default_page_size?: string
autosave_interval_seconds?: string
display_project_names_uppercase?: string
}
}
@@ -62,6 +65,7 @@ export function DefaultsSettingsForm({ settings }: DefaultsSettingsFormProps) {
default_timezone: settings.default_timezone || 'Europe/Monaco',
default_page_size: settings.default_page_size || '20',
autosave_interval_seconds: settings.autosave_interval_seconds || '30',
display_project_names_uppercase: settings.display_project_names_uppercase || 'true',
},
})
@@ -81,6 +85,7 @@ export function DefaultsSettingsForm({ settings }: DefaultsSettingsFormProps) {
{ 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 },
{ key: 'display_project_names_uppercase', value: data.display_project_names_uppercase || 'true' },
],
})
}
@@ -162,6 +167,29 @@ export function DefaultsSettingsForm({ settings }: DefaultsSettingsFormProps) {
)}
/>
<FormField
control={form.control}
name="display_project_names_uppercase"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Display Project Names in Uppercase
</FormLabel>
<FormDescription>
Show all project names in uppercase across the platform for a cleaner presentation
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value === 'true'}
onCheckedChange={(checked) => field.onChange(String(checked))}
/>
</FormControl>
</FormItem>
)}
/>
<Button type="submit" disabled={updateSettings.isPending}>
{updateSettings.isPending ? (
<>

View File

@@ -116,6 +116,7 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
'default_timezone',
'default_page_size',
'autosave_interval_seconds',
'display_project_names_uppercase',
])
const digestSettings = getSettingsByKeys([
@@ -152,58 +153,142 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
return (
<>
<Tabs defaultValue="ai" className="space-y-6">
<TabsList className="flex flex-wrap h-auto gap-1">
<TabsTrigger value="ai" className="gap-2">
<Bot className="h-4 w-4" />
<span className="hidden sm:inline">AI</span>
</TabsTrigger>
<TabsTrigger value="tags" className="gap-2">
<Tags className="h-4 w-4" />
<span className="hidden sm:inline">Tags</span>
</TabsTrigger>
<TabsTrigger value="branding" className="gap-2">
<Palette className="h-4 w-4" />
<span className="hidden sm:inline">Branding</span>
</TabsTrigger>
<TabsTrigger value="email" className="gap-2">
<Mail className="h-4 w-4" />
<span className="hidden sm:inline">Email</span>
</TabsTrigger>
<TabsTrigger value="notifications" className="gap-2">
<Bell className="h-4 w-4" />
<span className="hidden sm:inline">Notifications</span>
</TabsTrigger>
<TabsTrigger value="storage" className="gap-2">
<HardDrive className="h-4 w-4" />
<span className="hidden sm:inline">Storage</span>
</TabsTrigger>
<TabsTrigger value="security" className="gap-2">
<Shield className="h-4 w-4" />
<span className="hidden sm:inline">Security</span>
</TabsTrigger>
<TabsTrigger value="defaults" className="gap-2">
<Tabs defaultValue="defaults" className="space-y-6">
{/* Mobile: horizontal scrollable tabs */}
<TabsList className="flex h-auto gap-1 overflow-x-auto whitespace-nowrap lg:hidden">
<TabsTrigger value="defaults" className="gap-2 shrink-0">
<SettingsIcon className="h-4 w-4" />
<span className="hidden sm:inline">Defaults</span>
Defaults
</TabsTrigger>
<TabsTrigger value="digest" className="gap-2">
<Newspaper className="h-4 w-4" />
<span className="hidden sm:inline">Digest</span>
<TabsTrigger value="branding" className="gap-2 shrink-0">
<Palette className="h-4 w-4" />
Branding
</TabsTrigger>
<TabsTrigger value="analytics" className="gap-2">
<BarChart3 className="h-4 w-4" />
<span className="hidden sm:inline">Analytics</span>
</TabsTrigger>
<TabsTrigger value="audit" className="gap-2">
<ShieldAlert className="h-4 w-4" />
<span className="hidden sm:inline">Audit</span>
</TabsTrigger>
<TabsTrigger value="localization" className="gap-2">
<TabsTrigger value="localization" className="gap-2 shrink-0">
<Globe className="h-4 w-4" />
<span className="hidden sm:inline">Locale</span>
Locale
</TabsTrigger>
<TabsTrigger value="email" className="gap-2 shrink-0">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
<TabsTrigger value="notifications" className="gap-2 shrink-0">
<Bell className="h-4 w-4" />
Notif.
</TabsTrigger>
<TabsTrigger value="digest" className="gap-2 shrink-0">
<Newspaper className="h-4 w-4" />
Digest
</TabsTrigger>
<TabsTrigger value="security" className="gap-2 shrink-0">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
<TabsTrigger value="audit" className="gap-2 shrink-0">
<ShieldAlert className="h-4 w-4" />
Audit
</TabsTrigger>
<TabsTrigger value="ai" className="gap-2 shrink-0">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
<TabsTrigger value="tags" className="gap-2 shrink-0">
<Tags className="h-4 w-4" />
Tags
</TabsTrigger>
<TabsTrigger value="analytics" className="gap-2 shrink-0">
<BarChart3 className="h-4 w-4" />
Analytics
</TabsTrigger>
<TabsTrigger value="storage" className="gap-2 shrink-0">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
</TabsList>
<div className="lg:flex lg:gap-8">
{/* Desktop: sidebar navigation */}
<div className="hidden lg:block lg:w-56 lg:shrink-0">
<nav className="space-y-6">
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">General</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="defaults" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<SettingsIcon className="h-4 w-4" />
Defaults
</TabsTrigger>
<TabsTrigger value="branding" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Palette className="h-4 w-4" />
Branding
</TabsTrigger>
<TabsTrigger value="localization" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Globe className="h-4 w-4" />
Locale
</TabsTrigger>
</TabsList>
</div>
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Communication</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="email" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Mail className="h-4 w-4" />
Email
</TabsTrigger>
<TabsTrigger value="notifications" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bell className="h-4 w-4" />
Notifications
</TabsTrigger>
<TabsTrigger value="digest" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Newspaper className="h-4 w-4" />
Digest
</TabsTrigger>
</TabsList>
</div>
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Security</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="security" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Shield className="h-4 w-4" />
Security
</TabsTrigger>
<TabsTrigger value="audit" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<ShieldAlert className="h-4 w-4" />
Audit
</TabsTrigger>
</TabsList>
</div>
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Features</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bot className="h-4 w-4" />
AI
</TabsTrigger>
<TabsTrigger value="tags" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Tags className="h-4 w-4" />
Tags
</TabsTrigger>
<TabsTrigger value="analytics" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<BarChart3 className="h-4 w-4" />
Analytics
</TabsTrigger>
</TabsList>
</div>
<div>
<p className="mb-2 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Infrastructure</p>
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
<TabsTrigger value="storage" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<HardDrive className="h-4 w-4" />
Storage
</TabsTrigger>
</TabsList>
</div>
</nav>
</div>
{/* Content area */}
<div className="flex-1 min-w-0">
<TabsContent value="ai" className="space-y-6">
<Card>
<CardHeader>
@@ -390,6 +475,8 @@ export function SettingsContent({ initialSettings }: SettingsContentProps) {
</CardContent>
</Card>
</TabsContent>
</div>{/* end content area */}
</div>{/* end lg:flex */}
</Tabs>
{/* Quick Links to sub-pages */}