diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 325ecbe..ac6f847 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -628,7 +628,7 @@ model ProjectFile {
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
- round Round? @relation(fields: [roundId], references: [id])
+ round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
requirement FileRequirement? @relation(fields: [requirementId], references: [id], onDelete: SetNull)
replacedBy ProjectFile? @relation("FileVersions", fields: [replacedById], references: [id], onDelete: SetNull)
replacements ProjectFile[] @relation("FileVersions")
@@ -706,7 +706,7 @@ model Evaluation {
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
- form EvaluationForm @relation(fields: [formId], references: [id])
+ form EvaluationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
@@index([status])
@@index([submittedAt])
diff --git a/src/app/(admin)/admin/projects/page.tsx b/src/app/(admin)/admin/projects/page.tsx
index efb30e7..e5dae26 100644
--- a/src/app/(admin)/admin/projects/page.tsx
+++ b/src/app/(admin)/admin/projects/page.tsx
@@ -104,7 +104,7 @@ const statusColors: Record<
function parseFiltersFromParams(
searchParams: URLSearchParams
-): ProjectFilters & { page: number } {
+): ProjectFilters & { page: number; perPage: number } {
return {
search: searchParams.get('q') || '',
statuses: searchParams.get('status')
@@ -133,11 +133,12 @@ function parseFiltersFromParams(
? false
: undefined,
page: parseInt(searchParams.get('page') || '1', 10),
+ perPage: parseInt(searchParams.get('pp') || '20', 10),
}
}
function filtersToParams(
- filters: ProjectFilters & { page: number }
+ filters: ProjectFilters & { page: number; perPage: number }
): URLSearchParams {
const params = new URLSearchParams()
if (filters.search) params.set('q', filters.search)
@@ -155,11 +156,10 @@ function filtersToParams(
if (filters.hasAssignments !== undefined)
params.set('hasAssign', String(filters.hasAssignments))
if (filters.page > 1) params.set('page', String(filters.page))
+ if (filters.perPage !== 20) params.set('pp', String(filters.perPage))
return params
}
-const PER_PAGE = 20
-
export default function ProjectsPage() {
const pathname = usePathname()
@@ -178,8 +178,17 @@ export default function ProjectsPage() {
hasAssignments: parsed.hasAssignments,
})
const [page, setPage] = useState(parsed.page)
+ const [perPage, setPerPage] = useState(parsed.perPage || 20)
const [searchInput, setSearchInput] = useState(parsed.search)
+ // Fetch display settings
+ const { data: displaySettings } = trpc.settings.getMultiple.useQuery({
+ keys: ['display_project_names_uppercase'],
+ })
+ const uppercaseNames = displaySettings?.find(
+ (s: { key: string; value: string }) => s.key === 'display_project_names_uppercase'
+ )?.value !== 'false'
+
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
@@ -193,8 +202,8 @@ export default function ProjectsPage() {
// Sync URL
const syncUrl = useCallback(
- (f: ProjectFilters, p: number) => {
- const params = filtersToParams({ ...f, page: p })
+ (f: ProjectFilters, p: number, pp: number) => {
+ const params = filtersToParams({ ...f, page: p, perPage: pp })
const qs = params.toString()
window.history.replaceState(null, '', qs ? `${pathname}?${qs}` : pathname)
},
@@ -202,8 +211,13 @@ export default function ProjectsPage() {
)
useEffect(() => {
- syncUrl(filters, page)
- }, [filters, page, syncUrl])
+ syncUrl(filters, page, perPage)
+ }, [filters, page, perPage, syncUrl])
+
+ const handlePerPageChange = (newPerPage: number) => {
+ setPerPage(newPerPage)
+ setPage(1)
+ }
// Reset page when filters change
const handleFiltersChange = (newFilters: ProjectFilters) => {
@@ -248,7 +262,7 @@ export default function ProjectsPage() {
hasFiles: filters.hasFiles,
hasAssignments: filters.hasAssignments,
page,
- perPage: PER_PAGE,
+ perPage,
}
const utils = trpc.useUtils()
@@ -459,14 +473,14 @@ export default function ProjectsPage() {
return (
{/* Header */}
-
+
Projects
- Manage submitted projects across all rounds
+ {data ? `${data.total} projects across all rounds` : 'Manage submitted projects across all rounds'}
-
+
{/* Search */}
-
-
-
setSearchInput(e.target.value)}
- placeholder="Search projects by title, team, or description..."
- className="pl-10"
- />
+
+
+
+ setSearchInput(e.target.value)}
+ placeholder="Search projects by title, team, or description..."
+ className="pl-10 pr-10"
+ />
+ {searchInput && (
+
+ )}
+
+ {filters.search && data && (
+
+ {data.total} result{data.total !== 1 ? 's' : ''} for “{filters.search}”
+
+ )}
{/* Filters */}
@@ -510,6 +540,37 @@ export default function ProjectsPage() {
onChange={handleFiltersChange}
/>
+ {/* Stats Summary */}
+ {data && data.projects.length > 0 && (
+
+ {Object.entries(
+ data.projects.reduce>((acc, p) => {
+ const s = p.status ?? 'SUBMITTED'
+ acc[s] = (acc[s] || 0) + 1
+ return acc
+ }, {})
+ )
+ .sort(([a], [b]) => {
+ const order = ['SUBMITTED', 'ELIGIBLE', 'ASSIGNED', 'SEMIFINALIST', 'FINALIST', 'WINNER', 'REJECTED', 'WITHDRAWN']
+ return order.indexOf(a) - order.indexOf(b)
+ })
+ .map(([status, count]) => (
+
+ {count} {status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')}
+
+ ))}
+ {data.total > data.projects.length && (
+
+ (page {data.page} of {data.totalPages})
+
+ )}
+
+ )}
+
{/* Content */}
{isLoading ? (
@@ -576,10 +637,11 @@ export default function ProjectsPage() {
/>
)}
- Project
+ Project
Round
Files
Assignments
+ Submitted
Status
Actions
@@ -613,11 +675,14 @@ export default function ProjectsPage() {
fallback="initials"
/>
-
+
{truncate(project.title, 40)}
{project.teamName}
+ {project.country && (
+ ยท {project.country}
+ )}
@@ -650,6 +715,11 @@ export default function ProjectsPage() {
{project._count.assignments}
+
+ {project.createdAt
+ ? new Date(project.createdAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
+ : '-'}
+
@@ -733,7 +803,7 @@ export default function ProjectsPage() {
/>
-
+
{project.title}
Assignments
{project._count.assignments} jurors
+
+ Files
+ {project._count?.files ?? 0}
+
+ {project.createdAt && (
+
+ Submitted
+ {new Date(project.createdAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })}
+
+ )}
@@ -773,8 +853,9 @@ export default function ProjectsPage() {
page={data.page}
totalPages={data.totalPages}
total={data.total}
- perPage={PER_PAGE}
+ perPage={perPage}
onPageChange={setPage}
+ onPerPageChange={handlePerPageChange}
/>
>
) : null}
diff --git a/src/components/settings/defaults-settings-form.tsx b/src/components/settings/defaults-settings-form.tsx
index 28b2578..9ffd972 100644
--- a/src/components/settings/defaults-settings-form.tsx
+++ b/src/components/settings/defaults-settings-form.tsx
@@ -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
@@ -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) {
)}
/>
+ (
+
+
+
+ Display Project Names in Uppercase
+
+
+ Show all project names in uppercase across the platform for a cleaner presentation
+
+
+
+ field.onChange(String(checked))}
+ />
+
+
+ )}
+ />
+