[];
+ value: T;
+ onChange: (v: T) => void;
+ ariaLabel: string;
+ // Override the row wrapper (e.g. "contents" to dissolve it into a shared toolbar flex row).
+ className?: string;
+}) {
+ return (
+
+ {options.map((o) => {
+ const on = value === o.value;
+ return (
+ onChange(o.value)}
+ title={o.title}
+ aria-pressed={on}
+ className={filterChipClass}
+ >
+ {o.icon}
+ {o.label}
+ {o.count != null && (
+
+ {o.count}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 203219e1..0f82c53b 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -16,6 +16,7 @@ import SidePanel from "./SidePanel";
import PanelGroups, { type PanelItem } from "./PanelGroups";
import ChannelPicker from "./ChannelPicker";
import SegmentedToggle from "./SegmentedToggle";
+import FilterChipGroup from "./FilterChipGroup";
import { useFeedFilters, useFeedFiltersActions } from "./FeedFiltersProvider";
import { useChannelsActions } from "./ChannelsProvider";
import { useMe } from "../lib/useMe";
@@ -150,42 +151,47 @@ export default function Sidebar({
return ;
case "channel":
return ;
- case "date":
+ case "date": {
+ // Single-select across the presets + Custom: a preset is active only when its maxAge matches
+ // and no custom range is open; otherwise Custom owns the selection (or none).
+ const activePresetDays = DATE_PRESETS.find(
+ (p) => filters.maxAgeDays === p.days && !customDates,
+ )?.days;
+ const dateKey =
+ customDates || filters.dateFrom || filters.dateTo
+ ? "custom"
+ : activePresetDays != null
+ ? String(activePresetDays)
+ : "";
return (
<>
-
- {DATE_PRESETS.map((p) => {
- const activePreset = filters.maxAgeDays === p.days && !customDates;
- return (
-
- setFilters({
- ...filters,
- maxAgeDays: activePreset ? undefined : p.days,
- dateFrom: undefined,
- dateTo: undefined,
- })
- }
- aria-pressed={activePreset}
- className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px"
- >
- {t("sidebar.datePreset." + p.key)}
-
- );
- })}
- {
+ ({
+ value: String(p.days),
+ label: t("sidebar.datePreset." + p.key),
+ })),
+ { value: "custom", label: t("sidebar.custom") },
+ ]}
+ onChange={(key) => {
+ if (key === "custom") {
const next = !customDates;
setCustomDates(next);
if (next) setFilters({ ...filters, maxAgeDays: undefined });
- }}
- aria-pressed={!!(customDates || filters.dateFrom || filters.dateTo)}
- className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px"
- >
- {t("sidebar.custom")}
-
-
+ return;
+ }
+ const days = Number(key);
+ const activePreset = filters.maxAgeDays === days && !customDates;
+ setFilters({
+ ...filters,
+ maxAgeDays: activePreset ? undefined : days,
+ dateFrom: undefined,
+ dateTo: undefined,
+ });
+ }}
+ />
{(customDates || filters.dateFrom || filters.dateTo) && (
@@ -233,6 +239,7 @@ export default function Sidebar({
)}
>
);
+ }
case "language": {
const chips = visibleChips(languages);
return (
diff --git a/frontend/src/i18n/locales/en/feed.json b/frontend/src/i18n/locales/en/feed.json
index 7374ec5a..74ee3954 100644
--- a/frontend/src/i18n/locales/en/feed.json
+++ b/frontend/src/i18n/locales/en/feed.json
@@ -20,6 +20,8 @@
"videoCount_other": "{{formattedCount}} videos",
"sortLabel": "Sort",
"viewLabel": "View",
+ "showLabel": "Watch state",
+ "contentLabel": "Content types",
"view": {
"cards": "Large cards",
"cardsSmall": "Small cards",
diff --git a/frontend/src/i18n/locales/en/sidebar.json b/frontend/src/i18n/locales/en/sidebar.json
index ffcc83d9..ea21751c 100644
--- a/frontend/src/i18n/locales/en/sidebar.json
+++ b/frontend/src/i18n/locales/en/sidebar.json
@@ -66,6 +66,7 @@
"shorts": "Shorts",
"live": "Live / Upcoming"
},
+ "datePresetLabel": "Upload date",
"datePreset": {
"24h": "24h",
"1week": "1 week",
diff --git a/frontend/src/i18n/locales/hu/feed.json b/frontend/src/i18n/locales/hu/feed.json
index 7e0625b6..08fa0f85 100644
--- a/frontend/src/i18n/locales/hu/feed.json
+++ b/frontend/src/i18n/locales/hu/feed.json
@@ -20,6 +20,8 @@
"videoCount_other": "{{formattedCount}} videó",
"sortLabel": "Rendezés",
"viewLabel": "Nézet",
+ "showLabel": "Megtekintési állapot",
+ "contentLabel": "Tartalomtípusok",
"view": {
"cards": "Nagy kártya",
"cardsSmall": "Kis kártya",
diff --git a/frontend/src/i18n/locales/hu/sidebar.json b/frontend/src/i18n/locales/hu/sidebar.json
index 0049936a..6b8c01fa 100644
--- a/frontend/src/i18n/locales/hu/sidebar.json
+++ b/frontend/src/i18n/locales/hu/sidebar.json
@@ -66,6 +66,7 @@
"shorts": "Shorts",
"live": "Élő / Hamarosan"
},
+ "datePresetLabel": "Feltöltés dátuma",
"datePreset": {
"24h": "24 óra",
"1week": "1 hét",