feat: sortable directories in admin panel
This commit is contained in:
1 parent
0b7738b472
commit
a275edb901
5 files changed
+92
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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) =>
|
||||
|
||||
Reference in new issue
Block a user