From 77264982188067fbd6eae52a1a303cdc7b14c552 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sat, 16 May 2026 18:05:45 +0200 Subject: [PATCH] feat(desktop): GIF picker popover (Tenor + trending/search/recent) --- apps/desktop/src/components/GifPicker.tsx | 126 ++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 apps/desktop/src/components/GifPicker.tsx diff --git a/apps/desktop/src/components/GifPicker.tsx b/apps/desktop/src/components/GifPicker.tsx new file mode 100644 index 0000000..ef11de0 --- /dev/null +++ b/apps/desktop/src/components/GifPicker.tsx @@ -0,0 +1,126 @@ +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('trending'); + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( +
+
+
+ {(['trending', 'search', 'recent'] as Tab[]).map((t) => ( + + ))} +
+ +
+ {tab === 'search' && ( +
+ 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" + /> +
+ )} +
+ {loading && ( +
+ +
+ )} + {error && ( +

+ {error === 'tenor_api_key_missing' + ? 'GIFs sind nicht konfiguriert (VITE_TENOR_API_KEY fehlt).' + : 'GIFs gerade nicht verfügbar.'} +

+ )} + {!loading && !error && visible.length === 0 && ( +

+ {tab === 'recent' ? 'Noch keine zuletzt verwendeten GIFs.' : 'Keine Treffer.'} +

+ )} + {!loading && !error && visible.map((g) => ( + + ))} +
+
+ ); +}