diff --git a/client/src/curriculum/landing.tsx b/client/src/curriculum/landing.tsx index b6d36e9808c5..bc3cbd5c96bd 100644 --- a/client/src/curriculum/landing.tsx +++ b/client/src/curriculum/landing.tsx @@ -15,8 +15,10 @@ import "./index.scss"; import "./landing.scss"; import { ProseSection } from "../../../libs/types/document"; import { PartnerBanner } from "./partner-banner"; -import { useIsServer } from "../hooks"; +import { useIsServer, useViewed } from "../hooks"; import scrimBg from "../assets/curriculum/landing-scrim.png"; +import { useGleanClick } from "../telemetry/glean-context"; +import { CURRICULUM } from "../telemetry/constants"; const ScrimInline = lazy(() => import("./scrim-inline")); @@ -133,6 +135,12 @@ function About({ section }) { const { title, content, id } = section.value; const html = useMemo(() => ({ __html: content }), [content]); const isServer = useIsServer(); + const gleanClick = useGleanClick(); + const observedNode = useViewed(() => { + const url = new URL(SCRIM_URL); + const id = url.pathname.slice(1); + gleanClick(`${CURRICULUM}: scrim view id:${id}`); + }); return (
@@ -148,6 +156,7 @@ function About({ section }) { url={SCRIM_URL} img={scrimBg} scrimTitle="MDN + Scrimba partnership announcement scrim" + ref={observedNode} /> )} diff --git a/client/src/curriculum/partner-banner.tsx b/client/src/curriculum/partner-banner.tsx index 5374e6f1c41a..831c1ae8b2bb 100644 --- a/client/src/curriculum/partner-banner.tsx +++ b/client/src/curriculum/partner-banner.tsx @@ -1,12 +1,21 @@ import ThemedPicture from "../ui/atoms/themed-picture"; +import { useGleanClick } from "../telemetry/glean-context"; +import { useViewed } from "../hooks"; +import { CURRICULUM } from "../telemetry/constants"; + import bannerDark from "../../public/assets/curriculum/curriculum-partner-banner-illustration-large-dark.svg"; import bannerLight from "../../public/assets/curriculum/curriculum-partner-banner-illustration-large-light.svg"; import "./partner-banner.scss"; export function PartnerBanner() { + const gleanClick = useGleanClick(); + const observedNode = useViewed(() => { + gleanClick(`${CURRICULUM}: partner banner view`); + }); + return ( -
+

Learn the curriculum with Scrimba and become job ready

@@ -16,6 +25,9 @@ export function PartnerBanner() { target="_blank" rel="origin noreferrer" className="external" + onClick={() => { + gleanClick(`${CURRICULUM}: partner banner click`); + }} > Scrimba's Frontend Developer Career Path {" "} @@ -28,6 +40,9 @@ export function PartnerBanner() { target="_blank" rel="origin noreferrer" className="external" + onClick={() => { + gleanClick(`${CURRICULUM}: partner banner click`); + }} > Find out more diff --git a/client/src/curriculum/scrim-inline.ts b/client/src/curriculum/scrim-inline.ts index 196a88ee6204..7bab646ab3bf 100644 --- a/client/src/curriculum/scrim-inline.ts +++ b/client/src/curriculum/scrim-inline.ts @@ -80,6 +80,7 @@ class ScrimInline extends LitElement { target="_blank" rel="origin noreferrer" class="external" + data-glean="${CURRICULUM}: scrim link id:${this._scrimId}" > Open on Scrimba diff --git a/client/src/hooks.ts b/client/src/hooks.ts index b6c9cedb235b..9a691f443c15 100644 --- a/client/src/hooks.ts +++ b/client/src/hooks.ts @@ -1,4 +1,10 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { + useCallback, + useEffect, + useRef, + useState, + useMemo, +} from "react"; import { useLocation, useNavigationType, useParams } from "react-router-dom"; import { DEFAULT_LOCALE } from "../../libs/constants"; import { isValidLocale } from "../../libs/locale-utils"; @@ -269,3 +275,46 @@ export const useScrollToAnchor = () => { } }); }; + +interface ViewedTimer { + timeout: number | null; +} + +export function useViewed(callback: Function) { + const timer = useRef({ timeout: null }); + const isVisible = usePageVisibility(); + const [node, setNode] = useState(); + const isIntersecting = useIsIntersecting(node, { + root: null, + rootMargin: "0px", + threshold: 0.5, + }); + + useEffect(() => { + if (timer.current.timeout !== -1) { + // timeout !== -1 means the viewed has not been sent + if (isVisible && isIntersecting) { + if (timer.current.timeout === null) { + timer.current = { + timeout: window.setTimeout(() => { + timer.current = { timeout: -1 }; + callback(); + }, 1000), + }; + } + } + } + return () => { + if (timer.current.timeout !== null && timer.current.timeout !== -1) { + clearTimeout(timer.current.timeout); + timer.current = { timeout: null }; + } + }; + }, [isVisible, isIntersecting, callback]); + + return useCallback((node: HTMLElement | null) => { + if (node) { + setNode(node); + } + }, []); +} diff --git a/client/src/telemetry/constants.ts b/client/src/telemetry/constants.ts index eaaa57cb9e56..4f1c7bc01842 100644 --- a/client/src/telemetry/constants.ts +++ b/client/src/telemetry/constants.ts @@ -22,6 +22,7 @@ export const BANNER_BLOG_LAUNCH_CLICK = "banner_blog_launch_click"; export const AI_HELP = "ai_help"; export const BANNER_AI_HELP_CLICK = "banner_ai_help_click"; export const BANNER_SCRIMBA_CLICK = "banner_scrimba_click"; +export const BANNER_SCRIMBA_VIEW = "banner_scrimba_view"; export const PLAYGROUND = "play_action"; export const AI_EXPLAIN = "ai_explain"; export const SETTINGS = "settings"; diff --git a/client/src/ui/organisms/placement/index.tsx b/client/src/ui/organisms/placement/index.tsx index e9f7a051ceb3..a02859efbb52 100644 --- a/client/src/ui/organisms/placement/index.tsx +++ b/client/src/ui/organisms/placement/index.tsx @@ -1,20 +1,14 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { - useIsIntersecting, - useIsServer, - usePageVisibility, -} from "../../../hooks"; +import { useIsServer, useViewed } from "../../../hooks"; import { User, useUserData } from "../../../user-context"; import "./index.scss"; import { useGleanClick } from "../../../telemetry/glean-context"; import { Status, usePlacement } from "../../../placement-context"; import { Payload as PlacementData } from "../../../../../libs/pong/types"; -import { BANNER_SCRIMBA_CLICK } from "../../../telemetry/constants"; - -interface Timer { - timeout: number | null; -} +import { + BANNER_SCRIMBA_CLICK, + BANNER_SCRIMBA_VIEW, +} from "../../../telemetry/constants"; interface PlacementRenderArgs { place: any; @@ -33,12 +27,6 @@ interface PlacementRenderArgs { heading?: string; } -const INTERSECTION_OPTIONS = { - root: null, - rootMargin: "0px", - threshold: 0.5, -}; - function viewed(pong?: PlacementData) { pong?.view && navigator.sendBeacon?.( @@ -91,29 +79,18 @@ export function SidePlacement() { function TopPlacementFallbackContent() { const gleanClick = useGleanClick(); + const observedNode = useViewed(() => { + gleanClick(BANNER_SCRIMBA_VIEW); + }); - return Date.now() < Date.parse("2024-10-12") ? ( -

- Learn front-end development with a 30% discount on{" "} - { - gleanClick(BANNER_SCRIMBA_CLICK); - }} - > - Scrimba - {" "} - — limited time offer! -

- ) : ( + return (

Learn front-end development with high quality, interactive courses from{" "} { gleanClick(BANNER_SCRIMBA_CLICK); }} @@ -277,44 +254,12 @@ export function PlacementInner({ }) { const isServer = useIsServer(); const user = useUserData(); - const isVisible = usePageVisibility(); const gleanClick = useGleanClick(); - const timer = useRef({ timeout: null }); - - const [node, setNode] = useState(); - const isIntersecting = useIsIntersecting(node, INTERSECTION_OPTIONS); - - const sendViewed = useCallback(() => { + const place = useViewed(() => { viewed(pong); gleanClick(`pong: pong->viewed ${typ}`); - timer.current = { timeout: -1 }; - }, [pong, gleanClick, typ]); - - const place = useCallback((node: HTMLElement | null) => { - if (node) { - setNode(node); - } - }, []); - - useEffect(() => { - if (timer.current.timeout !== -1) { - // timeout !== -1 means the viewed has not been sent - if (isVisible && isIntersecting) { - if (timer.current.timeout === null) { - timer.current = { - timeout: window.setTimeout(sendViewed, 1000), - }; - } - } - } - return () => { - if (timer.current.timeout !== null && timer.current.timeout !== -1) { - clearTimeout(timer.current.timeout); - timer.current = { timeout: null }; - } - }; - }, [isVisible, isIntersecting, sendViewed]); + }); const { image, copy, alt, click, version, heading } = pong || {}; return (