diff --git a/client/src/app.scss b/client/src/app.scss index 389909a3fe76..5b73e574ac45 100644 --- a/client/src/app.scss +++ b/client/src/app.scss @@ -73,10 +73,10 @@ --top-navigation-height: 4rem; --top-navigation-offset: -4rem; --top-banner-inner-height: 3.125rem; - // top-banner-inner-height + 2 * padding + 2 * border; - --top-banner-height: calc( - var(--top-banner-inner-height) + 2 * 0.125rem + 2 * 1px - ); + // top-banner-inner-height + 2 * padding; + --top-banner-height: calc(var(--top-banner-inner-height) + 2 * 0.125rem); + // + border + --top-banner-outer-height: calc(var(--top-banner-height) + 2 * 1px); --z-index-back: -1; --z-index-top: 9999; @@ -101,10 +101,11 @@ ); } -.top-banner.visible ~ * { +.top-banner.visible ~ *, +.top-banner.loading ~ * { --sticky-header-height: calc( var(--top-nav-height) + var(--article-actions-container-height) + - var(--top-banner-height) + 2px + var(--top-banner-outer-height) + 2px ); } diff --git a/client/src/app.tsx b/client/src/app.tsx index 6be740bc62af..9d2be309ce59 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -35,7 +35,6 @@ const Sitemap = React.lazy(() => import("./sitemap")); function Layout({ pageType, children }) { const { pathname } = useLocation(); - const isServer = useIsServer(); const [category, setCategory] = React.useState( getCategoryByPathname(pathname) ); @@ -52,7 +51,7 @@ function Layout({ pageType, children }) { category ? `category-${category}` : "" } ${pageType}`} > - {!isServer && } + {pageType !== "document-page" && ( )} diff --git a/client/src/document/index.scss b/client/src/document/index.scss index 1cf415495551..9879d0bd00a7 100644 --- a/client/src/document/index.scss +++ b/client/src/document/index.scss @@ -9,7 +9,8 @@ top: 0; z-index: var(--z-index-top); @media screen and (min-width: $screen-md) { - .top-banner.visible ~ & { + .top-banner.visible ~ &, + .top-banner.loading ~ & { top: var(--top-banner-height); } } diff --git a/client/src/placement-context.tsx b/client/src/placement-context.tsx index 74c39205d3a6..b14d991bcc7c 100644 --- a/client/src/placement-context.tsx +++ b/client/src/placement-context.tsx @@ -9,6 +9,7 @@ export enum Status { success = "success", geoUnsupported = "geo_unsupported", capReached = "cap_reached", + loading = "loading", } export interface Fallback { @@ -19,7 +20,7 @@ export interface Fallback { by: string; } -export interface PlacementStatus { +export interface PlacementData { status: Status; click: string; view: string; @@ -36,7 +37,10 @@ export interface PlacementStatus { } type PlacementType = "side" | "top"; -type PlacementData = Record; +export interface PlacementContextData + extends Partial> { + status: Status; +} const PLACEMENT_MAP: Record = { side: /\/[^/]+\/(docs\/|blog\/|search$|_homepage)/i, @@ -50,7 +54,7 @@ function placementTypes(pathname: string): string[] { } export const PlacementContext = React.createContext< - PlacementData | null | undefined + PlacementContextData | null | undefined >(undefined); export function PlacementProvider(props: { children: React.ReactNode }) { @@ -63,7 +67,7 @@ export function PlacementProvider(props: { children: React.ReactNode }) { isLoading, isValidating, mutate, - } = useSWR( + } = useSWR( !PLACEMENT_ENABLED || user?.settings?.noAds || !placementTypes(location.pathname) @@ -88,8 +92,8 @@ export function PlacementProvider(props: { children: React.ReactNode }) { } try { - const placementResponse: PlacementData = await response.json(); - gleanClick(`pong: pong->status ${placementResponse.side.status}`); + const placementResponse: PlacementContextData = await response.json(); + gleanClick(`pong: pong->status ${placementResponse.side?.status}`); return placementResponse; } catch (e) { throw Error(response.statusText); @@ -110,7 +114,9 @@ export function PlacementProvider(props: { children: React.ReactNode }) { }, [location.pathname, mutate]); return ( - + {props.children} ); diff --git a/client/src/telemetry/constants.ts b/client/src/telemetry/constants.ts index 1823e56998c8..eb2a5f545b70 100644 --- a/client/src/telemetry/constants.ts +++ b/client/src/telemetry/constants.ts @@ -31,6 +31,7 @@ export const TOGGLE_PLUS_OFFLINE_DISABLED = "toggle_plus_offline_disabled"; export const TOGGLE_PLUS_OFFLINE_ENABLED = "toggle_plus_offline_enabled"; export const TOGGLE_PLUS_ADS_FREE_DISABLED = "toggle_plus_ads_free_disabled"; export const TOGGLE_PLUS_ADS_FREE_ENABLED = "toggle_plus_ads_free_enabled"; +export const BANNER_BLOG_LAUNCH_CLICK = "banner_blog_launch_click"; export const PLUS_UPDATES = Object.freeze({ EVENT_COLLAPSE: "plus_updates_event_collapse", diff --git a/client/src/ui/organisms/placement/index.scss b/client/src/ui/organisms/placement/index.scss index 4b204588854c..2d74f577caa5 100644 --- a/client/src/ui/organisms/placement/index.scss +++ b/client/src/ui/organisms/placement/index.scss @@ -68,6 +68,7 @@ section.place { font-size: 0.625rem; grid-template-areas: "pong pong pong" "no space note"; + height: var(--top-banner-height); margin: 0 auto; width: 100%; @@ -139,20 +140,34 @@ section.place { --place-top-cta-color: var(--background-secondary); background-color: var(--place-top-background); + + border-bottom: 1px solid var(--border-primary); + height: var(--top-banner-height); position: sticky; top: 0; z-index: var(--z-index-top); - &.empty { - height: 0; - } - - &.visible { - border: 1px solid var(--border-primary); - height: fit-content; + &.fallback { + position: initial; } @media screen and (max-width: #{$screen-md - 1}) { display: none; } + + .fallback-copy { + font-size: 1rem; + grid-column: 1/4; + line-height: var(--top-banner-height); + margin: 0 auto; + + a:not(.button) { + color: var(--apis-accent-color); + + &:hover, + &:focus { + text-decoration: underline; + } + } + } } diff --git a/client/src/ui/organisms/placement/index.tsx b/client/src/ui/organisms/placement/index.tsx index 1baf0dd1e92b..8405e834777a 100644 --- a/client/src/ui/organisms/placement/index.tsx +++ b/client/src/ui/organisms/placement/index.tsx @@ -4,7 +4,12 @@ import { useUserData } from "../../../user-context"; import "./index.scss"; import { useGleanClick } from "../../../telemetry/glean-context"; -import { PlacementStatus, usePlacement } from "../../../placement-context"; +import { + PlacementData, + Status, + usePlacement, +} from "../../../placement-context"; +import { BANNER_BLOG_LAUNCH_CLICK } from "../../../telemetry/constants"; interface Timer { timeout: number | null; @@ -13,7 +18,7 @@ interface Timer { } function viewed( - pong: PlacementStatus, + pong: PlacementData, observer: IntersectionObserver | null = null ) { navigator?.sendBeacon?.( @@ -41,7 +46,26 @@ export function SidePlacement() { ); } +function Fallback() { + const gleanClick = useGleanClick(); + + return ( +

+ Discover the latest web development insights on our new{" "} + { + gleanClick(BANNER_BLOG_LAUNCH_CLICK); + }} + > + MDN Blog + +

+ ); +} + export function TopPlacement() { + const isServer = useIsServer(); const placementData = usePlacement(); const { textColor, backgroundColor, ctaTextColor, ctaBackgroundColor } = placementData?.top?.colors || {}; @@ -54,13 +78,21 @@ export function TopPlacement() { ].filter(([_, v]) => Boolean(v)) ); + const status = + isServer || placementData?.status === Status.loading + ? "loading" + : placementData?.top + ? "visible" + : "fallback"; + return ( -
- {!placementData?.top ? ( -
+
+ {isServer || !placementData?.top ? ( +
+ {!isServer && placementData?.status !== Status.loading && ( + + )} +
) : (