Skip to content

Commit

Permalink
feat: Banner pathname regex (#11761)
Browse files Browse the repository at this point in the history
* feat: add regex patterns and support for multiple banners

* add support for expiration date

* fix banner link text wrapping

* fix mobile issues

* simplify ternary

* improve doc comments

* add contributing page

* fix banner color in dark mode

* remove demo banners
  • Loading branch information
a-hariti authored Nov 8, 2024
1 parent df405f2 commit ab044fe
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 84 deletions.
72 changes: 72 additions & 0 deletions docs/contributing/pages/banners.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: Banners
noindex: true
sidebar_order: 80
---

You can add arbitrary banners to the top of a page by adding adding an entry to the `BANNERS` array on
the `banner/index.tsx` file. The `BANNERS` array is an array of objects with the following properties:

```typescript {filename:banner/index.tsx}
type BannerType = {
/** This is an array of strings or RegExps to feed into new RegExp() */
appearsOn: (string | RegExp)[];
/** The label for the call to action button */
linkText: string;
/** The destination url of the call to action button */
linkURL: string;
/** The main 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;
};
```

You can add as many banners as you like. If you need to disable all banners, simply delete them from the array.

Each banner is evaluated in order, and the first one that matches will be shown.

Examples:

```typescript {filename:banner/index.tsx}
// ...
// appearsOn = []; // This is disabled
// appearsOn = ['^/$']; // This is enabled on the home page
// appearsOn = ['^/welcome/']; // This is enabled on the "/welcome" page
// ...

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',
},
];

```

Optionally, you can add an `expiresOn` property to a banner to hide it after a certain date without requiring a rebuild or manual removeal.
the ISO Date string should be in the format `YYYY-MM-DDTHH:MM:SSZ` to be parsed correctly and account for timezones.

```typescript {filename:banner/index.tsx}
const BANNERS = [
{
appearsOn: ['^/$'],
text: 'This home page banner will disappear after 2024-12-06',
linkURL: 'https://sentry.io/party',
linkText: 'RSVP',
expiresOn: '2024-12-06T00:00:00Z',
},
];
```
35 changes: 10 additions & 25 deletions src/components/banner/banner.module.scss
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
.promo-banner {
font-size: 15px;
color: #21201c;
background: var(--accent-yellow);
padding: 0.5rem;
padding: 0.5rem 1rem;
display: flex;
justify-content: center;
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,24 +32,18 @@
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;
}
}

.promo-banner-dismiss {
background: var(--flame6);
height: 3rem;
width: 3rem;
height: 1.5rem;
width: 1.5rem;
font-size: 1rem;
line-height: 100%;
font-size: 2.5rem;
border-radius: 3rem;
text-align: center;
position: absolute;
Expand All @@ -70,9 +56,8 @@
text-decoration: none;
}

@media (min-width: 576px) {
height: 1.5rem;
width: 1.5rem;
font-size: 1rem;
@media (max-width: 576px) {
top: 1.5rem;
right: 1rem;
}
}
160 changes: 102 additions & 58 deletions src/components/banner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
'use client';

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

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

//
// BANNER CONFIGURATION
// This is a lazy way of doing things but will work until
// we put a more robust solution in place.
//
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;
type BannerType = {
/** This is an array of strings or RegExps to feed into new RegExp() */
appearsOn: (string | RegExp)[];
/** The label for the call to action button */
linkText: string;
/** The destination url of the call to action button */
linkURL: string;
/** The main 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, simply delete them from the array. Each banner
// is evaluated in order, and the first one that matches will be shown.
//
// Examples:
// appearsOn = []; // This is disabled
// appearsOn = ['^/$']; // This is enabled on the home page
// appearsOn = ['^/welcome/']; // This is enabled on the "/welcome" page
// 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[] = [
/// ⚠️ KEEP THIS LAST BANNER ACTIVE FOR DOCUMENTATION
// check it out on `/contributing/pages/banners/`
{
appearsOn: ['^/contributing/pages/banners/'],
text: 'Edit this banner on `/src/components/banner/index.tsx`',
linkURL: 'https://docs.sentry.io/contributing/pages/banners/',
linkText: 'CTA',
},
];

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 +86,63 @@ 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});
}, []);

if (!banner) {
return null;
}
return (
<div className={[styles['promo-banner']].filter(Boolean).join(' ')}>
<div className={styles['promo-banner-message']}>
<span className="flex flex-col md:flex-row 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>
);
}
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 ab044fe

Please sign in to comment.