fix(e4): address code-review findings (a11y sweep + dedup guards)

Resolves the 5 /code-review high findings on this branch:
- a11y sweep: extend aria-label to the remaining icon-only Plex card buttons the
  first pass missed — PlexPosterCard Info, SeasonCard watched-toggle + add-season
  (EpisodeCard already had them). "Fix a principle everywhere, in one sweep."
- new shared lib/parseId.ts `parsePositiveIntId`, used by BOTH readSelectedId
  (now guarding the stored ?pl fallback the same as the URL — was unguarded) and
  plexSubUrl's ?v=playlist decode; removes the hand-duplicated guard.
- PAGE_KEY exported from navigationInit and imported by NavigationProvider (was
  defined in both).
- hoist a `watchedLabel` const per card so title + aria-label can't drift.

+8 tests (parseId unit + guarded-stored ?pl); full FE gate green (tsc/eslint/
prettier/knip, 317 vitest).
This commit is contained in:
2026-08-20 05:40:59 +02:00
parent 0b7180ce8f
commit 482dd28006
8 changed files with 68 additions and 23 deletions
+13 -7
View File
@@ -50,6 +50,7 @@ export function PlexPosterCard({
const { t } = useTranslation();
const inProgress = (card.position_seconds ?? 0) > 0 && card.status !== "watched";
const isPlayable = card.type !== "show"; // movies/episodes play; shows open the drill-down
const watchedLabel = card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched");
const pct =
inProgress && card.duration_seconds
? Math.min(100, Math.round(((card.position_seconds ?? 0) / card.duration_seconds) * 100))
@@ -99,8 +100,8 @@ export function PlexPosterCard({
e.stopPropagation();
onToggleWatched(card);
}}
title={card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
aria-label={card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
title={watchedLabel}
aria-label={watchedLabel}
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 reveal hover:bg-black/80 transition"
>
<WatchedIcon watched={card.status === "watched"} />
@@ -114,6 +115,7 @@ export function PlexPosterCard({
onInfo();
}}
title={t("plex.info.title")}
aria-label={t("plex.info.title")}
className="absolute bottom-1.5 left-1.5 p-1 rounded-full bg-black/60 reveal hover:bg-black/80 transition"
>
<Info className="w-4 h-4 text-white" />
@@ -192,6 +194,9 @@ export function SeasonCard({
}) {
const { t } = useTranslation();
const watched = se.status === "watched";
const watchedLabel = watched
? t("plex.series.markSeasonUnwatched")
: t("plex.series.markSeasonWatched");
return (
<div className="group text-left">
<div
@@ -225,9 +230,8 @@ export function SeasonCard({
e.stopPropagation();
onToggleWatched();
}}
title={
watched ? t("plex.series.markSeasonUnwatched") : t("plex.series.markSeasonWatched")
}
title={watchedLabel}
aria-label={watchedLabel}
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 reveal hover:bg-black/80 transition"
>
<WatchedIcon watched={watched} />
@@ -241,6 +245,7 @@ export function SeasonCard({
onAdd();
}}
title={t("plex.playlist.addSeason")}
aria-label={t("plex.playlist.addSeason")}
className="absolute bottom-1.5 right-1.5 p-1 rounded-full bg-black/60 reveal hover:bg-black/80 transition"
>
<ListPlus className="w-4 h-4 text-white" />
@@ -404,6 +409,7 @@ export function EpisodeCard({
}) {
const { t } = useTranslation();
const inProgress = (ep.position_seconds ?? 0) > 0 && ep.status !== "watched";
const watchedLabel = ep.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched");
const pct =
inProgress && ep.duration_seconds
? Math.min(100, Math.round(((ep.position_seconds ?? 0) / ep.duration_seconds) * 100))
@@ -438,8 +444,8 @@ export function EpisodeCard({
e.stopPropagation();
onToggleWatched();
}}
title={ep.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
aria-label={ep.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
title={watchedLabel}
aria-label={watchedLabel}
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 reveal hover:bg-black/80 transition"
>
<WatchedIcon watched={ep.status === "watched"} />