import { CollectionSearchableSelect } from "@/components/CollectionSearchableSelect"; import LibraryMultiSelect from "@/components/LibraryMultiSelect"; import { useAllUserCollections } from "@/hooks/queries/useAllUserCollections"; import { useAvailableUserLibraries } from "@/hooks/queries/libraries"; import type { RecipeDefinition } from "@/lib/recipes"; export interface RecipeParamFieldsProps { def: RecipeDefinition; params: Record; onChange: (next: Record) => void; } export default function RecipeParamFields({ def, params, onChange }: RecipeParamFieldsProps) { if (def.type === "collection") { return ; } if (def.type === "continue_watching") { return ; } if (def.type === "watchlist" || def.type === "favorites") { return ; } if (def.type === "seasonal_themed") { return ; } if (def.type === "editorial_spotlight") { const subjectType = (params.subject_type as string) ?? "director"; const autoRotate = (params.auto_rotate as boolean) ?? true; const cadence = (params.rotation_cadence as string) ?? "weekly"; const subject = (params.subject as string) ?? ""; return (
{autoRotate ? ( ) : ( )}
); } if (def.type === "because_you_watched") { const anchor = (params.anchor_item_id as string) ?? ""; return (
onChange({ ...params, anchor_item_id: e.target.value })} />
Leave blank to auto-pick the most recent watch.
); } if (def.type === "taste_match") { const genre = (params.genre as string) ?? ""; return (
onChange({ ...params, genre: e.target.value })} />
); } return null; } interface ParamFieldProps { params: Record; onChange: (next: Record) => void; } // Sort choices for watchlist/favorites sections. The empty value keeps the // list's stored order (provider sync order, then newest-added first). const PERSONAL_LIST_SORT_OPTIONS = [ { value: "", label: "List order (default)" }, { value: "added_at:desc", label: "Date added (newest first)" }, { value: "added_at:asc", label: "Date added (oldest first)" }, { value: "title:asc", label: "Title (A–Z)" }, { value: "title:desc", label: "Title (Z–A)" }, { value: "release_date:desc", label: "Release date (newest first)" }, { value: "release_date:asc", label: "Release date (oldest first)" }, { value: "rating_imdb:desc", label: "IMDb rating (highest first)" }, ]; // PersonalListFilterFields edits the optional filter_type / filter_library_ids // filters and sort for watchlist and favorites sections, e.g. a "Movies // watchlist" rail sorted by release date. function PersonalListFilterFields({ params, onChange }: ParamFieldProps) { const { data: libraries } = useAvailableUserLibraries(); const filterType = typeof params.filter_type === "string" ? params.filter_type : ""; const libraryIds = Array.isArray(params.filter_library_ids) ? params.filter_library_ids.filter((id): id is number => typeof id === "number") : []; const sortField = typeof params.sort === "string" ? params.sort : ""; const sortOrder = typeof params.order === "string" && params.order ? params.order : sortField === "title" ? "asc" // mirror the backend's per-field default order : "desc"; const sortValue = sortField ? `${sortField}:${sortOrder}` : ""; return (
); } function ContinueTypeParamField({ params, onChange }: ParamFieldProps) { const continueType = params.continue_type === "listening" ? "listening" : "watching"; return ( ); } // Order matches SeasonalThemeOrder in the backend. Higher entries take // priority when multiple enabled themes match the current date. const SEASONAL_THEMES: Array<{ key: string; label: string; window: string; icon: string }> = [ { key: "valentines", label: "Valentine's Day", window: "Feb 7–14", icon: "šŸ’" }, { key: "st_patricks", label: "St. Patrick's Day", window: "Mar 15–17", icon: "šŸ€" }, { key: "thanksgiving", label: "Thanksgiving", window: "Nov 22–30", icon: "🦃" }, { key: "christmas", label: "Christmas", window: "Dec 1–31", icon: "šŸŽ„" }, { key: "halloween", label: "Halloween", window: "All October", icon: "šŸŽƒ" }, { key: "saturday_morning", label: "Saturday Morning Cartoons", window: "Saturday before 1pm", icon: "šŸ“ŗ", }, { key: "summer_blockbuster", label: "Summer Blockbusters", window: "June – August", icon: "🌓", }, ]; function SeasonalParamField({ params, onChange }: ParamFieldProps) { // Resolve the current enabled set, falling back to the legacy single-theme // shape when the section was saved before EnabledThemes existed. const rawEnabled = params.enabled_themes; const enabled = new Set( Array.isArray(rawEnabled) ? rawEnabled.filter((v): v is string => typeof v === "string") : typeof params.theme === "string" && params.theme ? [params.theme] : [], ); // theme_titles is an optional map of theme key → custom display name. const rawTitles = params.theme_titles; const themeTitles: Record = {}; if (rawTitles && typeof rawTitles === "object" && !Array.isArray(rawTitles)) { for (const [k, v] of Object.entries(rawTitles)) { if (typeof v === "string") themeTitles[k] = v; } } function commit(nextEnabled: Set, nextTitles: Record) { // Drop empty/whitespace-only titles and titles for disabled themes so the // saved config stays tidy. const cleanedTitles: Record = {}; for (const [k, v] of Object.entries(nextTitles)) { if (!nextEnabled.has(k)) continue; const trimmed = v.trim(); if (trimmed) cleanedTitles[k] = trimmed; } onChange({ ...params, enabled_themes: Array.from(nextEnabled), theme_titles: Object.keys(cleanedTitles).length > 0 ? cleanedTitles : undefined, // Clear legacy fields so they don't shadow multi-theme resolution. theme: "", mode: "", }); } function toggle(key: string, on: boolean) { const next = new Set(enabled); if (on) next.add(key); else next.delete(key); commit(next, themeTitles); } function setTitle(key: string, value: string) { commit(enabled, { ...themeTitles, [key]: value }); } return (
Holidays to celebrate
{SEASONAL_THEMES.map((t) => { const isOn = enabled.has(t.key); return (
{isOn && ( setTitle(t.key, e.target.value)} placeholder={`Section title in season — defaults to "${t.label}"`} className="ml-7 w-[calc(100%-1.75rem)] rounded border border-white/10 bg-white/5 px-2 py-1 text-xs" /> )}
); })}

