build collections
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { Search, Upload as UploadIcon, X, Share2, Lock, LogOut } from 'lucide-react';
|
||||
import { useMemes, useTags } from '../hooks/useMemes';
|
||||
import { useMemes, useTags, useCollections } from '../hooks/useMemes';
|
||||
import { useAuth, useLogout } from '../hooks/useAuth';
|
||||
import { GalleryGrid } from '../components/GalleryGrid';
|
||||
import { MemeDetail } from '../components/MemeDetail';
|
||||
import { UploadModal } from '../components/UploadModal';
|
||||
import { LoginModal } from '../components/LoginModal';
|
||||
import { SharePanel } from '../components/SharePanel';
|
||||
import { CollectionBar } from '../components/CollectionBar';
|
||||
import type { Meme } from '../api/client';
|
||||
|
||||
export function Gallery() {
|
||||
const [activeTag, setActiveTag] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
const [activeTag, setActiveTag] = useState<string | null>(null);
|
||||
const [activeCollectionId, setActiveCollectionId] = useState<number | null>(null);
|
||||
const [selectedMemeId, setSelectedMemeId] = useState<string | null>(null);
|
||||
const [quickShareMeme, setQuickShareMeme] = useState<Meme | null>(null);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
@@ -22,6 +24,20 @@ export function Gallery() {
|
||||
const logout = useLogout();
|
||||
const isAdmin = auth?.authenticated === true;
|
||||
|
||||
const { data: collections } = useCollections();
|
||||
|
||||
// Once collections load, default to the Unsorted (default) collection
|
||||
const unsorted = collections?.find((c) => c.is_default);
|
||||
useEffect(() => {
|
||||
if (unsorted && activeCollectionId === null) {
|
||||
setActiveCollectionId(unsorted.id);
|
||||
}
|
||||
}, [unsorted, activeCollectionId]);
|
||||
|
||||
// When on Unsorted, cap at 50 (recent uploads view); other folders show all (paginated)
|
||||
const isUnsorted = activeCollectionId === unsorted?.id;
|
||||
const limit = isUnsorted ? 50 : 100;
|
||||
|
||||
// Debounce search
|
||||
const [searchTimer, setSearchTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||
function handleSearchChange(val: string) {
|
||||
@@ -35,30 +51,33 @@ export function Gallery() {
|
||||
tag: activeTag ?? undefined,
|
||||
q: debouncedSearch || undefined,
|
||||
parent_only: true,
|
||||
collection_id: activeCollectionId ?? undefined,
|
||||
limit,
|
||||
});
|
||||
|
||||
const { data: tags } = useTags();
|
||||
|
||||
const handleOpen = useCallback((meme: Meme) => {
|
||||
setSelectedMemeId(meme.id);
|
||||
}, []);
|
||||
|
||||
const handleShare = useCallback((meme: Meme) => {
|
||||
setQuickShareMeme(meme);
|
||||
}, []);
|
||||
const handleOpen = useCallback((meme: Meme) => setSelectedMemeId(meme.id), []);
|
||||
const handleShare = useCallback((meme: Meme) => setQuickShareMeme(meme), []);
|
||||
|
||||
function handleUploadClick() {
|
||||
if (isAdmin) {
|
||||
setShowUpload(true);
|
||||
} else {
|
||||
setShowLogin(true);
|
||||
}
|
||||
if (isAdmin) setShowUpload(true);
|
||||
else setShowLogin(true);
|
||||
}
|
||||
|
||||
function handleCollectionSelect(id: number) {
|
||||
setActiveCollectionId(id);
|
||||
setActiveTag(null);
|
||||
setSearch('');
|
||||
setDebouncedSearch('');
|
||||
}
|
||||
|
||||
const activeCollection = collections?.find((c) => c.id === activeCollectionId);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950">
|
||||
{/* Topbar */}
|
||||
<header className="sticky top-0 z-30 bg-zinc-950/80 backdrop-blur-md border-b border-zinc-800/60">
|
||||
<header className="sticky top-0 z-30 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800/60">
|
||||
<div className="max-w-screen-2xl mx-auto px-4 py-3 flex items-center gap-3">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 mr-2 flex-shrink-0">
|
||||
@@ -73,7 +92,7 @@ export function Gallery() {
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
placeholder="Search memes…"
|
||||
placeholder={`Search${activeCollection ? ` in ${activeCollection.name}` : ''}…`}
|
||||
className="w-full bg-zinc-900 border border-zinc-700 rounded-lg pl-8 pr-3 py-1.5 text-sm focus:outline-none focus:border-accent placeholder-zinc-600"
|
||||
/>
|
||||
{search && (
|
||||
@@ -86,19 +105,17 @@ export function Gallery() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right side actions */}
|
||||
{/* Right side */}
|
||||
<div className="ml-auto flex items-center gap-2 flex-shrink-0">
|
||||
{/* Upload button — always visible, gates on auth */}
|
||||
<button
|
||||
onClick={handleUploadClick}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-accent hover:bg-accent-hover text-white text-sm font-medium transition-colors"
|
||||
title={isAdmin ? 'Upload meme' : 'Sign in to upload'}
|
||||
>
|
||||
{isAdmin ? <UploadIcon size={15} /> : <Lock size={15} />}
|
||||
<span className="hidden sm:inline">{isAdmin ? 'Upload' : 'Upload'}</span>
|
||||
<span className="hidden sm:inline">Upload</span>
|
||||
</button>
|
||||
|
||||
{/* Auth state */}
|
||||
{isAdmin ? (
|
||||
<button
|
||||
onClick={() => logout.mutate()}
|
||||
@@ -122,7 +139,7 @@ export function Gallery() {
|
||||
|
||||
{/* Tag filter strip */}
|
||||
{tags && tags.length > 0 && (
|
||||
<div className="max-w-screen-2xl mx-auto px-4 pb-2.5 flex gap-2 overflow-x-auto scrollbar-none">
|
||||
<div className="max-w-screen-2xl mx-auto px-4 pb-2 flex gap-2 overflow-x-auto scrollbar-none">
|
||||
<button
|
||||
onClick={() => setActiveTag(null)}
|
||||
className={`text-xs px-2.5 py-1 rounded-full border whitespace-nowrap transition-colors flex-shrink-0 ${
|
||||
@@ -131,7 +148,7 @@ export function Gallery() {
|
||||
: 'border-zinc-700 text-zinc-400 hover:border-zinc-500 hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
All tags
|
||||
</button>
|
||||
{tags.map((tag) => (
|
||||
<button
|
||||
@@ -150,16 +167,43 @@ export function Gallery() {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Collection bar */}
|
||||
{collections && collections.length > 0 && (
|
||||
<div className="sticky top-[57px] z-20 bg-zinc-950/90 backdrop-blur-md border-b border-zinc-800/40 px-4 py-3">
|
||||
<div className="max-w-screen-2xl mx-auto">
|
||||
<CollectionBar
|
||||
collections={collections}
|
||||
activeId={activeCollectionId}
|
||||
onSelect={handleCollectionSelect}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gallery */}
|
||||
<main className="max-w-screen-2xl mx-auto px-4 py-6">
|
||||
{/* Section heading */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<p className="text-sm text-zinc-500">
|
||||
{isLoading
|
||||
? 'Loading…'
|
||||
: isError
|
||||
? 'Failed to load'
|
||||
: `${data?.total ?? 0} meme${data?.total !== 1 ? 's' : ''}${activeTag ? ` tagged "${activeTag}"` : ''}${debouncedSearch ? ` matching "${debouncedSearch}"` : ''}`}
|
||||
</p>
|
||||
<div>
|
||||
<p className="text-sm text-zinc-500">
|
||||
{isLoading
|
||||
? 'Loading…'
|
||||
: isError
|
||||
? 'Failed to load'
|
||||
: (() => {
|
||||
const count = data?.total ?? 0;
|
||||
const showing = data?.memes.length ?? 0;
|
||||
let label = `${count} meme${count !== 1 ? 's' : ''}`;
|
||||
if (isUnsorted && count > 50 && !debouncedSearch && !activeTag) {
|
||||
label = `Showing last 50 of ${count}`;
|
||||
}
|
||||
if (activeTag) label += ` tagged "${activeTag}"`;
|
||||
if (debouncedSearch) label += ` matching "${debouncedSearch}"`;
|
||||
return label;
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -168,7 +212,7 @@ export function Gallery() {
|
||||
<div
|
||||
key={i}
|
||||
className="break-inside-avoid mb-3 rounded-xl bg-zinc-900 animate-pulse"
|
||||
style={{ height: `${120 + Math.random() * 200}px` }}
|
||||
style={{ height: `${120 + (i * 37) % 200}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -212,8 +256,13 @@ export function Gallery() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload modal (admin only) */}
|
||||
{showUpload && isAdmin && <UploadModal onClose={() => setShowUpload(false)} />}
|
||||
{/* Upload modal */}
|
||||
{showUpload && isAdmin && (
|
||||
<UploadModal
|
||||
onClose={() => setShowUpload(false)}
|
||||
defaultCollectionId={activeCollectionId ?? unsorted?.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Login modal */}
|
||||
{showLogin && (
|
||||
|
||||
Reference in New Issue
Block a user