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:
parent
8fb41383e6
commit
ee601363c2
8 changed files with 54 additions and 71 deletions
|
|
@ -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")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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" : "")}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue