Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions components/CarbonAd/SideBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import classNames from 'classnames';

import useCarbonAd, { adColors } from '../../hooks/useCarbonAd.mjs';

import styles from './index.module.css';

/**
* Carbon ad card for the docs sidebar. It is rendered inside the sidebar
* island, so it needs no island of its own. The sidebar collapses to a select
* on small screens, where no ad is requested.
*/
export default function SideBarAd() {
const { ad, ref } = useCarbonAd('CWBI6K77', '(min-width: 56rem)');

if (!ad) return null;

return (
<aside
ref={ref}
className={styles.sideBar}
style={adColors(ad)}
aria-label="Advertisement"
>
<a
href={ad.statlink}
target="_blank"
rel="sponsored noopener"
className={styles.sideBarLink}
>
<img src={ad.largeImage} alt={ad.company} className={styles.image} />
<span className={styles.sideBarBody}>
<strong className={styles.title}>{ad.title}</strong>
<span>{ad.description}</span>
<span className={styles.sideBarCta}>{ad.callToAction}</span>
</span>
</a>
<a
href={ad.ad_via_link}
className={classNames(styles.badge, styles.overlay)}
>
Ad
</a>
</aside>
);
}
71 changes: 71 additions & 0 deletions components/CarbonAd/TopBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from 'react';
import { X } from 'lucide-preact';

import useCarbonAd, { adColors } from '../../hooks/useCarbonAd.mjs';
import withIsland from '../../node_modules/@doc-kit/generator-react/src/html/ui/islands/withIsland.jsx';

import styles from './index.module.css';

const DISMISSED_KEY = 'top-bar-ad-dismissed';

/** @param {{ onClose: () => void }} props */
const Ad = ({ onClose }) => {
const { ad, ref } = useCarbonAd('CWBI623E');

if (!ad) return null;

return (
<aside
ref={ref}
className={styles.topBar}
style={adColors(ad)}
aria-label="Advertisement"
>
<a
href={ad.statlink}
target="_blank"
rel="sponsored noopener"
className={styles.topBarLink}
>
<img src={ad.image} alt={ad.company} className={styles.logo} />
<span>{ad.description}</span>
<span className={styles.cta}>{ad.callToAction}</span>
</a>
<a href={ad.ad_via_link} className={styles.badge}>
Ad
</a>
<button
type="button"
onClick={onClose}
className={styles.close}
aria-label="Close advertisement"
>
<X size={14} aria-hidden="true" />
</button>
</aside>
);
};

/**
* Full-width Carbon ad shown under the navigation bar. Closing it hides it for
* the rest of the visit, without requesting (and so counting) another ad.
*/
const TopBarAd = () => {
// There is no storage while rendering on the server
const [dismissed, setDismissed] = useState(
() => globalThis.sessionStorage?.getItem(DISMISSED_KEY) === 'true'
);

if (dismissed) return null;

return (
<Ad
onClose={() => {
sessionStorage.setItem(DISMISSED_KEY, 'true');
setDismissed(true);
}}
/>
);
};

export default withIsland(TopBarAd, { name: 'TopBarAd', on: { idle: true } });
144 changes: 144 additions & 0 deletions components/CarbonAd/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
@reference "../../styles/index.css";

/* Colors, copy and images are the advertiser's, as approved, and must not be
changed: each creative sets its palette through `adColors`. */
.topBar,
.sideBar {
@apply relative
bg-(--ad-bg)
text-(--ad-text)
dark:bg-(--ad-bg-dark)
dark:text-(--ad-text-dark);

& a {
@apply text-inherit
no-underline!;
}
}

.badge {
@apply shrink-0
rounded-md
border
border-current
px-2
py-0.5
text-sm
opacity-70
hover:opacity-100;
}

