228 lines
6.5 KiB
TypeScript
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}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|