diff --git a/frontend/src/components/Banner.tsx b/frontend/src/components/Banner.tsx new file mode 100644 index 0000000..d789078 --- /dev/null +++ b/frontend/src/components/Banner.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from "react"; +import type { LucideIcon } from "lucide-react"; +import { X } from "lucide-react"; + +type BannerTone = "accent" | "warning"; + +const TONES: Record = { + accent: { bar: "bg-accent/15 border-accent/30", icon: "text-accent", action: "bg-accent text-accent-fg" }, + warning: { bar: "bg-amber-500/15 border-amber-500/30", icon: "text-amber-500", action: "bg-amber-500 text-black" }, +}; + +// Shared top-of-app notification bar. VersionBanner and DemoBanner are thin wrappers over this; +// a future news-ticker variant can build on the same shell (rotating content as children). +export default function Banner({ + tone, + icon: Icon, + children, + action, + onDismiss, + dismissTitle, +}: { + tone: BannerTone; + icon: LucideIcon; + children: ReactNode; + action?: { label: string; onClick: () => void }; + onDismiss?: () => void; + dismissTitle?: string; +}) { + const c = TONES[tone]; + return ( +
+ + {children} + {action && ( + + )} + {onDismiss && ( + + )} +
+ ); +} diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index ce0d9cf..62deae1 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -7,6 +7,7 @@ import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; import ChannelLink from "./ChannelLink"; import DataTable, { type Column } from "./DataTable"; +import { useConfirm } from "./ConfirmProvider"; // The Channel manager's "Discovery" tab: channels that turn up in the user's playlists but // that they don't subscribe to. Subscribing here only creates the subscription + enriches @@ -20,6 +21,7 @@ export default function ChannelDiscovery({ }) { const { t } = useTranslation(); const qc = useQueryClient(); + const confirm = useConfirm(); const query = useQuery({ queryKey: ["discovered-channels"], @@ -60,6 +62,17 @@ export default function ChannelDiscovery({ }, }); + // Subscribing changes the user's real YouTube account and spends a little quota — confirm + // first (mirrors the unsubscribe guard in the Subscriptions tab). + const onSubscribe = async (c: DiscoveredChannel) => { + const ok = await confirm({ + title: t("channels.discovery.subscribe"), + message: t("channels.discovery.confirmSubscribe", { name: c.title ?? c.id }), + confirmLabel: t("channels.discovery.subscribe"), + }); + if (ok) subscribe.mutate(c); + }; + const rows = query.data ?? []; const columns: Column[] = [ @@ -87,6 +100,19 @@ export default function ChannelDiscovery({ ), }, + { + key: "videos", + header: t("channels.discovery.cols.totalVideos"), + align: "right", + nowrap: true, + sortable: true, + sortValue: (c) => c.video_count ?? -1, + render: (c) => ( + + {c.video_count != null ? formatViews(c.video_count) : "—"} + + ), + }, { key: "inPlaylists", header: t("channels.discovery.cols.inPlaylists"), @@ -123,7 +149,7 @@ export default function ChannelDiscovery({ } > - - + }, + }} + onDismiss={markSeen} + dismissTitle={t("header.banner.dismiss")} + > + {t("header.banner.updated", { version: FRONTEND_VERSION })} + ); } diff --git a/frontend/src/i18n/locales/de/channels.json b/frontend/src/i18n/locales/de/channels.json index 673ef86..7898cba 100644 --- a/frontend/src/i18n/locales/de/channels.json +++ b/frontend/src/i18n/locales/de/channels.json @@ -19,7 +19,9 @@ "subscribedTitle": "Auf YouTube abonniert", "subscribedBody": "Du folgst jetzt {{name}} — neue Uploads erscheinen in deinem Feed.", "subscribeFailed": "Abonnieren fehlgeschlagen", + "confirmSubscribe": "„{{name}}“ auf YouTube abonnieren? Das ändert dein echtes YouTube-Konto und verbraucht etwas API-Kontingent.", "cols": { + "totalVideos": "Videos", "inPlaylists": "In Playlists", "inPlaylistsHint": "{{videos}} Video(s) von diesem Kanal in {{playlists}} deiner Playlist(s)." } diff --git a/frontend/src/i18n/locales/en/channels.json b/frontend/src/i18n/locales/en/channels.json index e2da771..fed9566 100644 --- a/frontend/src/i18n/locales/en/channels.json +++ b/frontend/src/i18n/locales/en/channels.json @@ -19,7 +19,9 @@ "subscribedTitle": "Subscribed on YouTube", "subscribedBody": "You're now following {{name}} — its new uploads will start arriving in your feed.", "subscribeFailed": "Subscribe failed", + "confirmSubscribe": "Subscribe to \"{{name}}\" on YouTube? This changes your real YouTube account and spends a little API quota.", "cols": { + "totalVideos": "Videos", "inPlaylists": "In playlists", "inPlaylistsHint": "{{videos}} video(s) from this channel across {{playlists}} of your playlist(s)." } diff --git a/frontend/src/i18n/locales/hu/channels.json b/frontend/src/i18n/locales/hu/channels.json index 6ab23bc..8595dfc 100644 --- a/frontend/src/i18n/locales/hu/channels.json +++ b/frontend/src/i18n/locales/hu/channels.json @@ -19,7 +19,9 @@ "subscribedTitle": "Feliratkozva a YouTube-on", "subscribedBody": "Mostantól követed: {{name}} — az új feltöltései megjelennek a hírfolyamodban.", "subscribeFailed": "A feliratkozás sikertelen", + "confirmSubscribe": "Feliratkozol erre a YouTube-on: „{{name}}”? Ez módosítja a valódi YouTube-fiókodat és kevés API-kvótát használ.", "cols": { + "totalVideos": "Videók", "inPlaylists": "Listákban", "inPlaylistsHint": "{{videos}} videó ettől a csatornától, {{playlists}} lejátszási listádban." }