import * as React from "react"; import PQueue from "p-queue"; import { useState, useCallback, useContext, useEffect, useRef, useMemo } from "@wordpress/element"; import type { Title, TitleIdType, TitleObjectType } from "../../types"; import JustwatchContext from "../../context/context"; import { AutoComplete, FetchSuggestionsOptions } from "../auto-complete/AutoComplete"; import { FetchResult, fetchTitleById, fetchTitlesByQuery } from "../../context/api"; import { isImdbId, getIdMatch, getId } from "../../context/api"; import { TitleInfo } from "../title-info/TitleInfo"; import { StyledSelectControl } from "./TitleSelect.styles"; import config from "../../context/config"; import { __, sprintf } from "@wordpress/i18n"; export type TitleIdDisplayType = TitleIdType | "abc"; export type TitleSelectProps = { inputRef?: React.RefObject; selectedTitle?: T; label?: string; help?: React.JSX.Element; idType?: TitleIdType; highlight?: boolean; onTitleSelect?: (title: T) => void; onTitleData?: (title: T) => void; }; type FilterOption = { label: string; value: string; } const filterOptions: FilterOption[] = [ { value: "movie", label: "MOVIE" }, { value: "show", label: "SHOW" }, { value: "all", label: "ALL" }, ].map(option => ({ ...option, value: option.value.toString() })); type FilterType = "all" | "movie" | "show"; export const TitleSelect = ({ selectedTitle: initialSelectedTitle, inputRef: inputRefProp, help, label, onTitleSelect, onTitleData, }: TitleSelectProps): JSX.Element => { const inputRef = inputRefProp || useRef(null); const [activeFilter, setActiveFilter] = useState("all"); const [selectedTitle, setSelectedTitle] = useState(initialSelectedTitle || null); const { apiKey, apiUrl } = useContext(JustwatchContext); const { locale, country } = config; const queue = useMemo(() => new PQueue({ concurrency: 1, intervalCap: 1, interval: 1000 }), []); const valueProp = 'title'; const [changed, setChanged] = useState(false); const [isFetched, setFetched] = useState(false); const fetchSelectedTitle = useCallback(async() => { if (initialSelectedTitle) { const justwatchId = Number(getId(initialSelectedTitle, "justwatch")); const objectType = initialSelectedTitle.objectType; const isDifferentTitle = justwatchId !== selectedTitle?.justwatchId || objectType !== selectedTitle.objectType; const isPartialTitle = !selectedTitle?.title || !selectedTitle?.originalReleaseYear || selectedTitle.objectType === 'show' && !selectedTitle?.seasons; const needsUpdate = !isFetched && (isDifferentTitle || isPartialTitle); if (needsUpdate) { setFetched(true); const item = await fetchTitleById(justwatchId, objectType, { apiKey, apiUrl }); if (item) { onTitleData?.(item as T); setSelectedTitle((prev) => { const isDifferentTitle = prev && ( prev.justwatchId !== item.justwatchId || prev.objectType !== item.objectType); if (isDifferentTitle || isPartialTitle) { return item as T; } return prev; }); } } } }, [initialSelectedTitle, selectedTitle]); useEffect(() => { fetchSelectedTitle(); }, [fetchSelectedTitle]); useEffect(() => { if (selectedTitle) { setActiveFilter(selectedTitle.objectType); setChanged(false); } }, [selectedTitle]); const fetchSuggestions = useCallback(async( query: string, options: FetchSuggestionsOptions = {} ): Promise<{ items: T[], totalResults: number }> => { queue.clear(); const { page, signal } = { ...options, } as { page?: number; signal?: AbortSignal }; const objectType = activeFilter === "all" ? undefined : activeFilter; if (!query.trim()) return Promise.resolve({ items: [], totalResults: 0 }); const tasks: (() => Promise)[] = []; const idType = isImdbId(query) ? "imdb" : undefined; const fetchOptions = { apiKey, page, objectType: objectType as TitleObjectType, apiUrl }; if (idType) { ["movie", "show"].forEach((objectType) => { tasks.push( () => fetchTitleById(query, objectType as TitleObjectType, { idType, ...fetchOptions }) .catch((error) => { if (error.status !== 404) { throw error; } return null; }) ); }); } else { tasks.push(() => fetchTitlesByQuery(query, fetchOptions)); } const results = await queue.addAll(tasks, { signal }) as Array; const mergedResults = results.reduce((acc, result) => { if (!result) return acc; if ("items" in result) { if (Array.isArray(result.items)) { acc.items.push(...result.items); } acc.totalResults += typeof result.totalResults === "number" ? result.totalResults : 0; } else { acc.items.push(result as T); acc.totalResults++; } return acc; }, { items: [], totalResults: 0, page: 1, totalPages: 1 }); if (idType) { mergedResults.items = mergedResults.items .filter((item: T) => getId(item, idType) === query) as T[]; } // Filter duplicates mergedResults.items = (mergedResults.items.filter((item, index, self) => item === self.find((t) => t.justwatchId === item.justwatchId && t.objectType === item.objectType ) )) as T[]; return mergedResults; } , [activeFilter, apiKey, apiUrl]); const handleSelect = (item: T) => { setSelectedTitle(item); onTitleSelect?.(item); }; const renderSuggestion = (item: T, query: string) => { const idMatch = getIdMatch(item, query); return ( ); } const handleFilterChange = (activeFilter: string | number) => { setActiveFilter(activeFilter as FilterType); }; useEffect(() => { if (changed) { setActiveFilter("all"); } }, [changed]); const suffix = ( handleFilterChange(value)} style={{ width: '60px', textAlign: 'right', marginBottom: 0 }} className="jw-title-select-filter" > {filterOptions.map((option) => ( ))} ); const keyProvider = (item: T) => `${item.justwatchId}:${item.objectType}`; const handleBlur = () => { if (selectedTitle) { setActiveFilter(selectedTitle.objectType); } }; const handleInput = () => { setChanged(true); }; const noResultsMessage = country?.country && country?.fullLocale === locale ? __(sprintf("No results", country?.country), "justwatch-widgets") // (%s) : __("No results", "justwatch-widgets"); return ( ); };