From a275edb901fbe981204acbb8a4484308c2a3355e Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Wed, 30 Sep 2026 21:05:50 +0200 Subject: [PATCH] feat: sortable directories in admin panel --- docker-compose.yml | 2 + prisma/schema.prisma | 17 ++++--- src/app/admin/page.tsx | 77 +++++++++++++++++++++++++++-- src/app/api/admin/settings/route.ts | 5 +- src/app/page.tsx | 14 +++++- 5 files changed, 100 insertions(+), 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d0e1f49..fc2a4b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - wedding-pgdata:/var/lib/postgresql/data + ports: + - "127.0.0.1:5433:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 848bcb1..ce93eab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -85,14 +85,15 @@ model Like { } model AppSettings { - id Int @id @default(1) - votingOpen Boolean @default(false) - zipVersion Int @default(0) - zipReady Boolean @default(false) - zipBuildAt DateTime? - zipFilePath String? // Path to current valid ZIP - lastScanAt DateTime? - updatedAt DateTime @updatedAt + id Int @id @default(1) + votingOpen Boolean @default(false) + zipVersion Int @default(0) + zipReady Boolean @default(false) + zipBuildAt DateTime? + zipFilePath String? // Path to current valid ZIP + lastScanAt DateTime? + directoryOrder String[] @default([]) + updatedAt DateTime @updatedAt } model AdminSession { diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 30d72b4..5888b6f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -11,6 +11,7 @@ type MediaItem = { published: boolean; featured: boolean; mediaType: string; + directory: string; }; export default function AdminDashboard() { @@ -18,6 +19,7 @@ export default function AdminDashboard() { const [items, setItems] = useState([]); const [stats, setStats] = useState({ total: 0, published: 0, unpublished: 0 }); const [votingOpen, setVotingOpen] = useState(false); + const [directoryOrder, setDirectoryOrder] = useState([]); const [loading, setLoading] = useState(true); const router = useRouter(); @@ -39,6 +41,14 @@ export default function AdminDashboard() { const fetchData = async () => { setLoading(true); try { + // Fetch settings + const settingsRes = await fetch('/api/admin/settings'); + if (settingsRes.ok) { + const settings = await settingsRes.json(); + setVotingOpen(settings.votingOpen ?? false); + setDirectoryOrder(settings.directoryOrder ?? []); + } + // Fetch all media (admin=true bypasses publication check) const res = await fetch('/api/media?admin=true&limit=5000'); if (res.ok) { @@ -49,9 +59,6 @@ export default function AdminDashboard() { published: data.items.filter((i: MediaItem) => i.published).length, unpublished: data.items.filter((i: MediaItem) => !i.published).length, }); - if (data.votingOpen !== undefined) { - setVotingOpen(data.votingOpen); - } } } catch (err) { console.error('Error fetching admin data:', err); @@ -143,6 +150,37 @@ export default function AdminDashboard() { } }; + const currentFolders = Array.from(new Set(items.map(item => item.directory))).filter(Boolean); + const orderedFolders = [ + ...directoryOrder.filter(d => currentFolders.includes(d)), + ...currentFolders.filter(d => !directoryOrder.includes(d)) + ]; + + const moveFolder = async (index: number, direction: 'up' | 'down') => { + const newOrder = [...orderedFolders]; + if (direction === 'up' && index > 0) { + [newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]]; + } else if (direction === 'down' && index < newOrder.length - 1) { + [newOrder[index + 1], newOrder[index]] = [newOrder[index], newOrder[index + 1]]; + } else { + return; + } + + setDirectoryOrder(newOrder); + + try { + await fetch('/api/admin/settings', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ directoryOrder: newOrder }) + }); + } catch (err) { + console.error('Failed to update directory order', err); + // Fallback + fetchData(); + } + }; + if (!authChecked) return
Prüfe Berechtigung...
; return ( @@ -166,7 +204,7 @@ export default function AdminDashboard() {

Einstellungen

-
diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts index dad606d..d221fb8 100644 --- a/src/app/api/admin/settings/route.ts +++ b/src/app/api/admin/settings/route.ts @@ -30,13 +30,14 @@ export async function PATCH(request: NextRequest) { if (authError) return authError; try { - const { votingOpen } = await request.json(); + const { votingOpen, directoryOrder } = await request.json(); const settings = await getSettings(); const updated = await prisma.appSettings.update({ where: { id: settings.id }, data: { - ...(votingOpen !== undefined && { votingOpen }) + ...(votingOpen !== undefined && { votingOpen }), + ...(directoryOrder !== undefined && { directoryOrder }), } }); diff --git a/src/app/page.tsx b/src/app/page.tsx index f1b9f02..f4e49b2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -53,10 +53,22 @@ async function getInitialMedia() { distinct: ['directory'], orderBy: { directory: 'asc' } }); - const directories = dirs + + let directories = dirs .map(d => d.directory) .filter(Boolean) .filter(d => !d.startsWith('_')); + + if (settings?.directoryOrder && settings.directoryOrder.length > 0) { + directories.sort((a, b) => { + const indexA = settings.directoryOrder.indexOf(a); + const indexB = settings.directoryOrder.indexOf(b); + if (indexA !== -1 && indexB !== -1) return indexA - indexB; + if (indexA !== -1) return -1; + if (indexB !== -1) return 1; + return a.localeCompare(b); + }); + } // Serialize BigInt values for client return {