chore(channels-ui): Phase 2 #3 frontend cleanup — dedup 403 handler, count cell, LS keys, dead i18n

- Extract notifyYouTubeActionError (new lib/youtubeErrors.ts): the "403 → Connect
  your YouTube account, else fallback toast" logic was duplicated in Channels.tsx
  and ChannelDiscovery.tsx. (Separate file, not lib/notifications, to avoid the
  api ↔ notifications import cycle.)
- Extract format.formatCountOrDash(): the `n != null ? formatViews(n) : "—"` count
  cell was repeated across the subs/videos columns in Channels + ChannelDiscovery.
- Register channelsTable / channelDiscoveryTable in storage.ts LS instead of bare
  "siftlode.*" string literals.
- Delete 7 dead channels.json keys (filterPlaceholder, tags.newTag/createTag,
  row.stored/subs/getFullHistory/getFullHistoryHint) from en/hu/de — verified 0
  refs (createTag matches were the unrelated api.createTag method, not the key).

Behavior-neutral. tsc green, knip no new unused, localdev boots.
This commit is contained in:
npeter83 2026-07-11 18:11:23 +02:00
parent 8fb41383e6
commit ee601363c2
8 changed files with 54 additions and 71 deletions

View file

@ -1,10 +1,11 @@
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { UserPlus } from "lucide-react";
import { api, HttpError, type DiscoveredChannel } from "../lib/api";
import { accountKey } from "../lib/storage";
import { formatViews } from "../lib/format";
import { api, type DiscoveredChannel } from "../lib/api";
import { accountKey, LS } from "../lib/storage";
import { formatCountOrDash } from "../lib/format";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import Tooltip from "./Tooltip";
import ChannelLink from "./ChannelLink";
import DataTable, { type Column } from "./DataTable";
@ -48,19 +49,8 @@ export default function ChannelDiscovery({
meta: { kind: "channel-subscribed", channelId: c.id, channelName: name },
});
},
onError: (err: unknown) => {
// A 403 means the user hasn't granted the write scope — offer to connect instead of
// a vague failure (mirrors the subscriptions tab).
if (err instanceof HttpError && err.status === 403) {
notify({
level: "error",
message: t("channels.notify.needYouTube"),
action: { label: t("channels.notify.connect"), onClick: onOpenWizard },
});
} else {
notify({ level: "error", message: t("channels.discovery.subscribeFailed") });
}
},
onError: (err: unknown) =>
notifyYouTubeActionError(err, t("channels.discovery.subscribeFailed"), onOpenWizard),
});
// Subscribing changes the user's real YouTube account and spends a little quota — confirm
@ -96,9 +86,7 @@ export default function ChannelDiscovery({
sortable: true,
sortValue: (c) => c.subscriber_count ?? -1,
render: (c) => (
<span className="text-muted tabular-nums">
{c.subscriber_count != null ? formatViews(c.subscriber_count) : "—"}
</span>
<span className="text-muted tabular-nums">{formatCountOrDash(c.subscriber_count)}</span>
),
},
{
@ -109,9 +97,7 @@ export default function ChannelDiscovery({
sortable: true,
sortValue: (c) => c.video_count ?? -1,
render: (c) => (
<span className="text-muted tabular-nums">
{c.video_count != null ? formatViews(c.video_count) : "—"}
</span>
<span className="text-muted tabular-nums">{formatCountOrDash(c.video_count)}</span>
),
},
{
@ -174,7 +160,7 @@ export default function ChannelDiscovery({
rows={rows}
columns={columns}
rowKey={(c) => c.id}
persistKey={accountKey("siftlode.channelDiscoveryTable") ?? undefined}
persistKey={accountKey(LS.channelDiscoveryTable) ?? undefined}
controlsPosition="top"
emptyText={t("channels.discovery.empty")}
/>

View file

@ -16,10 +16,11 @@ import {
UserMinus,
X,
} from "lucide-react";
import { api, HttpError, type ManagedChannel, type Tag } from "../lib/api";
import { accountKey } from "../lib/storage";
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 { formatEta, formatTotalHours, formatViews, relativeTime } from "../lib/format";
import { formatCountOrDash, formatEta, formatTotalHours, relativeTime } from "../lib/format";
import { notify } from "../lib/notifications";
import Tooltip from "./Tooltip";
import DataTable, { type Column } from "./DataTable";
@ -73,19 +74,6 @@ export default function Channels({
const qc = useQueryClient();
const confirm = useConfirm();
// A YouTube-gated action (sync, backfill, unsubscribe) that 403s means the user hasn't
// granted the needed scope — surface that with a "Connect" action instead of a vague fail.
const notifyActionError = (err: unknown, fallbackKey: string) => {
if (err instanceof HttpError && err.status === 403) {
notify({
level: "error",
message: t("channels.notify.needYouTube"),
action: { label: t("channels.notify.connect"), onClick: onOpenWizard },
});
} else {
notify({ level: "error", message: t(fallbackKey) });
}
};
const channelsQuery = useQuery({ queryKey: ["channels"], queryFn: api.channels });
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
const statusQuery = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus });
@ -160,7 +148,7 @@ export default function Channels({
invalidate();
notify({ level: "success", message: t("channels.notify.synced", { count: r.subscriptions ?? 0 }) });
},
onError: (e) => notifyActionError(e, "channels.notify.syncFailed"),
onError: (e) => notifyYouTubeActionError(e, t("channels.notify.syncFailed"), onOpenWizard),
});
const unsubscribe = useMutation({
mutationFn: (v: { id: string; name: string }) => api.unsubscribeChannel(v.id),
@ -169,7 +157,7 @@ export default function Channels({
qc.invalidateQueries({ queryKey: ["my-status"] });
notify({ level: "success", message: t("channels.notify.unsubscribed", { name: v.name }) });
},
onError: (e) => notifyActionError(e, "channels.notify.unsubscribeFailed"),
onError: (e) => notifyYouTubeActionError(e, t("channels.notify.unsubscribeFailed"), onOpenWizard),
});
const resetBackfill = useMutation({
mutationFn: (v: { id: string; name: string }) => api.resetChannelBackfill(v.id),
@ -178,7 +166,7 @@ export default function Channels({
qc.invalidateQueries({ queryKey: ["my-status"] });
notify({ level: "success", message: t("channels.notify.resetDone", { name: v.name }) });
},
onError: (e) => notifyActionError(e, "channels.notify.resetFailed"),
onError: (e) => notifyYouTubeActionError(e, t("channels.notify.resetFailed"), onOpenWizard),
});
const deepAll = useMutation({
mutationFn: () => api.deepAll(true),
@ -190,7 +178,7 @@ export default function Channels({
message: t("channels.notify.fullHistoryRequested", { count: r.updated ?? 0 }),
});
},
onError: (e) => notifyActionError(e, "channels.notify.fullHistoryFailed"),
onError: (e) => notifyYouTubeActionError(e, t("channels.notify.fullHistoryFailed"), onOpenWizard),
});
// Channel-name search + tag-chip filtering, applied client-side over the status-filtered list
@ -294,9 +282,7 @@ export default function Channels({
sortable: true,
sortValue: (c) => c.subscriber_count ?? -1,
render: (c) => (
<span className="text-muted tabular-nums">
{c.subscriber_count != null ? formatViews(c.subscriber_count) : "—"}
</span>
<span className="text-muted tabular-nums">{formatCountOrDash(c.subscriber_count)}</span>
),
},
{
@ -570,7 +556,7 @@ export default function Channels({
rows={visibleChannels}
columns={columns}
rowKey={(c) => c.id}
persistKey={accountKey("siftlode.channelsTable") ?? undefined}
persistKey={accountKey(LS.channelsTable) ?? undefined}
controlsPosition="top"
controlsLeading={statusChips}
rowClassName={(c) => (c.hidden ? "opacity-60" : "")}