/* A slim, single-line bar: the creative centred, the label and close button
tucked away at the end */
.topBar {
@apply flex
items-center
gap-2
px-4
py-1.5
text-sm
md:px-8;

& .badge {
@apply rounded
px-1
py-0
text-[0.625rem]
leading-4
tracking-wide
uppercase;
}
}

.topBarLink {
@apply flex
flex-1
items-center
justify-center
gap-3;
}

.logo {
@apply size-6
shrink-0;
}

.cta {
@apply hidden
shrink-0
rounded-full
bg-(--ad-cta-bg)
px-3
py-0.5
text-xs
font-semibold
text-(--ad-cta-text)
transition-opacity
hover:opacity-90
sm:block
dark:bg-(--ad-cta-bg-dark)
dark:text-(--ad-cta-text-dark);
}

.close {
@apply flex
shrink-0
cursor-pointer
rounded-full
p-0.5
opacity-60
transition-opacity
hover:opacity-100;
}

.sideBar {
/* Sits under the navigation groups rather than above them */
@apply order-last
overflow-hidden
rounded-xl;
}

.sideBarLink {
@apply flex
flex-col;
}

.image {
@apply aspect-square
w-full
object-cover;
}

.sideBarBody {
@apply flex
flex-col
items-start
gap-2
p-4
text-sm;
}

.title {
@apply text-base
font-semibold;
}

.sideBarCta {
@apply mt-2
font-semibold
rounded-full
border-2
border-current
px-4
py-1.5;
}

.overlay {
@apply absolute
top-3
right-3
border-transparent
bg-black/60
text-white
opacity-100;
}
37 changes: 21 additions & 16 deletions components/NavBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,34 @@ import Logo from '#theme/Logo';

import SearchBox from '@doc-kit/generator-react/html/ui/components/SearchBox/index.jsx';
import ThemeToggle from '@doc-kit/generator-react/html/ui/components/ThemeToggle.jsx';
import TopBarAd from '#theme/TopBarAd';

const versionBase = new URL(baseURL).pathname;

/**
* NavBar component that displays the headings, search, etc.
* NavBar component that displays the headings, search, etc., followed by the
* top bar ad.
*/
export default ({ metadata }) => {
return (
<NavBar
Logo={Logo}
sidebarItemTogglerAriaLabel="Toggle navigation menu"
navItems={navbar}
pathname={toPublicLink(metadata.path, versionBase)}
>
<SearchBox pathname={metadata.path} />
<ThemeToggle />
<a
href="https://github.com/webpack/webpack"
className={styles.ghIconWrapper}
aria-label="webpack on GitHub"
<>
<NavBar
Logo={Logo}
sidebarItemTogglerAriaLabel="Toggle navigation menu"
navItems={navbar}
pathname={toPublicLink(metadata.path, versionBase)}
>
<GitHubIcon />
</a>
</NavBar>
<SearchBox pathname={metadata.path} />
<ThemeToggle />
<a
href="https://github.com/webpack/webpack"
className={styles.ghIconWrapper}
aria-label="webpack on GitHub"
>
<GitHubIcon />
</a>
</NavBar>
<TopBarAd />
</>
);
};
2 changes: 2 additions & 0 deletions components/SideBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { sidebar } from '#theme/local/site';
import { version } from '#theme/config';
import { toPublicLink } from '../utils/helpers/urls.mjs';
import versions from '../versions.json' with { type: 'json' };
import SideBarAd from './CarbonAd/SideBar.jsx';
import withIsland from '../node_modules/@doc-kit/generator-react/src/html/ui/islands/withIsland.jsx';

/** @param {string} url */
Expand Down Expand Up @@ -53,6 +54,7 @@ const Sidebar = ({ metadata }) => (
/>
</div>
)}
<SideBarAd />
</SideBar>
);

Expand Down
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default [
globals: globals.browser,
},
},
{
files: ['hooks/**/*.mjs'],
languageOptions: { globals: globals.browser },
},
{
ignores: [
'node_modules/',
Expand Down
Loading
Loading