7f37f16c7b
- Upgrade react, react-dom to ^19.2.4 and @types/react, @types/react-dom to v19 - Upgrade @nostrify/react to 0.4.0 (peer deps fix for React 19) - Upgrade vaul to 1.1.2 and react-day-picker to 9.14.0 for React 19 compatibility - Fix useRef() calls to pass explicit initial values (required in React 19) - Update RefObject types to include null (React 19 type change) - Rewrite Calendar component for react-day-picker v9 classNames API - Add npm overrides for @emoji-mart/react (only remaining React 18 holdout)
151 lines
4.1 KiB
TypeScript
151 lines
4.1 KiB
TypeScript
import { useState, useCallback, useRef } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
const TENOR_API_KEY = 'AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ'; // Tenor public API key
|
|
const TENOR_BASE_URL = 'https://tenor.googleapis.com/v2';
|
|
const RESULTS_LIMIT = 30;
|
|
|
|
interface TenorMediaFormat {
|
|
url: string;
|
|
dims: [number, number];
|
|
duration: number;
|
|
size: number;
|
|
}
|
|
|
|
interface TenorResult {
|
|
id: string;
|
|
title: string;
|
|
media_formats: {
|
|
gif?: TenorMediaFormat;
|
|
tinygif?: TenorMediaFormat;
|
|
nanogif?: TenorMediaFormat;
|
|
mediumgif?: TenorMediaFormat;
|
|
gifpreview?: TenorMediaFormat;
|
|
tinygifpreview?: TenorMediaFormat;
|
|
};
|
|
content_description: string;
|
|
created: number;
|
|
url: string;
|
|
}
|
|
|
|
interface TenorResponse {
|
|
results: TenorResult[];
|
|
next: string;
|
|
}
|
|
|
|
export interface GifResult {
|
|
id: string;
|
|
title: string;
|
|
/** URL for the full-size GIF */
|
|
url: string;
|
|
/** URL for a smaller preview thumbnail */
|
|
previewUrl: string;
|
|
/** Width of the preview */
|
|
width: number;
|
|
/** Height of the preview */
|
|
height: number;
|
|
}
|
|
|
|
function mapTenorResult(result: TenorResult): GifResult {
|
|
const gif = result.media_formats.gif ?? result.media_formats.mediumgif;
|
|
const preview = result.media_formats.tinygif ?? result.media_formats.nanogif;
|
|
|
|
return {
|
|
id: result.id,
|
|
title: result.content_description || result.title,
|
|
url: gif?.url ?? preview?.url ?? '',
|
|
previewUrl: preview?.url ?? gif?.url ?? '',
|
|
width: preview?.dims[0] ?? gif?.dims[0] ?? 220,
|
|
height: preview?.dims[1] ?? gif?.dims[1] ?? 160,
|
|
};
|
|
}
|
|
|
|
async function fetchTenorSearch(query: string, pos?: string): Promise<{ results: GifResult[]; next: string }> {
|
|
const params = new URLSearchParams({
|
|
key: TENOR_API_KEY,
|
|
q: query,
|
|
limit: String(RESULTS_LIMIT),
|
|
media_filter: 'gif,tinygif',
|
|
contentfilter: 'medium',
|
|
client_key: 'ditto_nostr',
|
|
});
|
|
if (pos) params.set('pos', pos);
|
|
|
|
const res = await fetch(`${TENOR_BASE_URL}/search?${params}`);
|
|
if (!res.ok) throw new Error(`Tenor search failed: ${res.status}`);
|
|
|
|
const data: TenorResponse = await res.json();
|
|
return {
|
|
results: data.results.map(mapTenorResult).filter((g) => g.url),
|
|
next: data.next,
|
|
};
|
|
}
|
|
|
|
async function fetchTenorTrending(pos?: string): Promise<{ results: GifResult[]; next: string }> {
|
|
const params = new URLSearchParams({
|
|
key: TENOR_API_KEY,
|
|
limit: String(RESULTS_LIMIT),
|
|
media_filter: 'gif,tinygif',
|
|
contentfilter: 'medium',
|
|
client_key: 'ditto_nostr',
|
|
});
|
|
if (pos) params.set('pos', pos);
|
|
|
|
const res = await fetch(`${TENOR_BASE_URL}/featured?${params}`);
|
|
if (!res.ok) throw new Error(`Tenor featured failed: ${res.status}`);
|
|
|
|
const data: TenorResponse = await res.json();
|
|
return {
|
|
results: data.results.map(mapTenorResult).filter((g) => g.url),
|
|
next: data.next,
|
|
};
|
|
}
|
|
|
|
export function useGifSearch() {
|
|
const [query, setQuery] = useState('');
|
|
const [debouncedQuery, setDebouncedQuery] = useState('');
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
|
|
const handleQueryChange = useCallback((value: string) => {
|
|
setQuery(value);
|
|
clearTimeout(debounceRef.current);
|
|
debounceRef.current = setTimeout(() => {
|
|
setDebouncedQuery(value.trim());
|
|
}, 300);
|
|
}, []);
|
|
|
|
const clearQuery = useCallback(() => {
|
|
setQuery('');
|
|
setDebouncedQuery('');
|
|
clearTimeout(debounceRef.current);
|
|
}, []);
|
|
|
|
const isSearching = debouncedQuery.length > 0;
|
|
|
|
const trendingQuery = useQuery({
|
|
queryKey: ['tenor', 'trending'],
|
|
queryFn: () => fetchTenorTrending(),
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
enabled: !isSearching,
|
|
});
|
|
|
|
const searchQuery = useQuery({
|
|
queryKey: ['tenor', 'search', debouncedQuery],
|
|
queryFn: () => fetchTenorSearch(debouncedQuery),
|
|
staleTime: 2 * 60 * 1000, // 2 minutes
|
|
enabled: isSearching,
|
|
});
|
|
|
|
const activeQuery = isSearching ? searchQuery : trendingQuery;
|
|
|
|
return {
|
|
query,
|
|
setQuery: handleQueryChange,
|
|
clearQuery,
|
|
results: activeQuery.data?.results ?? [],
|
|
isLoading: activeQuery.isLoading,
|
|
isError: activeQuery.isError,
|
|
isSearching,
|
|
};
|
|
}
|