Skip to content
Merged
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
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
Loading