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:
@@ -81,6 +81,16 @@ export interface FeedFilters {
|
||||
maxDuration?: number;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
/** Filter-by-year (item 8): a single calendar year, expressed on the wire as a whole-year
|
||||
* published_after/before range. Mutually exclusive with maxAgeDays/dateFrom/dateTo (all
|
||||
* publish-date narrowings) — the setters clear the others, and `filterParams` lets year win. */
|
||||
year?: number;
|
||||
}
|
||||
|
||||
/** One year's video count for the year control's chips + "more…" dialog (item 8 / E8·S2). */
|
||||
export interface YearCount {
|
||||
year: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
// The filter half of the query (everything except paging/sort), shared by the feed and
|
||||
@@ -100,9 +110,16 @@ function filterParams(f: FeedFilters): URLSearchParams {
|
||||
p.set("include_live", String(f.includeLive));
|
||||
p.set("show", f.show);
|
||||
f.channelIds.forEach((id) => p.append("channel_ids", id));
|
||||
if (f.maxAgeDays) p.set("max_age_days", String(f.maxAgeDays));
|
||||
if (f.dateFrom) p.set("published_after", f.dateFrom);
|
||||
if (f.dateTo) p.set("published_before", f.dateTo);
|
||||
// Year (item 8) is a whole-year publish-date range and wins over the maxAge/custom-range controls
|
||||
// (they're mutually exclusive publish-date narrowings — the setters enforce it, this guards the wire).
|
||||
if (f.year) {
|
||||
p.set("published_after", `${f.year}-01-01`);
|
||||
p.set("published_before", `${f.year}-12-31`);
|
||||
} else {
|
||||
if (f.maxAgeDays) p.set("max_age_days", String(f.maxAgeDays));
|
||||
if (f.dateFrom) p.set("published_after", f.dateFrom);
|
||||
if (f.dateTo) p.set("published_before", f.dateTo);
|
||||
}
|
||||
if (f.minDuration != null) p.set("min_duration", String(f.minDuration));
|
||||
if (f.maxDuration != null) p.set("max_duration", String(f.maxDuration));
|
||||
// Only relevant in Library scope; the backend ignores it for "my". Default = hide search.
|
||||
@@ -148,6 +165,10 @@ export const feedApi = {
|
||||
// the separate feedCount + facets the feed and sidebar used to fire on every filter change (PERF-13).
|
||||
feedSummary: (f: FeedFilters): Promise<{ count: number; facets: Record<string, number> }> =>
|
||||
req(`/api/feed/summary?${filterParams(f).toString()}`),
|
||||
// Per-year video counts for the year control (item 8 / E8·S2). The backend drops the publish-date
|
||||
// narrowing, so callers key this on the date-stripped filters (picking a year mustn't refetch it).
|
||||
feedYears: (f: FeedFilters): Promise<{ years: YearCount[] }> =>
|
||||
req(`/api/feed/years?${filterParams(f).toString()}`),
|
||||
// idempotent: setting a state / saving a progress checkpoint is safe to replay, so these
|
||||
// recover from a transient gateway/keepalive 502 instead of surfacing it (see req()).
|
||||
setState: (id: string, status: string) =>
|
||||
|
||||
Reference in New Issue
Block a user