feat(sections): wire watchlist/favorites rails with type and library filters

The dedicated watchlist/favorites section types were never dispatched by
the section fetcher, so their home rails always resolved empty. Resolve
them from the profile's user store, preserving stored order, and honor
the section config's filter_type / filter_library_ids so admins can
build e.g. separate "Movies Watchlist" and "TV Shows Watchlist" rails.
Apply the same filters on the catalog "see all" path, and expose Media
Type + Libraries pickers in the section editor for these types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-07-05 16:07:49 -04:00
co-authored by Claude Fable 5
parent b8b708cbe0
commit 8fa40e928b
4 changed files with 169 additions and 13 deletions
@@ -1,5 +1,7 @@
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 {
@@ -15,6 +17,9 @@ export default function RecipeParamFields({ def, params, onChange }: RecipeParam
if (def.type === "continue_watching") {
return <ContinueTypeParamField params={params} onChange={onChange} />;
}
if (def.type === "watchlist" || def.type === "favorites") {
return <PersonalListFilterFields params={params} onChange={onChange} />;
}
if (def.type === "seasonal_themed") {
return <SeasonalParamField params={params} onChange={onChange} />;
}
@@ -112,6 +117,49 @@ interface ParamFieldProps {
onChange: (next: Record<string, unknown>) => void;
}
// PersonalListFilterFields edits the optional filter_type / filter_library_ids
// filters for watchlist and favorites sections, e.g. a "Movies watchlist" rail.
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")
: [];
return (
<div className="grid gap-4 md:grid-cols-2">
<label className="block">
<span className="mb-1 block text-xs text-white/70">Media type</span>
<select
value={filterType || "all"}
onChange={(e) =>
onChange({
...params,
filter_type: e.target.value === "all" ? undefined : e.target.value,
})
}
className="w-full rounded border border-white/15 bg-white/5 px-3 py-2 text-sm"
>
<option value="all">All Media</option>
<option value="movie">Movies</option>
<option value="series">TV Shows</option>
<option value="audiobook">Audiobooks</option>
</select>
</label>
<label className="block">
<span className="mb-1 block text-xs text-white/70">Libraries</span>
<LibraryMultiSelect
libraries={libraries ?? []}
value={libraryIds}
onChange={(next) =>
onChange({ ...params, filter_library_ids: next.length > 0 ? next : undefined })
}
/>
</label>
</div>
);
}
function ContinueTypeParamField({ params, onChange }: ParamFieldProps) {
const continueType = params.continue_type === "listening" ? "listening" : "watching";