155 lines
4.0 KiB
TypeScript
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 };
|