"use client" import { useState, useRef, useEffect, useCallback } from "react" import { Search, Loader2, ImageIcon } from "lucide-react" interface GifResult { id: string title: string url: string previewUrl: string previewHeight: number width: number height: number } interface GifPickerProps { onSelect: (gif: { url: string; previewUrl: string; title: string }) => void onClose: () => void } const SEARCH_CACHE = new Map() export function GifPicker({ onSelect, onClose }: GifPickerProps) { const [gifs, setGifs] = useState([]) const [loading, setLoading] = useState(true) const [loadingMore, setLoadingMore] = useState(false) const [search, setSearch] = useState("") const [offset, setOffset] = useState(0) const [hasMore, setHasMore] = useState(true) const [error, setError] = useState("") const sentinelRef = useRef(null) const inputRef = useRef(null) const searchTimeoutRef = useRef>() const fetchGifs = useCallback(async (q: string, off: number, append: boolean) => { if (off === 0) { const cacheKey = q || "__trending__" const cached = SEARCH_CACHE.get(cacheKey) if (cached) { setGifs(cached) setLoading(false) setHasMore(true) setOffset(cached.length) return } } try { if (!append) setLoading(true) else setLoadingMore(true) setError("") const params = new URLSearchParams() if (q) { params.set("type", "search") params.set("q", q) } params.set("offset", String(off)) params.set("limit", "20") const res = await fetch(`/api/ai/giphy?${params}`) if (!res.ok) { const data = await res.json() throw new Error(data.error || "Failed to fetch GIFs") } const data = await res.json() const newGifs: GifResult[] = data.gifs || [] if (append) { setGifs((prev) => [...prev, ...newGifs]) } else { setGifs(newGifs) if (off === 0 && !q) { SEARCH_CACHE.set("__trending__", newGifs) } if (q) { SEARCH_CACHE.set(q, newGifs) } } setOffset(off + newGifs.length) setHasMore(newGifs.length === 20) } catch (err: any) { setError(err.message || "Failed to load GIFs") } finally { setLoading(false) setLoadingMore(false) } }, []) useEffect(() => { fetchGifs("", 0, false) }, [fetchGifs]) useEffect(() => { inputRef.current?.focus() }, []) useEffect(() => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current) if (!search.trim()) { fetchGifs("", 0, false) return } searchTimeoutRef.current = setTimeout(() => { fetchGifs(search.trim(), 0, false) }, 400) return () => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current) } }, [search, fetchGifs]) useEffect(() => { const sentinel = sentinelRef.current if (!sentinel) return const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) { fetchGifs(search.trim(), offset, true) } }, { rootMargin: "200px" } ) observer.observe(sentinel) return () => observer.disconnect() }, [hasMore, loadingMore, loading, offset, search, fetchGifs]) return (
setSearch(e.target.value)} placeholder="Search GIFs..." className="w-full bg-muted/50 text-foreground text-sm rounded-xl pl-9 pr-4 py-2.5 outline-none focus:ring-2 focus:ring-primary/30 placeholder:text-muted-foreground/60" />
{loading ? (
) : error ? (

{error}

) : gifs.length === 0 ? (

No GIFs found.

) : ( <>
{gifs.map((gif) => ( ))}
{loadingMore && (
)} {hasMore &&
} )}
) }