From b59ad00fac0ad3cf4c4db17c6304a7af9f7a8829 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 12 Jul 2026 00:46:52 +0200 Subject: [PATCH] =?UTF-8?q?chore(ui):=20dedup=20last=203=20jscpd=20clones?= =?UTF-8?q?=20=E2=86=92=200=20(collapsed=20rail,=20video-card=20text=20blo?= =?UTF-8?q?ck,=20subs=20column)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollapsedFilterRail: the byte-identical 31-line collapsed sidebar rail was in both Sidebar (feed) and PlexSidebar → one shared component. - VideoCard: the title + channel-button + meta block was duplicated across the list-row and card layouts → a local `textBlock` element (only one layout branch renders, so reusing the element is safe). - subsColumn(t): the identical 'subscribers' DataTable column in Channels + ChannelDiscovery → a generic factory in channelColumns.tsx. All behavior-neutral; jscpd now reports 0 clones (was 8 at Phase 0 baseline). tsc + knip green. --- frontend/src/components/ChannelDiscovery.tsx | 13 +----- frontend/src/components/Channels.tsx | 15 ++----- .../src/components/CollapsedFilterRail.tsx | 39 +++++++++++++++++ frontend/src/components/PlexSidebar.tsx | 28 ++---------- frontend/src/components/Sidebar.tsx | 28 +----------- frontend/src/components/VideoCard.tsx | 43 ++++++++----------- frontend/src/components/channelColumns.tsx | 18 ++++++++ 7 files changed, 86 insertions(+), 98 deletions(-) create mode 100644 frontend/src/components/CollapsedFilterRail.tsx create mode 100644 frontend/src/components/channelColumns.tsx diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index f4ddb7b..17e98d1 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -4,6 +4,7 @@ import { UserPlus } from "lucide-react"; import { api, type DiscoveredChannel } from "../lib/api"; import { accountKey, LS } from "../lib/storage"; import { formatCountOrDash } from "../lib/format"; +import { subsColumn } from "./channelColumns"; import { notify } from "../lib/notifications"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import Tooltip from "./Tooltip"; @@ -78,17 +79,7 @@ export default function ChannelDiscovery({ ), }, - { - key: "subs", - header: t("channels.cols.subs"), - align: "right", - nowrap: true, - sortable: true, - sortValue: (c) => c.subscriber_count ?? -1, - render: (c) => ( - {formatCountOrDash(c.subscriber_count)} - ), - }, + subsColumn(t), { key: "videos", header: t("channels.discovery.cols.totalVideos"), diff --git a/frontend/src/components/Channels.tsx b/frontend/src/components/Channels.tsx index 4b08b08..85b7f93 100644 --- a/frontend/src/components/Channels.tsx +++ b/frontend/src/components/Channels.tsx @@ -20,7 +20,8 @@ import { api, type ManagedChannel, type Tag } from "../lib/api"; import { notifyYouTubeActionError } from "../lib/youtubeErrors"; import { accountKey, LS } from "../lib/storage"; import { useDismiss } from "../lib/useDismiss"; -import { formatCountOrDash, formatEta, formatTotalHours, relativeTime } from "../lib/format"; +import { formatEta, formatTotalHours, relativeTime } from "../lib/format"; +import { subsColumn } from "./channelColumns"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; import DataTable, { type Column } from "./DataTable"; @@ -274,17 +275,7 @@ export default function Channels({ sortValue: (c) => c.stored_videos, render: (c) => {c.stored_videos.toLocaleString()}, }, - { - key: "subs", - header: t("channels.cols.subs"), - align: "right", - nowrap: true, - sortable: true, - sortValue: (c) => c.subscriber_count ?? -1, - render: (c) => ( - {formatCountOrDash(c.subscriber_count)} - ), - }, + subsColumn(t), { key: "lastUpload", header: t("channels.cols.lastUpload"), diff --git a/frontend/src/components/CollapsedFilterRail.tsx b/frontend/src/components/CollapsedFilterRail.tsx new file mode 100644 index 0000000..41124a6 --- /dev/null +++ b/frontend/src/components/CollapsedFilterRail.tsx @@ -0,0 +1,39 @@ +import { ChevronRight, SlidersHorizontal } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +// The thin 46px rail shown when a filter sidebar is collapsed (both the feed Sidebar and the Plex +// PlexSidebar rendered byte-identical copies): an expand control + a filter icon carrying the +// active-filter count so you can tell filters are on without opening the panel. +export default function CollapsedFilterRail({ + activeCount, + onToggleCollapse, +}: { + activeCount: number; + onToggleCollapse: () => void; +}) { + const { t } = useTranslation(); + return ( + + ); +} diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index 65ee75c..7358318 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -1,7 +1,8 @@ import { useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { ChevronRight, Film, Layers, ListMusic, Plus, SlidersHorizontal, Tv2, X } from "lucide-react"; +import { Film, Layers, ListMusic, Plus, Tv2, X } from "lucide-react"; +import CollapsedFilterRail from "./CollapsedFilterRail"; import { api, EMPTY_PLEX_FILTERS, plexFilterCount, type PlexFilters } from "../lib/api"; import { useDebounced } from "../lib/useDebounced"; @@ -115,30 +116,7 @@ export default function PlexSidebar({ )?.key; if (collapsed) { - return ( - - ); + return ; } return ( diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 1aeaaf2..6368c22 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -5,7 +5,6 @@ import { Check, ChevronDown, ChevronLeft, - ChevronRight, Eye, EyeOff, GripVertical, @@ -13,7 +12,6 @@ import { Pencil, RotateCcw, Share2, - SlidersHorizontal, User, X, } from "lucide-react"; @@ -44,6 +42,7 @@ import { useDebounced } from "../lib/useDebounced"; import { Switch } from "./ui/form"; import TagManager from "./TagManager"; import SavedViewsWidget from "./SavedViewsWidget"; +import CollapsedFilterRail from "./CollapsedFilterRail"; // Filter ids; display labels resolved at render time via t("sidebar.sort.") etc. const SORT_IDS = [ @@ -477,30 +476,7 @@ export default function Sidebar({ // Collapsed: a thin rail (mirroring the nav) — an expand control plus a filter icon that // carries the active-filter count so you can tell filters are on without opening it. if (collapsed) { - return ( - - ); + return ; } return ( diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx index 726bb38..32a76d6 100644 --- a/frontend/src/components/VideoCard.tsx +++ b/frontend/src/components/VideoCard.tsx @@ -285,6 +285,23 @@ function VideoCard({ {video.title} ); + // Title + channel button + meta — identical in both layouts (only their wrapper / Actions placement + // differs). Safe to reuse the same element: exactly one of the two branches below renders. + const textBlock = ( + <> + {title} + +
{meta}
+ + ); if (view === "list") { return ( @@ -295,19 +312,7 @@ function VideoCard({ )} > -
- {title} - -
{meta}
-
+
{textBlock}
); @@ -328,17 +333,7 @@ function VideoCard({ className="w-9 h-9 rounded-full shrink-0 mt-0.5" />
- {title} - -
{meta}
+ {textBlock}
diff --git a/frontend/src/components/channelColumns.tsx b/frontend/src/components/channelColumns.tsx new file mode 100644 index 0000000..3de4670 --- /dev/null +++ b/frontend/src/components/channelColumns.tsx @@ -0,0 +1,18 @@ +import type { TFunction } from "i18next"; +import { formatCountOrDash } from "../lib/format"; +import type { Column } from "./DataTable"; + +/** The shared "subscribers" DataTable column (right-aligned, sortable, em-dash for null) — identical + * in the Channels manager and the ChannelDiscovery table, which show subscriber counts on different + * channel row shapes. */ +export function subsColumn(t: TFunction): Column { + return { + key: "subs", + header: t("channels.cols.subs"), + align: "right", + nowrap: true, + sortable: true, + sortValue: (c) => c.subscriber_count ?? -1, + render: (c) => {formatCountOrDash(c.subscriber_count)}, + }; +}