build search global
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
import { Search, Upload as UploadIcon, X, Share2, Lock, LogOut, Settings } from 'lucide-react';
|
import { Search, Upload as UploadIcon, X, Share2, Lock, LogOut, Settings, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
import { useMemes, useTags, useCollections } from '../hooks/useMemes';
|
import { useMemes, useTags, useCollections } from '../hooks/useMemes';
|
||||||
import { useAuth, useLogout } from '../hooks/useAuth';
|
import { useAuth, useLogout } from '../hooks/useAuth';
|
||||||
import { GalleryGrid } from '../components/GalleryGrid';
|
import { GalleryGrid } from '../components/GalleryGrid';
|
||||||
@@ -11,11 +11,14 @@ import { CollectionBar } from '../components/CollectionBar';
|
|||||||
import { SettingsModal } from '../components/SettingsModal';
|
import { SettingsModal } from '../components/SettingsModal';
|
||||||
import type { Meme } from '../api/client';
|
import type { Meme } from '../api/client';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100;
|
||||||
|
|
||||||
export function Gallery() {
|
export function Gallery() {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||||
const [activeTag, setActiveTag] = useState<string | null>(null);
|
const [activeTag, setActiveTag] = useState<string | null>(null);
|
||||||
const [activeCollectionId, setActiveCollectionId] = useState<number | null>(null);
|
const [activeCollectionId, setActiveCollectionId] = useState<number | null>(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
const [selectedMemeId, setSelectedMemeId] = useState<string | null>(null);
|
const [selectedMemeId, setSelectedMemeId] = useState<string | null>(null);
|
||||||
const [quickShareMeme, setQuickShareMeme] = useState<Meme | null>(null);
|
const [quickShareMeme, setQuickShareMeme] = useState<Meme | null>(null);
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
@@ -36,14 +39,11 @@ export function Gallery() {
|
|||||||
}
|
}
|
||||||
}, [unsorted, activeCollectionId]);
|
}, [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
|
// Debounce search
|
||||||
const [searchTimer, setSearchTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
const [searchTimer, setSearchTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
||||||
function handleSearchChange(val: string) {
|
function handleSearchChange(val: string) {
|
||||||
setSearch(val);
|
setSearch(val);
|
||||||
|
setPage(1);
|
||||||
if (searchTimer) clearTimeout(searchTimer);
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
const t = setTimeout(() => setDebouncedSearch(val), 300);
|
const t = setTimeout(() => setDebouncedSearch(val), 300);
|
||||||
setSearchTimer(t);
|
setSearchTimer(t);
|
||||||
@@ -55,9 +55,13 @@ export function Gallery() {
|
|||||||
q: debouncedSearch || undefined,
|
q: debouncedSearch || undefined,
|
||||||
parent_only: true,
|
parent_only: true,
|
||||||
collection_id: isSearching ? undefined : (activeCollectionId ?? undefined),
|
collection_id: isSearching ? undefined : (activeCollectionId ?? undefined),
|
||||||
limit: isSearching ? 200 : limit,
|
page,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const total = data?.total ?? 0;
|
||||||
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
|
||||||
const { data: tags } = useTags();
|
const { data: tags } = useTags();
|
||||||
|
|
||||||
const handleOpen = useCallback((meme: Meme) => setSelectedMemeId(meme.id), []);
|
const handleOpen = useCallback((meme: Meme) => setSelectedMemeId(meme.id), []);
|
||||||
@@ -71,11 +75,20 @@ export function Gallery() {
|
|||||||
function handleCollectionSelect(id: number) {
|
function handleCollectionSelect(id: number) {
|
||||||
setActiveCollectionId(id);
|
setActiveCollectionId(id);
|
||||||
setActiveTag(null);
|
setActiveTag(null);
|
||||||
|
setPage(1);
|
||||||
setSearch('');
|
setSearch('');
|
||||||
setDebouncedSearch('');
|
setDebouncedSearch('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeCollection = collections?.find((c) => c.id === activeCollectionId);
|
function handleTagSelect(tag: string | null) {
|
||||||
|
setActiveTag(tag);
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPage(p: number) {
|
||||||
|
setPage(p);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-950">
|
<div className="min-h-screen bg-zinc-950">
|
||||||
@@ -100,7 +113,7 @@ export function Gallery() {
|
|||||||
/>
|
/>
|
||||||
{search && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSearch(''); setDebouncedSearch(''); }}
|
onClick={() => { setSearch(''); setDebouncedSearch(''); setPage(1); }}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300"
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
@@ -154,7 +167,7 @@ export function Gallery() {
|
|||||||
{tags && tags.length > 0 && (
|
{tags && tags.length > 0 && (
|
||||||
<div className="max-w-screen-2xl mx-auto px-4 pb-2 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
|
<button
|
||||||
onClick={() => setActiveTag(null)}
|
onClick={() => handleTagSelect(null)}
|
||||||
className={`text-xs px-2.5 py-1 rounded-full border whitespace-nowrap transition-colors flex-shrink-0 ${
|
className={`text-xs px-2.5 py-1 rounded-full border whitespace-nowrap transition-colors flex-shrink-0 ${
|
||||||
activeTag === null
|
activeTag === null
|
||||||
? 'bg-accent text-white border-accent'
|
? 'bg-accent text-white border-accent'
|
||||||
@@ -166,7 +179,7 @@ export function Gallery() {
|
|||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<button
|
<button
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
onClick={() => setActiveTag(activeTag === tag.name ? null : tag.name)}
|
onClick={() => handleTagSelect(activeTag === tag.name ? null : tag.name)}
|
||||||
className={`text-xs px-2.5 py-1 rounded-full border whitespace-nowrap transition-colors flex-shrink-0 ${
|
className={`text-xs px-2.5 py-1 rounded-full border whitespace-nowrap transition-colors flex-shrink-0 ${
|
||||||
activeTag === tag.name
|
activeTag === tag.name
|
||||||
? 'bg-accent text-white border-accent'
|
? 'bg-accent text-white border-accent'
|
||||||
@@ -198,25 +211,43 @@ export function Gallery() {
|
|||||||
<main className="max-w-screen-2xl mx-auto px-4 py-6">
|
<main className="max-w-screen-2xl mx-auto px-4 py-6">
|
||||||
{/* Section heading */}
|
{/* Section heading */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<p className="text-sm text-zinc-500">
|
||||||
<p className="text-sm text-zinc-500">
|
{isLoading
|
||||||
{isLoading
|
? 'Loading…'
|
||||||
? 'Loading…'
|
: isError
|
||||||
: isError
|
? 'Failed to load'
|
||||||
? 'Failed to load'
|
: (() => {
|
||||||
: (() => {
|
if (isSearching) {
|
||||||
const count = data?.total ?? 0;
|
return `${total} result${total !== 1 ? 's' : ''} across all folders for "${debouncedSearch}"`;
|
||||||
const showing = data?.memes.length ?? 0;
|
}
|
||||||
let label = `${count} meme${count !== 1 ? 's' : ''}`;
|
let label = `${total} meme${total !== 1 ? 's' : ''}`;
|
||||||
if (isUnsorted && count > 50 && !debouncedSearch && !activeTag) {
|
if (activeTag) label += ` tagged "${activeTag}"`;
|
||||||
label = `Showing last 50 of ${count}`;
|
return label;
|
||||||
}
|
})()}
|
||||||
if (activeTag) label += ` tagged "${activeTag}"`;
|
</p>
|
||||||
if (debouncedSearch) label = `${showing} result${showing !== 1 ? 's' : ''} across all folders for "${debouncedSearch}"`;
|
|
||||||
return label;
|
{/* Pagination controls */}
|
||||||
})()}
|
{totalPages > 1 && !isLoading && (
|
||||||
</p>
|
<div className="flex items-center gap-1">
|
||||||
</div>
|
<button
|
||||||
|
onClick={() => goToPage(page - 1)}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={16} />
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-zinc-500 px-2 tabular-nums">
|
||||||
|
{page} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => goToPage(page + 1)}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
className="p-1.5 rounded-lg text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -238,6 +269,29 @@ export function Gallery() {
|
|||||||
onShare={handleShare}
|
onShare={handleShare}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bottom pagination */}
|
||||||
|
{totalPages > 1 && !isLoading && !isError && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-8">
|
||||||
|
<button
|
||||||
|
onClick={() => goToPage(page - 1)}
|
||||||
|
disabled={page === 1}
|
||||||
|
className="flex items-center gap-1 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={15} /> Prev
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-zinc-500 tabular-nums">
|
||||||
|
Page {page} of {totalPages}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => goToPage(page + 1)}
|
||||||
|
disabled={page === totalPages}
|
||||||
|
className="flex items-center gap-1 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
Next <ChevronRight size={15} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Meme detail modal */}
|
{/* Meme detail modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user