Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
18 changes: 1 addition & 17 deletions src/components/banner/banner.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,11 @@
position: relative;
width: 100%;
z-index: 2;
margin-top: var(--header-height);
animation: slide-down 0.08s ease-out;

a {
color: inherit;
}

&.banner-module {
border-radius: 5px;
margin-bottom: 1rem;
}

&+ :global(.hero) {
margin-top: -60px;
}
}

@keyframes slide-down {
Expand All @@ -40,13 +30,7 @@
align-items: center;
text-align: left;

>img {
max-height: 3rem;
margin-right: 0.5rem;
flex-shrink: 0;
}

>span a {
> span a {
text-decoration: underline;
margin-left: 0.5rem;
}
Expand Down
185 changes: 128 additions & 57 deletions src/components/banner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,94 @@
'use client';

import {useEffect, useState} from 'react';
import Image from 'next/image';

import styles from './banner.module.scss';

type BannerType = {
/** This is an array of strings or RegExps to feed into new RegExp() */
appearsOn: (string | RegExp)[];
/** String that is the label for the call to action button */
linkText: string;
/** String that is the destination url of the call to action button */
linkURL: string;
/** String for the text of the banner */
text: string;
/** Optional ISO Date string that will hide the banner after this date without the need for a rebuild */
expiresOn?: string;
};

// BANNERS is an array of banner objects. You can add as many as you like. If
// you need to disable all banners, set BANNERS to an empty array. Each banner
// is evaluated in order, and the first one that matches will be shown.
//
// BANNER CONFIGURATION
// This is a lazy way of doing things but will work until
// we put a more robust solution in place.
// Example:
//
const SHOW_BANNER = false;
const BANNER_TEXT =
'Behind the Code: A Conversation With Backend Experts featuring CEOs of Laravel, Prisma, and Supabase.';
const BANNER_LINK_URL =
'https://sentry.io/resources/behind-the-code-a-discussion-with-backend-experts/';
const BANNER_LINK_TEXT = 'RSVP';
const OPTIONAL_BANNER_IMAGE = null;

// Examples:
// const SHOW_BANNER_ON = []; // This is disabled
// const SHOW_BANNER_ON = ['^/$']; // This is enabled on the home page
// const SHOW_BANNER_ON = ['^/welcome/']; // This is enabled on the "/welcome" page
// const BANNER_TEXT =
// 'your message here';
// const BANNER_LINK_URL =
// 'link here';
// const BANNER_LINK_TEXT = 'your cta here';
// const BANNERS = [
//
// BANNER CODE
// Don't edit unless you need to change how the banner works.
// This one will take precedence over the last banner in the array
// (which matches all /platforms pages), because it matches first.
// {
// appearsOn: ['^/platforms/javascript/guides/astro/'],
// text: 'This banner appears on the Astro guide',
// linkURL: 'https://sentry.io/thought-leadership',
// linkText: 'Get webinarly',
// },
//
// // This one will match the /welcome page and all /for pages
// {
// appearsOn: ['^/$', '^/platforms/'],
// text: 'This banner appears on the home page and all /platforms pages',
// linkURL: 'https://sentry.io/thought-leadership',
// linkText: 'Get webinarly',
// },
// ];

const BANNERS: BannerType[] = [
{
// Match the homepage
appearsOn: ['^/$'],
text: 'This is a banner for the homepage',
linkURL: 'https://sentry.io/',
linkText: 'RSVP',
},
// javascript -> Astro example
{
appearsOn: ['^/platforms/javascript/guides/astro/'],
text: 'This banner appears on the Astro guide',
linkURL: 'https://sentry.io/thought-leadership',
linkText: 'Get webinarly',
},
// example with an expiration date
{
appearsOn: ['^/platforms/javascript/guides/aws-lambda/'],
text: "This banner should appear on the AWS Lambda guide, but won't because it's expired",
linkURL: 'https://sentry.io/thought-leadership',
linkText: 'Get webinarly',
expiresOn: '2024-01-01T00:00:00Z',
},
// generic javascript example
{
// we can constrain it to the javascript platform page only
// by adding a more specific regex ie '^/platforms/javascript/$'
appearsOn: ['^/platforms/javascript/'],
text: 'This banner appears on the JavaScript platform page and all subpages',
linkURL: 'https://sentry.io/thought-leadership',
linkText: 'Get webinarly',
},
];

const LOCALSTORAGE_NAMESPACE = 'banner-manifest';

// https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
const fastHash = (input: string) => {
let hash = 0;
if (input.length === 0) {
Expand Down Expand Up @@ -52,53 +116,60 @@ const readOrResetLocalStorage = () => {
}
};

export function Banner({isModule = false}) {
const [isVisible, setIsVisible] = useState(false);
const hash = fastHash(`${BANNER_TEXT}:${BANNER_LINK_URL}`).toString();

const enablebanner = () => {
setIsVisible(true);
};
export function Banner() {
type BannerWithHash = BannerType & {hash: string};
const [banner, setBanner] = useState<BannerWithHash | null>(null);

useEffect(() => {
const manifest = readOrResetLocalStorage();
if (!manifest) {
enablebanner();
const matchingBanner = BANNERS.find(b => {
return b.appearsOn.some(matcher =>
new RegExp(matcher).test(window.location.pathname)
);
});

// Bail if no banner matches this page or if the banner has expired
if (
!matchingBanner ||
(matchingBanner.expiresOn &&
new Date() > new Date(matchingBanner.expiresOn ?? null))
) {
return;
}

if (manifest.indexOf(hash) === -1) {
enablebanner();
const manifest = readOrResetLocalStorage();
const hash = fastHash(matchingBanner.text + matchingBanner.linkURL).toString();

// Bail if this banner has already been seen
if (manifest && manifest.indexOf(hash) >= 0) {
return;
}
});

return SHOW_BANNER
? isVisible && (
<div
className={[styles['promo-banner'], isModule && styles['banner-module']]
.filter(Boolean)
.join(' ')}
>
<div className={styles['promo-banner-message']}>
{OPTIONAL_BANNER_IMAGE ? <Image src={OPTIONAL_BANNER_IMAGE} alt="" /> : ''}
<span>
{BANNER_TEXT}
<a href={BANNER_LINK_URL}>{BANNER_LINK_TEXT}</a>
</span>
</div>
<button
className={styles['promo-banner-dismiss']}
role="button"
onClick={() => {
const manifest = readOrResetLocalStorage() || [];
const payload = JSON.stringify([...manifest, hash]);
localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload);
setIsVisible(false);
}}
>
×
</button>
</div>
)
: null;

// Enable the banner
setBanner({...matchingBanner, hash});
}, []);

return banner ? (
<div className={[styles['promo-banner']].filter(Boolean).join(' ')}>
<div className={styles['promo-banner-message']}>
<span className="flex gap-4">
{banner.text}
<a href={banner.linkURL} className="min-w-max">
{banner.linkText}
</a>
</span>
</div>
<button
className={styles['promo-banner-dismiss']}
role="button"
onClick={() => {
const manifest = readOrResetLocalStorage() || [];
const payload = JSON.stringify([...manifest, banner.hash]);
localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload);
setBanner(null);
}}
>
×
</button>
</div>
) : null;
}
4 changes: 4 additions & 0 deletions src/components/docPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {getUnversionedPath} from 'sentry-docs/versioning';

import './type.scss';

import {Banner} from '../banner';
import {Breadcrumbs} from '../breadcrumbs';
import {CodeContextProvider} from '../codeContext';
import {GitHubCTA} from '../githubCTA';
Expand Down Expand Up @@ -75,6 +76,9 @@ export function DocPage({
fullWidth ? 'max-w-none w-full' : 'w-[75ch] xl:max-w-[calc(100%-250px)]',
].join(' ')}
>
<div className="mb-4">
<Banner />
</div>
{leafNode && <Breadcrumbs leafNode={leafNode} />}
<div>
<hgroup>
Expand Down
4 changes: 3 additions & 1 deletion src/components/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export async function Home() {
return (
<div className="tw-app">
<Header pathname="/" searchPlatforms={[]} />
<Banner />
<div className="mt-[var(--header-height)]">
<Banner />
</div>
<div className="hero max-w-screen-xl mx-auto px-6 lg:px-8 py-2">
<div className="flex flex-col md:flex-row gap-4 mx-auto justify-between pt-20">
<div className="flex flex-col justify-center items-start">
Expand Down
Loading