feat: sortable directories in admin panel
This commit is contained in:
1 parent
0b7738b472
commit
a275edb901
5 files changed
+100
-15
No files matched your search
@@ -28,6 +28,8 @@ services:
|
|||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
volumes:
|
volumes:
|
||||||
- wedding-pgdata:/var/lib/postgresql/data
|
- wedding-pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5433:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -85,14 +85,15 @@ model Like {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model AppSettings {
|
model AppSettings {
|
||||||
id Int @id @default(1)
|
id Int @id @default(1)
|
||||||
votingOpen Boolean @default(false)
|
votingOpen Boolean @default(false)
|
||||||
zipVersion Int @default(0)
|
zipVersion Int @default(0)
|
||||||
zipReady Boolean @default(false)
|
zipReady Boolean @default(false)
|
||||||
zipBuildAt DateTime?
|
zipBuildAt DateTime?
|
||||||
zipFilePath String? // Path to current valid ZIP
|
zipFilePath String? // Path to current valid ZIP
|
||||||
lastScanAt DateTime?
|
lastScanAt DateTime?
|
||||||
updatedAt DateTime @updatedAt
|
directoryOrder String[] @default([])
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model AdminSession {
|
model AdminSession {
|
||||||
|
|||||||
+73
-4
@@ -11,6 +11,7 @@ type MediaItem = {
|
|||||||
published: boolean;
|
published: boolean;
|
||||||
featured: boolean;
|
featured: boolean;
|
||||||
mediaType: string;
|
mediaType: string;
|
||||||
|
directory: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
@@ -18,6 +19,7 @@ export default function AdminDashboard() {
|
|||||||
const [items, setItems] = useState<MediaItem[]>([]);
|
const [items, setItems] = useState<MediaItem[]>([]);
|
||||||
const [stats, setStats] = useState({ total: 0, published: 0, unpublished: 0 });
|
const [stats, setStats] = useState({ total: 0, published: 0, unpublished: 0 });
|
||||||
const [votingOpen, setVotingOpen] = useState(false);
|
const [votingOpen, setVotingOpen] = useState(false);
|
||||||
|
const [directoryOrder, setDirectoryOrder] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -39,6 +41,14 @@ export default function AdminDashboard() {
|
|||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
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)
|
// Fetch all media (admin=true bypasses publication check)
|
||||||
const res = await fetch('/api/media?admin=true&limit=5000');
|
const res = await fetch('/api/media?admin=true&limit=5000');
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -49,9 +59,6 @@ export default function AdminDashboard() {
|
|||||||
published: data.items.filter((i: MediaItem) => i.published).length,
|
published: data.items.filter((i: MediaItem) => i.published).length,
|
||||||
unpublished: 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) {
|
} catch (err) {
|
||||||
console.error('Error fetching admin data:', 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>;
|
if (!authChecked) return <div style={{ padding: '20px' }}>Prüfe Berechtigung...</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -166,7 +204,7 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: '1 1 300px', background: 'var(--color-surface)', padding: '20px', borderRadius: 'var(--radius)', border: '1px solid var(--color-border)' }}>
|
<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>
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={votingOpen}
|
checked={votingOpen}
|
||||||
@@ -175,6 +213,37 @@ export default function AdminDashboard() {
|
|||||||
/>
|
/>
|
||||||
Voting aktiviert (Likes)
|
Voting aktiviert (Likes)
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ export async function PATCH(request: NextRequest) {
|
|||||||
if (authError) return authError;
|
if (authError) return authError;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { votingOpen } = await request.json();
|
const { votingOpen, directoryOrder } = await request.json();
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
|
|
||||||
const updated = await prisma.appSettings.update({
|
const updated = await prisma.appSettings.update({
|
||||||
where: { id: settings.id },
|
where: { id: settings.id },
|
||||||
data: {
|
data: {
|
||||||
...(votingOpen !== undefined && { votingOpen })
|
...(votingOpen !== undefined && { votingOpen }),
|
||||||
|
...(directoryOrder !== undefined && { directoryOrder }),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -53,10 +53,22 @@ async function getInitialMedia() {
|
|||||||
distinct: ['directory'],
|
distinct: ['directory'],
|
||||||
orderBy: { directory: 'asc' }
|
orderBy: { directory: 'asc' }
|
||||||
});
|
});
|
||||||
const directories = dirs
|
|
||||||
|
let directories = dirs
|
||||||
.map(d => d.directory)
|
.map(d => d.directory)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(d => !d.startsWith('_'));
|
.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
|
// Serialize BigInt values for client
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user