From 8a7a7ba8b0707fe3969c259fc2f681bbe0974128 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Aug 2026 08:45:31 +0200 Subject: [PATCH] fix(portals): make facet chips keyboard-operable and reject zero years - The chips were plain spans with a click handler, so the whole feature was mouse-only. Actionable chips are + } @else { + {{ + serial.info.releasedate + }} + } } @if (serial.info.tmdb_genres?.length) { @for (genre of serial.info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (serial.info.genre) { {{ serial.info.genre }} @@ -59,11 +68,13 @@ country of serial.info.tmdb_countries; track country.code ) { - {{ country.name }} + {{ country.name }} + } } diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html index 0dde0ab5a..24d943a74 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.html @@ -17,22 +17,31 @@ }} } @if (info.releaseDate) { - {{ info.releaseDate | slice: 0 : 4 }} + @if (discover.canOpenYear(info.tmdb_id, info.releaseDate)) { + + } @else { + {{ + info.releaseDate | slice: 0 : 4 + }} + } } @if (info.tmdb_genres?.length) { @for (genre of info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (info.genre) { {{ info.genre }} @@ -49,11 +58,13 @@ } @if (info.tmdb_countries?.length) { @for (country of info.tmdb_countries; track country.code) { - {{ country.name }} + {{ country.name }} + } } diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html index 53702cdf0..3d177f1a6 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-details-route.component.html @@ -295,22 +295,31 @@ > @if (info.releasedate) { - {{ info.releasedate | slice: 0 : 4 }} + @if (discover.canOpenYear(info.tmdb_id, info.releasedate)) { + + } @else { + {{ + info.releasedate | slice: 0 : 4 + }} + } } @if (info.tmdb_genres?.length) { @for (genre of info.tmdb_genres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (info.genre) { {{ info.genre }} @@ -325,11 +334,13 @@ } @if (info.tmdb_countries?.length) { @for (country of info.tmdb_countries; track country.code) { - {{ country.name }} + {{ country.name }} + } } @else if (info.country) { {{ info.country }} diff --git a/libs/shared/interfaces/src/lib/tmdb.interface.ts b/libs/shared/interfaces/src/lib/tmdb.interface.ts index f723d997a..1f4e81936 100644 --- a/libs/shared/interfaces/src/lib/tmdb.interface.ts +++ b/libs/shared/interfaces/src/lib/tmdb.interface.ts @@ -119,6 +119,18 @@ export interface TmdbCountryFacet { name: string; } +/** + * Whether a number is a year the Discover page can actually filter by. + * + * Providers ship `0000-00-00` as their "no date" placeholder, which reads + * as a four-digit year and would otherwise produce a `0000` chip that + * filters by nothing. Shared by the chip side and the route-param side so + * a deep link cannot smuggle in what a chip refuses to offer. + */ +export function isTmdbYearFacet(year: number): boolean { + return Number.isInteger(year) && year >= 1000 && year <= 9999; +} + /** * One cached TMDB lookup. Two kinds of rows share the table, discriminated * by the `lookupKey` prefix: diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss index e5abface0..24e42bc79 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss @@ -320,8 +320,15 @@ border-color: rgba(255, 215, 0, 0.3); } + // Rendered as a + } @else { + {{ meta.year }} + } } @if (meta.tmdbGenres?.length) { @for (genre of meta.tmdbGenres; track genre.id) { - {{ genre.name }} + {{ genre.name }} + } } @else if (meta.genre) { {{ meta.genre }} @@ -42,11 +47,13 @@ } @if (meta.tmdbCountries?.length) { @for (country of meta.tmdbCountries; track country.code) { - {{ country.name }} + {{ country.name }} + } } @else if (meta.country) { {{ meta.country }} diff --git a/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts b/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts index 7f45ab8ee..a47693c57 100644 --- a/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts +++ b/libs/ui/shared-portals/src/lib/discover-view/discover-params.spec.ts @@ -38,6 +38,15 @@ describe('parseDiscoverParams', () => { expect(parseDiscoverParams({ year: '1990' }).year).toBe(1990); }); + it('rejects a zero year a deep link could smuggle in', () => { + // The chips refuse '0000'; the route must refuse it too, or the + // request drops the filter and returns unfiltered popular titles + expect(parseDiscoverParams({ year: '0000' }).year).toBeNull(); + expect(hasDiscoverFacet(parseDiscoverParams({ year: '0000' }))).toBe( + false + ); + }); + it('drops a genre label without a valid genre id', () => { const facets = parseDiscoverParams({ genre: 'drama', diff --git a/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts b/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts index ad869365c..47ea52486 100644 --- a/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts +++ b/libs/ui/shared-portals/src/lib/discover-view/discover-params.ts @@ -1,4 +1,4 @@ -import { TmdbMediaType } from '@iptvnator/shared/interfaces'; +import { TmdbMediaType, isTmdbYearFacet } from '@iptvnator/shared/interfaces'; /** * Facets of a portal Discover page, parsed from route query params. @@ -30,8 +30,13 @@ export function parseDiscoverParams(params: RawParams): DiscoverRouteParams { const type: TmdbMediaType = rawString(params, 'type') === 'tv' ? 'tv' : 'movie'; + // `0000` parses as four digits but filters by nothing — a deep link + // must not reach a state the chips themselves refuse to offer const rawYear = rawString(params, 'year'); - const year = rawYear && /^\d{4}$/.test(rawYear) ? Number(rawYear) : null; + const parsedYear = + rawYear && /^\d{4}$/.test(rawYear) ? Number(rawYear) : null; + const year = + parsedYear !== null && isTmdbYearFacet(parsedYear) ? parsedYear : null; const rawGenre = rawString(params, 'genre'); const parsedGenre = rawGenre && /^\d+$/.test(rawGenre) ? Number(rawGenre) : null;