From f26e329ecf65bb7e17c60dc8cff9f177af754852 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 15 Aug 2026 00:48:09 +0200 Subject: [PATCH] refactor(ui): unify Plex filter chips; retire local Chip/ChipRow (R13 S2c step 3) PlexSidebar's bespoke chip system is gone. The media scope (both/movie/show) now uses the shared SegmentedToggle (fullWidth bar, matching the feed Mine/Library scope), and every filter chip renders the canonical rounded-full look: - show / rating / duration / added (single-select) -> FilterChipGroup, with the null/"" "Any" sentinels mapped to string option values - genres / content-ratings (multi-select) -> plain buttons on filterChipClass in their own labelled group (same look, independent semantics) The local Chip (glass-card resting) and ChipRow helpers are deleted; the unused ReactNode import dropped. Self-E2E'd on Plex: scope bar, show + rating chips toggle, zero leftover glass-resting chips. gate green (tsc/eslint-0/ vitest 136/knip/prettier). Branch not merged. --- frontend/src/components/PlexSidebar.tsx | 171 ++++++++++-------------- 1 file changed, 72 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index f247148c..ce96be23 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -1,4 +1,4 @@ -import { useState, type ReactNode } from "react"; +import { useState } from "react"; import { qk } from "../lib/queryKeys"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -12,6 +12,7 @@ import SidePanel from "./SidePanel"; import PanelGroups, { type PanelItem } from "./PanelGroups"; import SortControl from "./SortControl"; import SegmentedToggle from "./SegmentedToggle"; +import FilterChipGroup, { filterChipClass } from "./FilterChipGroup"; import { usePlexClearFilters } from "./usePlexActiveFilters"; import { usePlex, usePlexActions } from "./PlexProvider"; @@ -131,20 +132,17 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "scope", title: t("plex.filter.scope"), body: ( -
- {(["both", "movie", "show"] as const).map((s) => ( - - ))} -
+ ({ + value: s, + label: t(`plex.filter.scopeOpt.${s}`), + }))} + value={scope} + onChange={setScope} + /> ), }, { @@ -193,13 +191,12 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "watch", title: t("plex.filter.show"), body: ( - - {SHOW_OPTS.map((s) => ( - setShow(s)}> - {t(`plex.filter.showOpt.${s}`)} - - ))} - + ({ value: s, label: t(`plex.filter.showOpt.${s}`) }))} + value={show} + onChange={setShow} + /> ), }, { @@ -304,16 +301,15 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "rating", title: t("plex.filter.rating"), body: ( - - patch({ ratingMin: null })}> - {t("plex.filter.any")} - - {RATING_STEPS.map((r) => ( - patch({ ratingMin: r })}> - {r}+ - - ))} - + ({ value: String(r), label: `${r}+` })), + ]} + onChange={(v) => patch({ ratingMin: v === "" ? null : Number(v) })} + /> ), }, ...(facets && facets.genres.length > 0 @@ -329,17 +325,22 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl onChange={(m) => patch({ genreMode: m })} /> )} - +
{facets.genres.map((g) => ( - patch({ genres: toggleIn(filters.genres, g.value) })} + aria-pressed={filters.genres.includes(g.value)} + className={filterChipClass} > {g.value} - + ))} - +
), }, @@ -370,25 +371,21 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "duration", title: t("plex.filter.duration"), body: ( - - patch({ durationMin: null, durationMax: null })} - > - {t("plex.filter.any")} - - {DURATION_BUCKETS.map((b) => ( - patch({ durationMin: b.min, durationMax: b.max })} - > - {t(`plex.filter.durationOpt.${b.key}`)} - - ))} - + ({ + value: b.key, + label: t(`plex.filter.durationOpt.${b.key}`), + })), + ]} + onChange={(key) => { + const b = DURATION_BUCKETS.find((x) => x.key === key); + patch({ durationMin: b?.min ?? null, durationMax: b?.max ?? null }); + }} + /> ), }, ] @@ -397,20 +394,15 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "added", title: t("plex.filter.added"), body: ( - - patch({ addedWithin: "" })}> - {t("plex.filter.any")} - - {ADDED_OPTS.map((a) => ( - patch({ addedWithin: a })} - > - {t(`plex.filter.addedOpt.${a}`)} - - ))} - + ({ value: a, label: t(`plex.filter.addedOpt.${a}`) })), + ]} + onChange={(v) => patch({ addedWithin: v })} + /> ), }, ...(facets && facets.content_ratings.length > 0 @@ -419,19 +411,24 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl id: "contentrating", title: t("plex.filter.contentRating"), body: ( - +
{facets.content_ratings.map((c) => ( - patch({ contentRatings: toggleIn(filters.contentRatings, c.value) }) } + aria-pressed={filters.contentRatings.includes(c.value)} + className={filterChipClass} > {c.value} - + ))} - +
), }, ] @@ -466,30 +463,6 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl ); } -function ChipRow({ children }: { children: ReactNode }) { - return
{children}
; -} - -function Chip({ - active, - onClick, - children, -}: { - active: boolean; - onClick: () => void; - children: ReactNode; -}) { - return ( - - ); -} - function RemovableChip({ label, onRemove }: { label: string; onRemove: () => void }) { return (