feat: sortable directories in admin panel

This commit is contained in:
Jannik committed 2026-09-30 21:05:50 +02:00
1 parent 0b7738b472
commit a275edb901
5 files changed
+92 -7

No files matched your search

+2
View File
@@ -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
+1
View File
@@ -92,6 +92,7 @@ model AppSettings {
zipBuildAt DateTime?
zipFilePath String? // Path to current valid ZIP
lastScanAt DateTime?
directoryOrder String[] @default([])
updatedAt DateTime @updatedAt
}
+73 -4
View File
@@ -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<MediaItem[]>([]);
const [stats, setStats] = useState({ total: 0, published: 0, unpublished: 0 });
const [votingOpen, setVotingOpen] = useState(false);
const [directoryOrder, setDirectoryOrder] = useState<string[]>([]);
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 <div style={{ padding: '20px' }}>Prüfe Berechtigung...</div>;
return (
@@ -166,7 +204,7 @@ export default function AdminDashboard() {
</div>
<div style={{ flex: '1 1 300px', background: 'var(--color-surface)', padding: '20px', borderRadius: 'var(--radius)', border: '1px solid var(--color-border)' }}>
<h3 style={{ marginBottom: '10px', fontSize: '1.1rem' }}>Einstellungen</h3>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer', marginBottom: '20px' }}>
<input
type="checkbox"
checked={votingOpen}
@@ -175,6 +213,37 @@ export default function AdminDashboard() {
/>
Voting aktiviert (Likes)
</label>
<h4 style={{ marginBottom: '10px', fontSize: '1rem' }}>Ordner-Reihenfolge</h4>
{orderedFolders.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{orderedFolders.map((folder, index) => (
<div key={folder} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: 'var(--color-background)', padding: '8px 12px', borderRadius: '4px', border: '1px solid var(--color-border)' }}>
<span>{folder}</span>
<div style={{ display: 'flex', gap: '5px' }}>
<button
onClick={() => moveFolder(index, 'up')}
disabled={index === 0}
style={{ background: 'none', border: 'none', cursor: index === 0 ? 'default' : 'pointer', opacity: index === 0 ? 0.3 : 1, fontSize: '1.2rem' }}
title="Nach oben"
>
↑
</button>
<button
onClick={() => moveFolder(index, 'down')}
disabled={index === orderedFolders.length - 1}
style={{ background: 'none', border: 'none', cursor: index === orderedFolders.length - 1 ? 'default' : 'pointer', opacity: index === orderedFolders.length - 1 ? 0.3 : 1, fontSize: '1.2rem' }}
title="Nach unten"
>
↓
</button>
</div>
</div>
))}
</div>
) : (
<p style={{ color: 'var(--color-text-light)', fontSize: '0.9rem' }}>Keine Ordner vorhanden.</p>
)}
</div>
</div>
+3 -2
View File
@@ -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 }),
}
});
+13 -1
View File
@@ -53,11 +53,23 @@ 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 {
items: JSON.parse(JSON.stringify(items, (_key, value) =>