feat(sections): configurable sort for watchlist/favorites sections

Watchlist and favorites sections keep their stored list order by default
(provider-synced positions first, then newest-added). An optional
sort/order config now supports title, release date, IMDb rating, and
date-added-to-list ordering, applied consistently on the home rail and
the catalog "see all" page. added_at is resolved from the list entries
via a shared OrderPersonalListIDs helper since it is distinct from the
catalog's library added_at. The section editor gains a Sort dropdown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-07-05 18:45:48 -04:00
co-authored by Claude Fable 5
parent 42602b7896
commit 69dde62fcd
5 changed files with 337 additions and 16 deletions
@@ -117,14 +117,36 @@ interface ParamFieldProps {
onChange: (next: Record<string, unknown>) => 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 for watchlist and favorites sections, e.g. a "Movies watchlist" rail.
// 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 (
<div className="grid gap-4 md:grid-cols-2">
@@ -156,6 +178,23 @@ function PersonalListFilterFields({ params, onChange }: ParamFieldProps) {
}
/>
</label>
<label className="block md:col-span-2">
<span className="mb-1 block text-xs text-white/70">Sort</span>
<select
value={PERSONAL_LIST_SORT_OPTIONS.some((o) => o.value === sortValue) ? sortValue : ""}
onChange={(e) => {
const [sort, order] = e.target.value.split(":");
onChange({ ...params, sort: sort || undefined, order: order || undefined });
}}
className="w-full rounded border border-white/15 bg-white/5 px-3 py-2 text-sm"
>
{PERSONAL_LIST_SORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
);
}