Skip to content

Commit

Permalink
feat: add regex patterns and support for multiple banners
Browse files Browse the repository at this point in the history
  • Loading branch information
a-hariti committed Nov 7, 2024
1 parent 60fd709 commit bb25cd2
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 75 deletions.
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
172 changes: 115 additions & 57 deletions src/components/banner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,87 @@
'use client';

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

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

type BannerType = {
appearsOn: (string | RegExp)[];
linkText: string;
linkURL: string;
text: 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.
// Banner Object Properties:
//
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;

// - appearsOn: An array of RegExps or strings to feed into new RegExp()
// - text: String for the text of the banner
// - linkURL: String that is the destination url of the call to action button
// - linkText: String that is the label for the call to action button
//
// Example:
//
// BANNER CODE
// Don't edit unless you need to change how the banner works.
// 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 = [
//
// 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',
},
// 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 +109,54 @@ 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
if (!matchingBanner) {
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}>{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

0 comments on commit bb25cd2

Please sign in to comment.