diff --git a/public/ads.txt b/public/ads.txt new file mode 100644 index 0000000..3f282eb --- /dev/null +++ b/public/ads.txt @@ -0,0 +1 @@ +google.com, ## YOUR pub-XXXXXXXXX, DIRECT, XXXXXXX diff --git a/public/adsense.json b/public/adsense.json new file mode 100644 index 0000000..8dcba1b --- /dev/null +++ b/public/adsense.json @@ -0,0 +1,5 @@ +{ + "slot": "### SLOT ID FROM GOOGLE - data-ad-slot ###", + "format": "auto", + "fullWidthResponsive": true +} diff --git a/public/ui-config.example b/public/ui-config.example index d33db29..946e5e0 100644 --- a/public/ui-config.example +++ b/public/ui-config.example @@ -27,6 +27,7 @@ "guides.enabled": true, "toolbar.hide.quests": true, "catalog.style.new": true, + "show.google.ads": false, "loginview": { "images": { "background": "${asset.url}/c_images/reception/stretch_blue.png", diff --git a/src/components/MainView.tsx b/src/components/MainView.tsx index f757aa7..5309364 100644 --- a/src/components/MainView.tsx +++ b/src/components/MainView.tsx @@ -24,6 +24,7 @@ import { NavigatorView } from './navigator/NavigatorView'; import { NitrobubbleHiddenView } from './nitrobubblehidden/NitrobubbleHiddenView'; import { NitropediaView } from './nitropedia/NitropediaView'; import { ExternalPluginLoader } from './plugins/ExternalPluginLoader'; +import { GoogleAdsView } from './ads/GoogleAdsView'; import { RightSideView } from './right-side/RightSideView'; import { RoomView } from './room/RoomView'; import { ToolbarView } from './toolbar/ToolbarView'; @@ -97,6 +98,7 @@ export const MainView: FC<{}> = props => } + diff --git a/src/components/ads/GoogleAdsView.tsx b/src/components/ads/GoogleAdsView.tsx new file mode 100644 index 0000000..b31574e --- /dev/null +++ b/src/components/ads/GoogleAdsView.tsx @@ -0,0 +1,164 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { GetConfigurationValue } from '../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; + +interface AdsenseConfig { + slot: string; + format?: string; + fullWidthResponsive?: boolean; +} + +const ADSENSE_SCRIPT_ID = 'google-adsense-script'; + +const parsePublisherIdFromAdsTxt = (text: string): string | null => { + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.split('#')[0].trim(); + if (!line) continue; + const parts = line.split(',').map(part => part.trim()); + if (parts.length < 2) continue; + if (parts[0].toLowerCase() !== 'google.com') continue; + const pub = parts[1]; + if (/^pub-\d+$/.test(pub)) return pub; + } + return null; +}; + +const ensureAdsenseScript = (publisherId: string): void => { + if (typeof document === 'undefined') return; + if (document.getElementById(ADSENSE_SCRIPT_ID)) return; + + const script = document.createElement('script'); + script.id = ADSENSE_SCRIPT_ID; + script.async = true; + script.crossOrigin = 'anonymous'; + script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-${ publisherId }`; + document.head.appendChild(script); +}; + +export const GoogleAdsView: FC<{}> = () => { + const adsEnabled = GetConfigurationValue('show.google.ads', false); + const [ isOpen, setIsOpen ] = useState(false); + const [ publisherId, setPublisherId ] = useState(null); + const [ config, setConfig ] = useState(null); + const [ loadError, setLoadError ] = useState(null); + const insRef = useRef(null); + const pushedRef = useRef(false); + const autoOpenedRef = useRef(false); + + useEffect(() => { + if (!adsEnabled) return; + const handler = () => setIsOpen(prev => !prev); + window.addEventListener('ads:toggle', handler); + return () => window.removeEventListener('ads:toggle', handler); + }, [ adsEnabled ]); + + // Auto-open once on initial mount (the login / landing stage). + // Subsequent toggles are driven by the "ads:toggle" window event + // (e.g. the Show Ad button in NitroSystemAlertView). + useEffect(() => { + if (!adsEnabled) return; + if (autoOpenedRef.current) return; + autoOpenedRef.current = true; + const t = setTimeout(() => setIsOpen(true), 500); + return () => clearTimeout(t); + }, [ adsEnabled ]); + + useEffect(() => { + let cancelled = false; + + (async () => { + try { + const [ adsTxtRes, configRes ] = await Promise.all([ + fetch('/ads.txt', { cache: 'no-cache' }), + fetch('/adsense.json', { cache: 'no-cache' }) + ]); + + if (!adsTxtRes.ok) throw new Error(`ads.txt ${ adsTxtRes.status }`); + + const adsTxt = await adsTxtRes.text(); + const pubId = parsePublisherIdFromAdsTxt(adsTxt); + + if (!pubId) throw new Error('No google.com publisher id in ads.txt'); + + let cfg: AdsenseConfig = { slot: '', format: 'auto', fullWidthResponsive: true }; + if (configRes.ok) cfg = { ...cfg, ...(await configRes.json()) }; + + if (cancelled) return; + setPublisherId(pubId); + setConfig(cfg); + } catch (err) { + if (!cancelled) setLoadError((err as Error).message); + } + })(); + + return () => { cancelled = true; }; + }, []); + + useEffect(() => { + if (!isOpen || !publisherId || !config) return; + ensureAdsenseScript(publisherId); + }, [ isOpen, publisherId, config ]); + + useEffect(() => { + if (!isOpen) { + pushedRef.current = false; + return; + } + if (!insRef.current || pushedRef.current) return; + if (!publisherId || !config?.slot) return; + + const tryPush = () => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = window as any; + w.adsbygoogle = w.adsbygoogle || []; + w.adsbygoogle.push({}); + pushedRef.current = true; + } catch { + // AdSense script may not be ready yet; retry once + setTimeout(() => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = window as any; + w.adsbygoogle = w.adsbygoogle || []; + w.adsbygoogle.push({}); + pushedRef.current = true; + } catch { /* give up */ } + }, 500); + } + }; + + const t = setTimeout(tryPush, 50); + return () => clearTimeout(t); + }, [ isOpen, publisherId, config ]); + + if (!adsEnabled) return null; + if (!isOpen) return null; + + return ( + + setIsOpen(false) } /> + +
+ { loadError && +
Ads unavailable: { loadError }
} + { !loadError && (!publisherId || !config) && +
Loading…
} + { !loadError && publisherId && config?.slot && + } + { !loadError && publisherId && config && !config.slot && +
Ad slot not configured in adsense.json
} +
+
+
+ ); +}; diff --git a/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx b/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx index 58db879..dd47863 100644 --- a/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx +++ b/src/components/notification-center/views/alert-layouts/NitroSystemAlertView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { GetRendererVersion, GetUIVersion, NotificationAlertItem } from '../../../../api'; +import { GetConfigurationValue, GetRendererVersion, GetUIVersion, NotificationAlertItem } from '../../../../api'; import { Button, Column, Grid, LayoutNotificationAlertView, LayoutNotificationAlertViewProps, Text } from '../../../../common'; interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewProps @@ -9,10 +9,11 @@ interface NotificationDefaultAlertViewProps extends LayoutNotificationAlertViewP export const NitroSystemAlertView: FC = props => { - const { title = 'Nitro', onClose = null, ...rest } = props; + const { title = 'Nitro', onClose = null, classNames = [], ...rest } = props; + const adsEnabled = GetConfigurationValue('show.google.ads', false); return ( - + @@ -23,6 +24,8 @@ export const NitroSystemAlertView: FC = props Renderer: v{ GetRendererVersion() } + { adsEnabled && + }
@@ -35,7 +38,7 @@ export const NitroSystemAlertView: FC = props
- +
); diff --git a/src/css/notification/NotificationCenterView.css b/src/css/notification/NotificationCenterView.css index 45e3e4b..ec2c5e5 100644 --- a/src/css/notification/NotificationCenterView.css +++ b/src/css/notification/NotificationCenterView.css @@ -19,7 +19,7 @@ min-width: auto; } } - + &.nitro-alert-credits { width: 370px; .notification-text { @@ -34,6 +34,19 @@ min-width: 225px; } } + + &.nitro-alert-system { + width: auto; + min-width: 260px; + max-width: 90vw; + min-height: auto; + max-height: none; + height: auto; + + .notification-text { + min-width: auto; + } + } } .nitro-notification-bubble {