From d55c145298c46b06097438254c2d0aa2f405b188 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 13 Oct 2025 17:32:28 -0700 Subject: [PATCH 1/6] publift homepage ads (#2160) ## Description: Put ads on the left and right gutters ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/GutterAds.ts | 108 +++++++++++++ src/client/Main.ts | 41 +++++ src/client/graphics/GameRenderer.ts | 10 -- src/client/graphics/layers/GutterAdModal.ts | 160 -------------------- src/client/index.html | 26 +--- 5 files changed, 155 insertions(+), 190 deletions(-) create mode 100644 src/client/GutterAds.ts delete mode 100644 src/client/graphics/layers/GutterAdModal.ts diff --git a/src/client/GutterAds.ts b/src/client/GutterAds.ts new file mode 100644 index 000000000..2e5731277 --- /dev/null +++ b/src/client/GutterAds.ts @@ -0,0 +1,108 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +const LEFT_FUSE = "gutter-ad-container-left"; +const RIGHT_FUSE = "gutter-ad-container-right"; +// Minimum screen width to show ads (larger than typical Chromebook) +const MIN_SCREEN_WIDTH = 1400; + +@customElement("gutter-ads") +export class GutterAds extends LitElement { + @state() + private isVisible: boolean = false; + + // Override createRenderRoot to disable shadow DOM + createRenderRoot() { + return this; + } + + private isScreenLargeEnough(): boolean { + return window.innerWidth >= MIN_SCREEN_WIDTH; + } + + // Called after the component's DOM is first rendered + firstUpdated() { + // DOM is guaranteed to be available here + console.log("GutterAd DOM is ready"); + } + + public show(): void { + if (!this.isScreenLargeEnough()) { + console.log("Screen too small for gutter ads, skipping"); + return; + } + + console.log("showing GutterAds"); + this.isVisible = true; + this.requestUpdate(); + + // Wait for the update to complete, then load ads + this.updateComplete.then(() => { + this.loadAds(); + }); + } + + public hide(): void { + console.log("hiding GutterAds"); + this.destroyAds(); + this.requestUpdate(); + } + + private loadAds(): void { + // Ensure the container elements exist before loading ads + const leftContainer = this.querySelector(`#${LEFT_FUSE}`); + const rightContainer = this.querySelector(`#${RIGHT_FUSE}`); + + if (!leftContainer || !rightContainer) { + console.warn("Ad containers not found in DOM"); + return; + } + + if (!window.fusetag) { + console.warn("Fuse tag not available"); + return; + } + + try { + console.log("registering zones"); + window.fusetag.que.push(() => { + window.fusetag.registerZone(LEFT_FUSE); + window.fusetag.registerZone(RIGHT_FUSE); + }); + } catch (error) { + console.error("Failed to load fuse ads:", error); + this.hide(); + } + } + + private destroyAds(): void { + if (!window.fusetag) { + return; + } + window.fusetag.que.push(() => { + window.fusetag.destroyZone(LEFT_FUSE); + window.fusetag.destroyZone(RIGHT_FUSE); + }); + this.requestUpdate(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.destroyAds(); + } + + render() { + if (!this.isVisible) { + return html``; + } + + return html` +
+
+
+
+
+
+ `; + } +} diff --git a/src/client/Main.ts b/src/client/Main.ts index 8b48f79cc..d876e662d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -15,6 +15,7 @@ import { FlagInput } from "./FlagInput"; import { FlagInputModal } from "./FlagInputModal"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; +import { GutterAds } from "./GutterAds"; import { HelpModal } from "./HelpModal"; import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal"; @@ -51,6 +52,12 @@ declare global { newPageView: () => void; }; }; + fusetag: { + registerZone: (id: string) => void; + destroyZone: (id: string) => void; + pageInit: (options?: any) => void; + que: Array<() => void>; + }; ramp: { que: Array<() => void>; passiveMode: boolean; @@ -94,6 +101,8 @@ class Client { private patternsModal: TerritoryPatternsModal; private tokenLoginModal: TokenLoginModal; + private gutterAds: GutterAds; + constructor() {} initialize(): void { @@ -161,6 +170,11 @@ class Client { } }); + const gutterAds = document.querySelector("gutter-ads"); + if (!(gutterAds instanceof GutterAds)) + throw new Error("Missing gutter-ads"); + this.gutterAds = gutterAds; + document.addEventListener("join-lobby", this.handleJoinLobby.bind(this)); document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this)); document.addEventListener("kick-player", this.handleKickPlayer.bind(this)); @@ -410,6 +424,8 @@ class Client { updateSliderProgress(slider); slider.addEventListener("input", () => updateSliderProgress(slider)); }); + + this.initializeFuseTag(); } private handleHash() { @@ -569,6 +585,7 @@ class Client { ) as GameStartingModal; startingModal instanceof GameStartingModal; startingModal.show(); + this.gutterAds.hide(); }, () => { this.joinModal.close(); @@ -601,6 +618,7 @@ class Client { console.log("leaving lobby, cancelling game"); this.gameStop(); this.gameStop = null; + this.gutterAds.hide(); this.publicLobby.leaveLobby(); } @@ -612,6 +630,29 @@ class Client { this.eventBus.emit(new SendKickPlayerIntentEvent(target)); } } + + private initializeFuseTag() { + const tryInitFuseTag = (): boolean => { + if (window.fusetag && typeof window.fusetag.pageInit === "function") { + console.log("initializing fuse tag"); + window.fusetag.que.push(() => { + window.fusetag.pageInit({ + blockingFuseIds: ["lhs_sticky_vrec", "rhs_sticky_vrec"], + }); + this.gutterAds.show(); + }); + return true; + } else { + return false; + } + }; + + const interval = setInterval(() => { + if (tryInitFuseTag()) { + clearInterval(interval); + } + }, 100); + } } // Initialize the client when the DOM is loaded diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 8003a38aa..1768015dd 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -16,7 +16,6 @@ import { FPSDisplay } from "./layers/FPSDisplay"; import { FxLayer } from "./layers/FxLayer"; import { GameLeftSidebar } from "./layers/GameLeftSidebar"; import { GameRightSidebar } from "./layers/GameRightSidebar"; -import { GutterAdModal } from "./layers/GutterAdModal"; import { HeadsUpMessage } from "./layers/HeadsUpMessage"; import { Layer } from "./layers/Layer"; import { Leaderboard } from "./layers/Leaderboard"; @@ -215,14 +214,6 @@ export function createRenderer( } spawnAd.g = game; - const gutterAdModal = document.querySelector( - "gutter-ad-modal", - ) as GutterAdModal; - if (!(gutterAdModal instanceof GutterAdModal)) { - console.error("gutter ad modal not found"); - } - gutterAdModal.eventBus = eventBus; - const alertFrame = document.querySelector("alert-frame") as AlertFrame; if (!(alertFrame instanceof AlertFrame)) { console.error("alert frame not found"); @@ -269,7 +260,6 @@ export function createRenderer( headsUpMessage, multiTabModal, spawnAd, - gutterAdModal, alertFrame, fpsDisplay, ]; diff --git a/src/client/graphics/layers/GutterAdModal.ts b/src/client/graphics/layers/GutterAdModal.ts deleted file mode 100644 index 707a04054..000000000 --- a/src/client/graphics/layers/GutterAdModal.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { LitElement, css, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { EventBus, GameEvent } from "../../../core/EventBus"; -import { getGamesPlayed } from "../../Utils"; -import { Layer } from "./Layer"; - -export class GutterAdModalEvent implements GameEvent { - constructor(public readonly isVisible: boolean) {} -} - -@customElement("gutter-ad-modal") -export class GutterAdModal extends LitElement implements Layer { - public eventBus: EventBus; - - @state() - private isVisible: boolean = false; - - @state() - private adLoaded: boolean = false; - - private leftAdType: string = "left_rail"; - private rightAdType: string = "right_rail"; - private leftContainerId: string = "gutter-ad-container-left"; - private rightContainerId: string = "gutter-ad-container-right"; - private margin: string = "10px"; - - // Override createRenderRoot to disable shadow DOM - createRenderRoot() { - return this; - } - - init() { - if (getGamesPlayed() > 1) { - this.eventBus.on(GutterAdModalEvent, (event) => { - if (event.isVisible) { - this.show(); - } else { - this.hide(); - } - }); - } - } - - tick() {} - - static styles = css``; - - // Called after the component's DOM is first rendered - firstUpdated() { - // DOM is guaranteed to be available here - console.log("GutterAdModal DOM is ready"); - } - - public show(): void { - console.log("showing GutterAdModal"); - this.isVisible = true; - this.requestUpdate(); - - // Wait for the update to complete, then load ads - this.updateComplete.then(() => { - this.loadAds(); - }); - } - - public hide(): void { - console.log("hiding GutterAdModal"); - this.isVisible = false; - this.destroyAds(); - this.adLoaded = false; - this.requestUpdate(); - } - - private loadAds(): void { - // Ensure the container elements exist before loading ads - const leftContainer = this.querySelector(`#${this.leftContainerId}`); - const rightContainer = this.querySelector(`#${this.rightContainerId}`); - - if (!leftContainer || !rightContainer) { - console.warn("Ad containers not found in DOM"); - return; - } - - if (!window.ramp) { - console.warn("Playwire RAMP not available"); - this.hide(); - return; - } - - if (this.adLoaded) { - console.log("Ads already loaded, skipping"); - return; - } - - try { - window.ramp.que.push(() => { - window.ramp.spaAddAds([ - { - type: this.leftAdType, - selectorId: this.leftContainerId, - }, - { - type: this.rightAdType, - selectorId: this.rightContainerId, - }, - ]); - this.adLoaded = true; - console.log("Playwire ads loaded:", this.leftAdType, this.rightAdType); - }); - } catch (error) { - console.error("Failed to load Playwire ads:", error); - this.hide(); - } - } - - private destroyAds(): void { - if (!window.ramp || !this.adLoaded) { - return; - } - try { - window.ramp.destroyUnits("all"); - } catch (error) { - console.error("Failed to destroy Playwire ad:", error); - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.destroyAds(); - } - - render() { - if (!this.isVisible) { - return html``; - } - - return html` - - - - - - `; - } -} diff --git a/src/client/index.html b/src/client/index.html index 5cc19cd5c..45a412891 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -84,12 +84,11 @@ document.documentElement.className = "preload"; - - + + - - -