feat(feed): filter by year — per-year count facet + year control (E8·S2)

Add a Year filter to the feed rail: quick chips for recent years plus a
'More…' popover listing all years with per-year video counts, sortable by
year or count. Selecting a year narrows the feed to that whole calendar
year (a published_after/before range applied in the api layer) and is
mutually exclusive with the Upload-date control.

Backend: new /api/feed/years endpoint + feed_years() — one grouped query
that honours every filter EXCEPT the publish-date narrowing (dropped), so
the year list stays a live drill-down and switching years never zeroes the
others. FE keys the list on the date-stripped filters, so picking a year
re-queries the feed but not the list.

Wires year through FeedFilters, the filterParams/URL (?year) round-trip,
the active-filter chip row, and the panel layout. i18n HU+EN.

Tests: feed_years count query (date narrowing ignored, other filters
honoured); dateFilter year-clearing; urlState year round-trip.
This commit is contained in:
2026-08-25 02:55:12 +02:00
parent 2c843e549b
commit edcea87ea5
16 changed files with 373 additions and 9 deletions
+9
View File
@@ -141,6 +141,15 @@ describe("filterParamUpdates", () => {
expect(back.tagModes).toEqual({ language: "or", topic: "and", other: "or" });
});
it("round-trips the year filter; emitted only when set", () => {
expect(filtersToParams({ ...DEFAULTS, year: 2024 }).get("year")).toBe("2024");
expect(filtersToParams({ ...DEFAULTS, year: undefined }).has("year")).toBe(false);
expect(paramsToFilters(new URLSearchParams("year=2024"), DEFAULTS).year).toBe(2024);
// Junk / absent → undefined (off), never a NaN year.
expect(paramsToFilters(new URLSearchParams("year=nope"), DEFAULTS).year).toBeUndefined();
expect(paramsToFilters(new URLSearchParams(""), DEFAULTS).year).toBeUndefined();
});
it("round-trips the 'No tag' (untagged) flag; emitted only when on", () => {
expect(filtersToParams({ ...DEFAULTS, untagged: true }).get("notag")).toBe("1");
expect(filtersToParams({ ...DEFAULTS, untagged: false }).has("notag")).toBe(false);