diff --git a/docs/contributing/pages/banners.mdx b/docs/contributing/pages/banners.mdx new file mode 100644 index 0000000000000..7acb753b5a4e2 --- /dev/null +++ b/docs/contributing/pages/banners.mdx @@ -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', + }, +]; +``` diff --git a/src/components/banner/banner.module.scss b/src/components/banner/banner.module.scss index b5e5ed1d39914..cec0bf8687cf6 100644 --- a/src/components/banner/banner.module.scss +++ b/src/components/banner/banner.module.scss @@ -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 { @@ -40,13 +32,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; } @@ -54,10 +40,10 @@ .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; @@ -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; } } diff --git a/src/components/banner/index.tsx b/src/components/banner/index.tsx index fa492e6f11efb..178d259592efc 100644 --- a/src/components/banner/index.tsx +++ b/src/components/banner/index.tsx @@ -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) { @@ -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(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 && ( -
-
- {OPTIONAL_BANNER_IMAGE ? : ''} - - {BANNER_TEXT} - {BANNER_LINK_TEXT} - -
- -
- ) - : null; + + // Enable the banner + setBanner({...matchingBanner, hash}); + }, []); + + if (!banner) { + return null; + } + return ( +
+
+ + {banner.text} + + {banner.linkText} + + +
+ +
+ ); } diff --git a/src/components/docPage/index.tsx b/src/components/docPage/index.tsx index 99414018fa069..29878eeb7ea2e 100644 --- a/src/components/docPage/index.tsx +++ b/src/components/docPage/index.tsx @@ -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'; @@ -75,6 +76,9 @@ export function DocPage({ fullWidth ? 'max-w-none w-full' : 'w-[75ch] xl:max-w-[calc(100%-250px)]', ].join(' ')} > +
+ +
{leafNode && }
diff --git a/src/components/home.tsx b/src/components/home.tsx index bb5dec09b10cb..cf408cabd6608 100644 --- a/src/components/home.tsx +++ b/src/components/home.tsx @@ -33,7 +33,9 @@ export async function Home() { return (
- +
+ +