/** * Content Picker Modal * * A modal for browsing and selecting content items to add to menus. * Uses cursor pagination to allow browsing beyond the initial page. */ import { Button, Dialog, Input, Loader, Select } from "@cloudflare/kumo"; import { useLingui } from "@lingui/react/macro"; import { MagnifyingGlass, FolderOpen, X } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import * as React from "react"; import { fetchCollections, fetchContentList, getDraftStatus } from "../lib/api"; import type { ContentItem } from "../lib/api"; import { useDebouncedValue } from "../lib/hooks"; import { cn } from "../lib/utils"; interface ContentPickerModalProps { open: boolean; onOpenChange: (open: boolean) => void; onSelect: (item: { collection: string; id: string; title: string }) => void; } function getItemTitle(item: { data: Record; slug: string | null; id: string }) { const rawTitle = item.data.title; const rawName = item.data.name; return ( (typeof rawTitle === "string" ? rawTitle : "") || (typeof rawName === "string" ? rawName : "") || item.slug || item.id ); } export function ContentPickerModal({ open, onOpenChange, onSelect }: ContentPickerModalProps) { const { t } = useLingui(); const [searchQuery, setSearchQuery] = React.useState(""); const debouncedSearch = useDebouncedValue(searchQuery, 300); const [selectedCollection, setSelectedCollection] = React.useState(""); const [allItems, setAllItems] = React.useState([]); const [nextCursor, setNextCursor] = React.useState(); const [isLoadingMore, setIsLoadingMore] = React.useState(false); const { data: collections = [] } = useQuery({ queryKey: ["collections"], queryFn: fetchCollections, enabled: open, }); // Default to first collection when collections load React.useEffect(() => { if (collections.length > 0 && !selectedCollection) { setSelectedCollection(collections[0]!.slug); } }, [collections, selectedCollection]); const { data: contentResult, isLoading: contentLoading } = useQuery({ queryKey: ["content-picker", selectedCollection, { limit: 50 }], queryFn: () => fetchContentList(selectedCollection, { limit: 50 }), enabled: open && !!selectedCollection, }); // Sync initial page into accumulated items React.useEffect(() => { if (contentResult) { setAllItems(contentResult.items); setNextCursor(contentResult.nextCursor); } }, [contentResult]); const handleLoadMore = async () => { if (!nextCursor || isLoadingMore) return; setIsLoadingMore(true); try { const result = await fetchContentList(selectedCollection, { limit: 50, cursor: nextCursor, }); setAllItems((prev) => [...prev, ...result.items]); setNextCursor(result.nextCursor); } finally { setIsLoadingMore(false); } }; const filteredItems = React.useMemo(() => { if (!debouncedSearch) return allItems; const query = debouncedSearch.toLowerCase(); return allItems.filter((item) => getItemTitle(item).toLowerCase().includes(query)); }, [allItems, debouncedSearch]); // Reset state when modal opens or collection changes React.useEffect(() => { if (open) { setSearchQuery(""); setSelectedCollection(""); setAllItems([]); setNextCursor(undefined); } }, [open]); const handleSelect = (item: ContentItem) => { onSelect({ collection: selectedCollection, id: item.id, title: getItemTitle(item), }); onOpenChange(false); }; return (
{t`Select Content`} ( )} />
{/* Search and collection filter */}
setSearchQuery(e.target.value)} className="ps-10" autoFocus />