Files
Nele-Till-Hochzeit/src/components/MediaWall.tsx
T

228 lines
6.5 KiB
TypeScript

'use client';
import React, { useEffect, useRef, useCallback, useState } from 'react';
import MediaTile from './MediaTile';
import FullscreenViewer from './FullscreenViewer';
import { useViewer } from '@/hooks/useViewer';
type MediaItem = {
id: number;
relativePath: string;
filename: string;
mediaType: string;
mimeType: string;
width: number | null;
height: number | null;
featured: boolean;
gridWidth: number;
gridHeight: number;
placementMode: string;
_count: { likes: number };
[key: string]: unknown;
};
type MediaWallProps = {
initialItems: MediaItem[];
initialCursor: number | null;
initialViewerIndex?: number;
initialVotingOpen?: boolean;
directories?: string[];
};
export default function MediaWall({
initialItems,
initialCursor,
initialViewerIndex = -1,
initialVotingOpen = false,
directories = [],
}: MediaWallProps) {
const [items, setItems] = useState<MediaItem[]>(initialItems);
const [cursor, setCursor] = useState<number | null>(initialCursor);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(!!initialCursor);
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 } =
useViewer(items, initialViewerIndex);
const observerRef = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useRef<HTMLDivElement>(null);
const wallRef = useRef<HTMLDivElement>(null);
const loadMore = useCallback(async () => {
if (loading || !hasMore || !cursor) return;
setLoading(true);
try {
const dirParam = selectedDir ? `&directory=${encodeURIComponent(selectedDir)}` : '';
const res = await fetch(`/api/media?limit=50&cursor=${cursor}${dirParam}`);
if (res.ok) {
const data = await res.json();
setItems((prev) => {
const existingIds = new Set(prev.map((p) => p.id));
const newItems = data.items.filter(
(item: MediaItem) => !existingIds.has(item.id)
);
return [...prev, ...newItems];
});
setCursor(data.nextCursor);
setHasMore(!!data.nextCursor);
if (data.votingOpen !== undefined) {
setVotingOpen(data.votingOpen);
}
}
} catch (err) {
console.error('Error loading more media:', err);
} finally {
setLoading(false);
}
}, [loading, hasMore, cursor, selectedDir]);
// Infinite scroll observer
useEffect(() => {
if (loading || !hasMore) return;
if (observerRef.current) observerRef.current.disconnect();
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMore();
}
});
if (loadMoreRef.current) {
observerRef.current.observe(loadMoreRef.current);
}
return () => {
if (observerRef.current) observerRef.current.disconnect();
};
}, [loading, hasMore, loadMore]);
// Scroll to current item when viewer closes
useEffect(() => {
if (!isOpen && currentIndex >= 0 && wallRef.current) {
const tile = wallRef.current.querySelector(
`[data-index="${currentIndex}"]`
);
if (tile) {
tile.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}, [isOpen, currentIndex]);
return (
<>
{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) => (
<MediaTile
key={item.id}
item={item}
index={index}
onClick={() => open(index)}
votingOpen={votingOpen}
/>
))}
{hasMore && (
<div
ref={loadMoreRef}
style={{ height: '20px', width: '100%', gridColumn: '1 / -1' }}
/>
)}
{loading && (
<div
style={{
padding: '20px',
textAlign: 'center',
gridColumn: '1 / -1',
color: 'var(--color-text-light)',
}}
>
Lade mehr...
</div>
)}
{!hasMore && items.length > 0 && (
<div
style={{
padding: '40px 20px',
textAlign: 'center',
gridColumn: '1 / -1',
color: 'var(--color-text-light)',
fontFamily: 'var(--font-display)',
fontSize: '0.9rem',
}}
>
♥
</div>
)}
</div>
{isOpen && currentItem && (
<FullscreenViewer
item={currentItem}
onClose={close}
onNext={next}
onPrev={prev}
hasNext={currentIndex < items.length - 1}
hasPrev={currentIndex > 0}
votingOpen={votingOpen}
/>
)}
</>
);
}