feat: add photo folder filter to gallery and migrate to docker image registry

This commit is contained in:
Jannik committed 2026-09-21 23:22:06 +02:00
1 parent b080660188
commit 39ef0c4b19
3 files changed
+75 -7

No files matched your search

+1 -3
View File
@@ -1,8 +1,6 @@
services: services:
app: app:
build: image: git.orfel.de/jannik/nele-till-hochzeit:latest
context: .
dockerfile: Dockerfile
container_name: wedding-app container_name: wedding-app
restart: unless-stopped restart: unless-stopped
ports: ports:
+13 -1
View File
@@ -22,8 +22,18 @@ async function getInitialMedia() {
nextCursor = extra!.id; nextCursor = extra!.id;
} }
// Fetch voting status for the client
const settings = await prisma.appSettings.findUnique({ where: { id: 1 } }); const settings = await prisma.appSettings.findUnique({ where: { id: 1 } });
// Fetch available directories for filtering
const dirs = await prisma.mediaItem.findMany({
where: { published: true },
select: { directory: true },
distinct: ['directory'],
orderBy: { directory: 'asc' }
});
const directories = dirs.map(d => d.directory).filter(Boolean);
// Serialize BigInt values for client // Serialize BigInt values for client
return { return {
items: JSON.parse(JSON.stringify(items, (_key, value) => items: JSON.parse(JSON.stringify(items, (_key, value) =>
@@ -31,10 +41,11 @@ async function getInitialMedia() {
)), )),
nextCursor, nextCursor,
votingOpen: settings?.votingOpen ?? false, votingOpen: settings?.votingOpen ?? false,
directories,
}; };
} catch (error) { } catch (error) {
console.error('[Page] Error fetching initial media:', error); console.error('[Page] Error fetching initial media:', error);
return { items: [], nextCursor: null, votingOpen: false }; return { items: [], nextCursor: null, votingOpen: false, directories: [] };
} }
} }
@@ -48,6 +59,7 @@ export default async function HomePage() {
initialItems={data.items} initialItems={data.items}
initialCursor={data.nextCursor} initialCursor={data.nextCursor}
initialVotingOpen={data.votingOpen} initialVotingOpen={data.votingOpen}
directories={data.directories}
/> />
</main> </main>
); );
+61 -3
View File
@@ -26,6 +26,7 @@ type MediaWallProps = {
initialCursor: number | null; initialCursor: number | null;
initialViewerIndex?: number; initialViewerIndex?: number;
initialVotingOpen?: boolean; initialVotingOpen?: boolean;
directories?: string[];
}; };
export default function MediaWall({ export default function MediaWall({
@@ -33,12 +34,42 @@ export default function MediaWall({
initialCursor, initialCursor,
initialViewerIndex = -1, initialViewerIndex = -1,
initialVotingOpen = false, initialVotingOpen = false,
directories = [],
}: MediaWallProps) { }: MediaWallProps) {
const [items, setItems] = useState<MediaItem[]>(initialItems); const [items, setItems] = useState<MediaItem[]>(initialItems);
const [cursor, setCursor] = useState<number | null>(initialCursor); const [cursor, setCursor] = useState<number | null>(initialCursor);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(!!initialCursor); const [hasMore, setHasMore] = useState(!!initialCursor);
const [votingOpen, setVotingOpen] = useState(initialVotingOpen); const [votingOpen, setVotingOpen] = useState(initialVotingOpen);
const [selectedDir, setSelectedDir] = useState<string>('');
const handleDirChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const dir = e.target.value;
setSelectedDir(dir);
if (dir === '') {
setItems(initialItems);
setCursor(initialCursor);
setHasMore(!!initialCursor);
return;
}
setLoading(true);
setItems([]);
try {
const res = await fetch(`/api/media?limit=50&directory=${encodeURIComponent(dir)}`);
if (res.ok) {
const data = await res.json();
setItems(data.items);
setCursor(data.nextCursor);
setHasMore(!!data.nextCursor);
}
} catch (err) {
console.error('Error fetching filtered media:', err);
} finally {
setLoading(false);
}
};
const { isOpen, currentItem, currentIndex, open, close, next, prev } = const { isOpen, currentItem, currentIndex, open, close, next, prev } =
useViewer(items, initialViewerIndex); useViewer(items, initialViewerIndex);
@@ -51,7 +82,8 @@ export default function MediaWall({
if (loading || !hasMore || !cursor) return; if (loading || !hasMore || !cursor) return;
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`/api/media?limit=50&cursor=${cursor}`); const dirParam = selectedDir ? `&directory=${encodeURIComponent(selectedDir)}` : '';
const res = await fetch(`/api/media?limit=50&cursor=${cursor}${dirParam}`);
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setItems((prev) => { setItems((prev) => {
@@ -72,7 +104,7 @@ export default function MediaWall({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [loading, hasMore, cursor]); }, [loading, hasMore, cursor, selectedDir]);
// Infinite scroll observer // Infinite scroll observer
useEffect(() => { useEffect(() => {
@@ -109,7 +141,33 @@ export default function MediaWall({
return ( return (
<> <>
<div className="media-wall" ref={wallRef}> {directories.length > 0 && (
<div style={{ padding: '80px 20px 0', maxWidth: '1920px', margin: '0 auto', display: 'flex', justifyContent: 'center' }}>
<select
value={selectedDir}
onChange={handleDirChange}
style={{
padding: '10px 20px',
borderRadius: '20px',
border: '1px solid var(--color-border)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-body)',
fontSize: '1rem',
outline: 'none',
cursor: 'pointer',
boxShadow: 'var(--shadow)',
color: 'var(--color-text)',
minWidth: '200px'
}}
>
<option value="">Alle Fotos</option>
{directories.map(dir => (
<option key={dir} value={dir}>{dir}</option>
))}
</select>
</div>
)}
<div className="media-wall" ref={wallRef} style={directories.length > 0 ? { paddingTop: '20px' } : {}}>
{items.map((item, index) => ( {items.map((item, index) => (
<MediaTile <MediaTile
key={item.id} key={item.id}