import { useEffect, useRef, useState } from 'react';
import { uniqueBy } from '@mary/array-fns';
import type { Gif } from '#/lib/gif';
import { useBreakpoints } from '#/lib/hooks/use-breakpoints';
import { useThrottledValue } from '#/lib/hooks/use-debounce';
import { addRecentGif, useRecentGifs } from '#/state/preferences/recent-gifs';
import {
GIF_CATEGORIES,
type GifCategory,
GifCategoryPills,
} from '#/features/gifPicker/components/GifCategoryPills';
import { GifPickerErrorBoundary } from '#/features/gifPicker/components/GifPickerErrorBoundary';
import { type GifPickerGridHandle, GifPickerGrid } from '#/features/gifPicker/components/GifPickerGrid';
import { GifPickerHeader } from '#/features/gifPicker/components/GifPickerHeader';
import { GifPickerPlaceholder } from '#/features/gifPicker/components/GifPickerPlaceholder';
import * as styles from '#/features/gifPicker/GifPickerDialog.css';
import { useGifPickerData } from '#/features/gifPicker/hooks/useGifPickerData';
import * as Dialog from '#/components/Dialog';
import { ErrorBoundary } from '#/components/ErrorBoundary';
export function GifPickerDialog({
handle,
onClose,
onSelectGif: onSelectGifProp,
}: {
handle: Dialog.DialogHandle;
onClose?: () => void;
onSelectGif: (gif: Gif) => void;
}) {
const onSelectGif = (gif: Gif) => {
handle.close();
onSelectGifProp(gif);
};
return (
<Dialog.Root
handle={handle}
onOpenChange={(open) => {
if (!open) {
onClose?.();
}
}}
>
<Dialog.Viewport>
<Dialog.Close variant="outer" />
<Dialog.Card scroll="body" label="GIFs" fullHeight>
<ErrorBoundary
renderError={(error) => <GifPickerErrorBoundary handle={handle} details={String(error)} />}
>
<GifPickerBody handle={handle} onSelectGif={onSelectGif} />
</ErrorBoundary>
</Dialog.Card>
</Dialog.Viewport>
</Dialog.Root>
);
}
function GifPickerBody({
handle,
onSelectGif,
}: {
handle: Dialog.DialogHandle;
onSelectGif: (gif: Gif) => void;
}) {
const { gtMobile } = useBreakpoints();
const inputRef = useRef<HTMLInputElement>(null);
const gridRef = useRef<GifPickerGridHandle>(null);
const [rawSearch, setRawSearch] = useState('');
const [activeCategory, setActiveCategory] = useState<string>('trending');
const search = useThrottledValue(rawSearch, 750);
const recentGifs = useRecentGifs();
// Determine the effective search query:
// - If user is typing, use the throttled text
// - If user clears the input, immediately drop the search (don't wait for
// the throttle to catch up — otherwise the previous query keeps driving
// the visible results until the next interval tick)
// - If a non-trending category is active, use its searchterm
// - Otherwise (trending/recents), empty string triggers the featured endpoint
const activeCategorySearchterm = GIF_CATEGORIES.find((c) => c.id === activeCategory)?.searchterm ?? '';
const effectiveSearch = rawSearch.length > 0 && search.length > 0 ? search : activeCategorySearchterm;
const isRecentsActive = activeCategory === 'recents' && rawSearch.length === 0;
const {
data,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error,
isPending,
isError,
isSearching,
refetch,
} = useGifPickerData(effectiveSearch, { enabled: !isRecentsActive });
const networkItems = uniqueBy(data?.pages.flatMap((page) => page.results) ?? [], (item) => item.id);
const items = isRecentsActive ? recentGifs : networkItems;
const hasData = items.length > 0;
const onEndReached = () => {
if (isRecentsActive) {
return;
}
if (isFetchingNextPage || !hasNextPage || error) {
return;
}
void fetchNextPage();
};
// Scroll to top when the effective query/category changes, NOT on every keystroke.
useEffect(() => {
gridRef.current?.scrollToTop();
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- query and category trigger the reset
}, [effectiveSearch, isRecentsActive]);
const onClearSearch = () => {
setRawSearch('');
setActiveCategory('trending');
inputRef.current?.focus();
};
const onGoBack = () => {
if (isSearching || activeCategory !== 'trending') {
onClearSearch();
} else {
handle.close();
}
};
const onChangeSearch = (text: string) => {
setRawSearch(text);
};
const onSelectCategory = (category: GifCategory) => {
setActiveCategory(category.id);
};
const handleSelectGif = (gif: Gif) => {
addRecentGif(gif);
onSelectGif(gif);
};
const showPills = rawSearch.length === 0;
return (
<>
<div className={styles.header}>
<GifPickerHeader
inputRef={inputRef}
value={rawSearch}
onChangeText={onChangeSearch}
onClear={onClearSearch}
onEscape={() => handle.close()}
/>
{showPills && (
<GifCategoryPills
activeId={activeCategory}
onSelect={onSelectCategory}
hasRecents={recentGifs.length > 0}
/>
)}
</div>
{hasData ? (
<GifPickerGrid
ref={gridRef}
items={items}
numColumns={gtMobile ? 3 : 2}
isFetchingNextPage={!isRecentsActive && isFetchingNextPage}
error={isRecentsActive ? null : error}
fetchNextPage={fetchNextPage}
onEndReached={onEndReached}
onSelectGif={handleSelectGif}
/>
) : (
<div className={styles.placeholder}>
<GifPickerPlaceholder
isLoading={!isRecentsActive && isPending}
isError={!isRecentsActive && isError}
isSearching={isSearching}
isRecentsEmpty={isRecentsActive}
query={effectiveSearch}
onRetry={refetch}
onGoBack={onGoBack}
/>
</div>
)}
</>
);
}