ef1f9f45d8
Google closed Tenor v2 to new API clients in Jan 2026, so the only people who could use the picker were those with a pre-existing Google Cloud Console key. Swapped to GIPHY's Developer API (still open, free keys at https://developers.giphy.com/dashboard/). - Env var renamed VITE_TENOR_API_KEY → VITE_GIPHY_API_KEY - Endpoint, response mapping, error sentinel updated - File still named tenor.ts for import-path stability — renaming later if it bothers anyone - Public GifResult interface unchanged so the picker UI didn't need edits beyond the error-message switch
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
import {
|
|
featuredGifs,
|
|
getRecentGifs,
|
|
type GifResult,
|
|
rememberRecentGif,
|
|
searchGifs,
|
|
} from '../lib/tenor';
|
|
import { SpinnerIcon, XIcon } from './icons';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onPick: (gif: GifResult) => void;
|
|
}
|
|
|
|
type Tab = 'trending' | 'search' | 'recent';
|
|
|
|
export function GifPicker({ open, onClose, onPick }: Props) {
|
|
const [tab, setTab] = useState<Tab>('trending');
|
|
const [query, setQuery] = useState('');
|
|
const [results, setResults] = useState<GifResult[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const recent = useMemo(() => getRecentGifs(), [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
setError(null);
|
|
const run = async () => {
|
|
try {
|
|
const gifs = tab === 'search' && query.trim().length > 0
|
|
? await searchGifs(query)
|
|
: tab === 'trending'
|
|
? await featuredGifs()
|
|
: [];
|
|
if (!cancelled) setResults(gifs);
|
|
} catch (err) {
|
|
if (!cancelled) setError(err instanceof Error ? err.message : 'GIFs gerade nicht verfügbar');
|
|
} finally {
|
|
if (!cancelled) setLoading(false);
|
|
}
|
|
};
|
|
void run();
|
|
return () => { cancelled = true; };
|
|
}, [open, tab, query]);
|
|
|
|
if (!open) return null;
|
|
|
|
const visible = tab === 'recent' ? recent : results;
|
|
|
|
return (
|
|
<div className="absolute bottom-full left-0 z-30 mb-2 w-[440px] rounded-2xl border border-line bg-surface-2 shadow-xl">
|
|
<header className="flex items-center justify-between border-b border-line px-3 py-2">
|
|
<div className="flex items-center gap-1">
|
|
{(['trending', 'search', 'recent'] as Tab[]).map((t) => (
|
|
<button
|
|
key={t}
|
|
type="button"
|
|
onClick={() => setTab(t)}
|
|
className={
|
|
'cursor-pointer rounded-md px-2 py-1 text-xs font-medium transition ' +
|
|
(t === tab ? 'bg-accent/15 text-accent' : 'text-fg-muted hover:text-fg')
|
|
}
|
|
>
|
|
{t === 'trending' ? 'Trending' : t === 'search' ? 'Suche' : 'Zuletzt'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Schließen"
|
|
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-fg-muted hover:bg-surface-3 hover:text-fg"
|
|
>
|
|
<XIcon className="h-3.5 w-3.5" />
|
|
</button>
|
|
</header>
|
|
{tab === 'search' && (
|
|
<div className="border-b border-line px-3 py-2">
|
|
<input
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
placeholder="GIFs suchen…"
|
|
className="w-full rounded-md border border-line bg-surface-3 px-2 py-1.5 text-sm text-fg placeholder-fg-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/40"
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="grid max-h-[360px] grid-cols-3 gap-1.5 overflow-y-auto p-2">
|
|
{loading && (
|
|
<div className="col-span-3 flex justify-center py-6 text-fg-muted">
|
|
<SpinnerIcon className="h-5 w-5" />
|
|
</div>
|
|
)}
|
|
{error && (
|
|
<p className="col-span-3 px-2 py-4 text-center text-xs text-rose-300">
|
|
{error === 'giphy_api_key_missing'
|
|
? 'GIFs sind nicht konfiguriert (VITE_GIPHY_API_KEY fehlt).'
|
|
: 'GIFs gerade nicht verfügbar.'}
|
|
</p>
|
|
)}
|
|
{!loading && !error && visible.length === 0 && (
|
|
<p className="col-span-3 px-2 py-6 text-center text-xs text-fg-muted">
|
|
{tab === 'recent' ? 'Noch keine zuletzt verwendeten GIFs.' : 'Keine Treffer.'}
|
|
</p>
|
|
)}
|
|
{!loading && !error && visible.map((g) => (
|
|
<button
|
|
key={g.id}
|
|
type="button"
|
|
onClick={() => { rememberRecentGif(g); onPick(g); onClose(); }}
|
|
className="overflow-hidden rounded-md border border-line bg-surface-3 hover:border-accent/40"
|
|
title={g.description}
|
|
>
|
|
<img src={g.previewUrl} alt={g.description || 'GIF'} className="h-24 w-full object-cover" loading="lazy" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|