Files
dumpsterChat/web/src/components/GiphyPicker.tsx
T

155 lines
4.0 KiB
TypeScript

import { useEffect, useRef, useState, useCallback } from 'react';
import { api } from '../lib/api';
interface GifImage {
url: string;
}
interface Gif {
id: string;
title: string;
url: string;
images: {
fixed_height: GifImage;
original: GifImage;
};
username: string;
}
interface GiphyPickerProps {
onSelect: (gif: Gif) => void;
onClose: () => void;
}
export function GiphyPicker({ onSelect, onClose }: GiphyPickerProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Gif[]>([]);
const [loading, setLoading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
// Fetch trending on mount
useEffect(() => {
const fetchTrending = async () => {
setLoading(true);
try {
const gifs = await api.get<Gif[]>('/gifs/trending');
setResults(gifs);
} catch {
setResults([]);
} finally {
setLoading(false);
}
};
fetchTrending();
inputRef.current?.focus();
}, []);
// Debounced search
useEffect(() => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
if (!query.trim()) {
// If query cleared, reload trending
const fetchTrending = async () => {
setLoading(true);
try {
const gifs = await api.get<Gif[]>('/gifs/trending');
setResults(gifs);
} catch {
setResults([]);
} finally {
setLoading(false);
}
};
fetchTrending();
return;
}
debounceRef.current = setTimeout(async () => {
setLoading(true);
try {
const gifs = await api.get<Gif[]>(
`/gifs/search?q=${encodeURIComponent(query.trim())}`
);
setResults(gifs);
} catch {
setResults([]);
} finally {
setLoading(false);
}
}, 300);
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, [query]);
const handleClick = useCallback(
(gif: Gif) => {
onSelect(gif);
},
[onSelect]
);
return (
<div className="border border-gb-bg-t bg-gb-bg rounded font-mono text-sm flex flex-col h-72 w-full">
{/* Header with search */}
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-gb-bg-t">
<span className="text-gb-fg-f select-none">{'>'} search:</span>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="_"
className="bg-transparent text-gb-fg outline-none flex-1 caret-gb-orange"
/>
<button
type="button"
onClick={onClose}
className="text-gb-fg-f hover:text-gb-orange select-none ml-1"
title="Close"
>
[x]
</button>
</div>
{/* Results grid */}
<div className="flex-1 overflow-y-auto p-2">
{loading && (
<p className="text-gb-fg-f text-center py-2">[loading...]</p>
)}
{!loading && results.length === 0 && (
<p className="text-gb-fg-f text-center py-2">[no results]</p>
)}
{!loading && results.length > 0 && (
<div className="columns-3 gap-1 space-y-1">
{results.map((gif) => (
<button
key={gif.id}
type="button"
onClick={() => handleClick(gif)}
className="cursor-pointer border border-transparent hover:border-gb-orange rounded overflow-hidden focus:outline-none focus:border-gb-orange w-full break-inside-avoid block"
>
<img
src={`/api/v1/gifs/proxy?url=${encodeURIComponent(gif.images.fixed_height.url)}`}
alt={gif.title || 'GIF'}
className="w-full h-auto block"
loading="lazy"
/>
</button>
))}
</div>
)}
</div>
</div>
);
}
export type { Gif };