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 (