The section auto-cycles: it shows whichever enabled holiday is currently in season, and hides itself when none match. Per-holiday titles override the section name only while that holiday is active.

); } function CollectionParamField({ params, onChange }: ParamFieldProps) { const { collections, isLoading } = useAllUserCollections(); const libraryID = (params.library_collection_id as string) ?? ""; const userID = (params.user_collection_id as string) ?? ""; const value = userID || libraryID; const sourceProvider = typeof params.source_provider === "string" ? params.source_provider : ""; const sourcePreset = typeof params.source_preset === "string" ? params.source_preset : ""; const mediaType = typeof params.media_type === "string" ? params.media_type : ""; const isTraktPreset = sourceProvider === "trakt"; const isAutoBackedTraktPreset = isTraktPreset && (sourcePreset === "trending" || sourcePreset === "popular"); const collectionOptions = isTraktPreset ? collections.filter((collection) => { if (collection.source !== "library" || collection.collection_type !== "trakt") { return false; } const sourceConfig = collection.source_config; if (!sourceConfig || typeof sourceConfig !== "object" || Array.isArray(sourceConfig)) { return false; } return sourceConfig.preset === sourcePreset && sourceConfig.media_type === mediaType; }) : collections; if (isAutoBackedTraktPreset && !value) { return (

A synced Trakt {sourcePreset} {mediaType === "tv" ? "shows" : "movies"} collection will be created automatically.

); } return (
Collection { // Pick the right param key based on the chosen collection's source. // Clearing the selection wipes both fields. if (!next) { onChange({ ...params, library_collection_id: "", user_collection_id: "" }); return; } const picked = collectionOptions.find((c) => c.id === next); if (picked?.source === "user") { onChange({ ...params, library_collection_id: "", user_collection_id: next }); } else { onChange({ ...params, library_collection_id: next, user_collection_id: "" }); } }} disabled={isLoading} isLoading={isLoading} /> {isTraktPreset && !isLoading && collectionOptions.length === 0 ? (

No synced Trakt {sourcePreset} {mediaType === "tv" ? "shows" : "movies"} collection was found. Create and sync one from Admin Collections first.

) : null}
); }