|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | 3 | import {useEffect, useState} from 'react'; |
4 | | -import Image from 'next/image'; |
5 | 4 |
|
6 | 5 | import styles from './banner.module.scss'; |
7 | 6 |
|
8 | | -// |
9 | | -// BANNER CONFIGURATION |
10 | | -// This is a lazy way of doing things but will work until |
11 | | -// we put a more robust solution in place. |
12 | | -// |
13 | | -const SHOW_BANNER = false; |
14 | | -const BANNER_TEXT = |
15 | | - 'Behind the Code: A Conversation With Backend Experts featuring CEOs of Laravel, Prisma, and Supabase.'; |
16 | | -const BANNER_LINK_URL = |
17 | | - 'https://sentry.io/resources/behind-the-code-a-discussion-with-backend-experts/'; |
18 | | -const BANNER_LINK_TEXT = 'RSVP'; |
19 | | -const OPTIONAL_BANNER_IMAGE = null; |
| 7 | +type BannerType = { |
| 8 | + /** This is an array of strings or RegExps to feed into new RegExp() */ |
| 9 | + appearsOn: (string | RegExp)[]; |
| 10 | + /** The label for the call to action button */ |
| 11 | + linkText: string; |
| 12 | + /** The destination url of the call to action button */ |
| 13 | + linkURL: string; |
| 14 | + /** The main text of the banner */ |
| 15 | + text: string; |
| 16 | + /** Optional ISO Date string that will hide the banner after this date without the need for a rebuild */ |
| 17 | + expiresOn?: string; |
| 18 | +}; |
20 | 19 |
|
| 20 | +// BANNERS is an array of banner objects. You can add as many as you like. If |
| 21 | +// you need to disable all banners, simply delete them from the array. Each banner |
| 22 | +// is evaluated in order, and the first one that matches will be shown. |
| 23 | +// |
| 24 | +// Examples: |
| 25 | +// appearsOn = []; // This is disabled |
| 26 | +// appearsOn = ['^/$']; // This is enabled on the home page |
| 27 | +// appearsOn = ['^/welcome/']; // This is enabled on the "/welcome" page |
| 28 | +// const BANNERS = [ |
21 | 29 | // |
22 | | -// BANNER CODE |
23 | | -// Don't edit unless you need to change how the banner works. |
| 30 | +// This one will take precedence over the last banner in the array |
| 31 | +// (which matches all /platforms pages), because it matches first. |
| 32 | +// { |
| 33 | +// appearsOn: ['^/platforms/javascript/guides/astro/'], |
| 34 | +// text: 'This banner appears on the Astro guide', |
| 35 | +// linkURL: 'https://sentry.io/thought-leadership', |
| 36 | +// linkText: 'Get webinarly', |
| 37 | +// }, |
24 | 38 | // |
| 39 | +// // This one will match the /welcome page and all /for pages |
| 40 | +// { |
| 41 | +// appearsOn: ['^/$', '^/platforms/'], |
| 42 | +// text: 'This banner appears on the home page and all /platforms pages', |
| 43 | +// linkURL: 'https://sentry.io/thought-leadership', |
| 44 | +// linkText: 'Get webinarly', |
| 45 | +// }, |
| 46 | +// ]; |
| 47 | + |
| 48 | +const BANNERS: BannerType[] = [ |
| 49 | + /// ⚠️ KEEP THIS LAST BANNER ACTIVE FOR DOCUMENTATION |
| 50 | + // check it out on `/contributing/pages/banners/` |
| 51 | + { |
| 52 | + appearsOn: ['^/contributing/pages/banners/'], |
| 53 | + text: 'Edit this banner on `/src/components/banner/index.tsx`', |
| 54 | + linkURL: 'https://docs.sentry.io/contributing/pages/banners/', |
| 55 | + linkText: 'CTA', |
| 56 | + }, |
| 57 | +]; |
25 | 58 |
|
26 | 59 | const LOCALSTORAGE_NAMESPACE = 'banner-manifest'; |
27 | 60 |
|
| 61 | +// https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript |
28 | 62 | const fastHash = (input: string) => { |
29 | 63 | let hash = 0; |
30 | 64 | if (input.length === 0) { |
@@ -52,53 +86,63 @@ const readOrResetLocalStorage = () => { |
52 | 86 | } |
53 | 87 | }; |
54 | 88 |
|
55 | | -export function Banner({isModule = false}) { |
56 | | - const [isVisible, setIsVisible] = useState(false); |
57 | | - const hash = fastHash(`${BANNER_TEXT}:${BANNER_LINK_URL}`).toString(); |
58 | | - |
59 | | - const enablebanner = () => { |
60 | | - setIsVisible(true); |
61 | | - }; |
| 89 | +export function Banner() { |
| 90 | + type BannerWithHash = BannerType & {hash: string}; |
| 91 | + const [banner, setBanner] = useState<BannerWithHash | null>(null); |
62 | 92 |
|
63 | 93 | useEffect(() => { |
64 | | - const manifest = readOrResetLocalStorage(); |
65 | | - if (!manifest) { |
66 | | - enablebanner(); |
| 94 | + const matchingBanner = BANNERS.find(b => { |
| 95 | + return b.appearsOn.some(matcher => |
| 96 | + new RegExp(matcher).test(window.location.pathname) |
| 97 | + ); |
| 98 | + }); |
| 99 | + |
| 100 | + // Bail if no banner matches this page or if the banner has expired |
| 101 | + if ( |
| 102 | + !matchingBanner || |
| 103 | + (matchingBanner.expiresOn && |
| 104 | + new Date() > new Date(matchingBanner.expiresOn ?? null)) |
| 105 | + ) { |
67 | 106 | return; |
68 | 107 | } |
69 | 108 |
|
70 | | - if (manifest.indexOf(hash) === -1) { |
71 | | - enablebanner(); |
| 109 | + const manifest = readOrResetLocalStorage(); |
| 110 | + const hash = fastHash(matchingBanner.text + matchingBanner.linkURL).toString(); |
| 111 | + |
| 112 | + // Bail if this banner has already been seen |
| 113 | + if (manifest && manifest.indexOf(hash) >= 0) { |
| 114 | + return; |
72 | 115 | } |
73 | | - }); |
74 | | - |
75 | | - return SHOW_BANNER |
76 | | - ? isVisible && ( |
77 | | - <div |
78 | | - className={[styles['promo-banner'], isModule && styles['banner-module']] |
79 | | - .filter(Boolean) |
80 | | - .join(' ')} |
81 | | - > |
82 | | - <div className={styles['promo-banner-message']}> |
83 | | - {OPTIONAL_BANNER_IMAGE ? <Image src={OPTIONAL_BANNER_IMAGE} alt="" /> : ''} |
84 | | - <span> |
85 | | - {BANNER_TEXT} |
86 | | - <a href={BANNER_LINK_URL}>{BANNER_LINK_TEXT}</a> |
87 | | - </span> |
88 | | - </div> |
89 | | - <button |
90 | | - className={styles['promo-banner-dismiss']} |
91 | | - role="button" |
92 | | - onClick={() => { |
93 | | - const manifest = readOrResetLocalStorage() || []; |
94 | | - const payload = JSON.stringify([...manifest, hash]); |
95 | | - localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload); |
96 | | - setIsVisible(false); |
97 | | - }} |
98 | | - > |
99 | | - × |
100 | | - </button> |
101 | | - </div> |
102 | | - ) |
103 | | - : null; |
| 116 | + |
| 117 | + // Enable the banner |
| 118 | + setBanner({...matchingBanner, hash}); |
| 119 | + }, []); |
| 120 | + |
| 121 | + if (!banner) { |
| 122 | + return null; |
| 123 | + } |
| 124 | + return ( |
| 125 | + <div className={[styles['promo-banner']].filter(Boolean).join(' ')}> |
| 126 | + <div className={styles['promo-banner-message']}> |
| 127 | + <span className="flex flex-col md:flex-row gap-4"> |
| 128 | + {banner.text} |
| 129 | + <a href={banner.linkURL} className="min-w-max"> |
| 130 | + {banner.linkText} |
| 131 | + </a> |
| 132 | + </span> |
| 133 | + </div> |
| 134 | + <button |
| 135 | + className={styles['promo-banner-dismiss']} |
| 136 | + role="button" |
| 137 | + onClick={() => { |
| 138 | + const manifest = readOrResetLocalStorage() || []; |
| 139 | + const payload = JSON.stringify([...manifest, banner.hash]); |
| 140 | + localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload); |
| 141 | + setBanner(null); |
| 142 | + }} |
| 143 | + > |
| 144 | + × |
| 145 | + </button> |
| 146 | + </div> |
| 147 | + ); |
104 | 148 | } |
0 commit comments