From f3d5b97eb781e20ad73d06597c88bd67303a9083 Mon Sep 17 00:00:00 2001 From: Benjamin Goering <171782+gobengo@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:39:58 -0800 Subject: [PATCH] feat: add w3up-launch announcement banner (when env.NEXT_PUBLIC_W3UP_LAUNCH_SUNSET_ANNOUNCEMENT_START is set) (#2319) Motivation: * https://github.com/web3-storage/secrets/issues/20 Tactics: * there is a new env var `NEXT_PUBLIC_W3UP_LAUNCH_SUNSET_ANNOUNCEMENT_START` that is an ISO8601 datetime of when the announcement should appear. If unset, the announcement will never appear. My intention is to set it only on the staging environment for acceptance testing. Once we're happy on staging, we can enable it on production by setting this var to whatever datetime we want it to appear. Scope: * when enabled at after the appropriate time, the banner should appear on the following routes * / * /account * /account/payment * /docs * the github actions `website` workflow now uses github environments. This is because I want to use them to configure the staging environment to have a particular value for `NEXT_PUBLIC_W3UP_LAUNCH_SUNSET_ANNOUNCEMENT_START` --- .github/workflows/website.yml | 3 + package-lock.json | 4 +- .../messagebanner/messagebanner.scss | 4 ++ .../page-banner/page-banner-portal.js | 59 +++++++++++++++++++ packages/website/components/w3up-launch.js | 59 +++++++++++++++++++ packages/website/modules/docs-theme/index.js | 14 +++-- packages/website/pages/_app.js | 35 ++++++++--- packages/website/pages/_document.js | 7 +++ packages/website/pages/account/index.js | 7 +++ packages/website/pages/account/payment.js | 8 ++- packages/website/pages/index.js | 8 +++ 11 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 packages/website/components/page-banner/page-banner-portal.js create mode 100644 packages/website/components/w3up-launch.js diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 3ad69b9d53..ec7f62f6cc 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -177,6 +177,9 @@ jobs: core.setFailed('e2e tests did not succeed') preview: name: Preview + environment: + name: ${{ (github.ref_name == 'main') && 'staging' || format('preview-{0}', github.ref_name) }} + url: ${{ (github.ref_name == 'main') && 'https://staging.web3.storage/' || steps.cloudflare_url.outputs.stdout }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/package-lock.json b/package-lock.json index 2b07b21f47..d4b5bdfc97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23830,7 +23830,7 @@ }, "packages/api": { "name": "@web3-storage/api", - "version": "7.18.1", + "version": "7.20.0", "license": "(Apache-2.0 OR MIT)", "dependencies": { "@aws-sdk/client-s3": "^3.53.1", @@ -26650,7 +26650,7 @@ }, "packages/website": { "name": "@web3-storage/website", - "version": "2.36.1", + "version": "2.36.3", "dependencies": { "@docsearch/react": "^3.0.0", "@fortawesome/free-brands-svg-icons": "^6.1.2", diff --git a/packages/website/components/messagebanner/messagebanner.scss b/packages/website/components/messagebanner/messagebanner.scss index 487642bbe1..43341872c0 100644 --- a/packages/website/components/messagebanner/messagebanner.scss +++ b/packages/website/components/messagebanner/messagebanner.scss @@ -75,6 +75,10 @@ } } +.message-banner-underline-links *:link { + text-decoration: underline; +} + .message-banner-close-button { position: absolute; display: flex; diff --git a/packages/website/components/page-banner/page-banner-portal.js b/packages/website/components/page-banner/page-banner-portal.js new file mode 100644 index 0000000000..d94e6aa1de --- /dev/null +++ b/packages/website/components/page-banner/page-banner-portal.js @@ -0,0 +1,59 @@ +/** + * @fileoverview component that contains a 'page banner', i.e. a banner across the whole + * web page that shows a prominent message to the end-user. + */ + +import clsx from 'clsx'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +export const defaultPortalElementId = 'page-banner-portal'; +export const defaultContentsClassName = 'page-banner-contents'; +export const defaultPortalQuerySelector = `#${defaultPortalElementId} .${defaultContentsClassName}`; + +/** + * wrap children in stiles like a .message-banner (see ../messagebanner/messagebanner.scss) + */ +export function MessageBannerStyled({ children }) { + return ( +
+
+
+ {children} +
+
+
+ ); +} + +/** + * component for an element across the top of page that can host banner messages on a per-page basis. + * Other components will use ReactDOM.createPortal() to render into this even though it isn't a child of + * that component (since it needs to be atop the page). + */ +export const PageBannerPortal = ({ id, contentsRef, contentsClassName = defaultContentsClassName }) => { + return ( +
+
+
+ ); +}; + +/** + * React Context used for passing around the HTMLElement for the PageBannerPortal + * so that the PageBanner component can pass it to React.createPortal. + */ +export const PageBannerPortalContainerContext = React.createContext(undefined); + +/** + * render children into a PageBannerPortal at the top of the page + */ +export const PageBanner = ({ children }) => { + const container = React.useContext(PageBannerPortalContainerContext); + const bannerChild = ( +
+ {children} +
+ ); + return <>{container && children && ReactDOM.createPortal(bannerChild, container)}; +}; diff --git a/packages/website/components/w3up-launch.js b/packages/website/components/w3up-launch.js new file mode 100644 index 0000000000..d07d21c8b3 --- /dev/null +++ b/packages/website/components/w3up-launch.js @@ -0,0 +1,59 @@ +import * as React from 'react'; + +const sunsetStartEnv = process.env.NEXT_PUBLIC_W3UP_LAUNCH_SUNSET_START ?? '2023-01-09T00:00:00Z'; +const sunsetStartDate = new Date(Date.parse(sunsetStartEnv)); + +/** + * If this isn't set, no announcements will appear + */ +const sunsetAnnouncementStartEnv = process.env.NEXT_PUBLIC_W3UP_LAUNCH_SUNSET_ANNOUNCEMENT_START; + +/** + * after this datetime, show announcements that web3.storage is sunset + * and end-users should switch to w3up/console.web3.storage + */ +const sunsetAnnouncementStartDate = sunsetAnnouncementStartEnv + ? new Date(Date.parse(sunsetAnnouncementStartEnv)) + : undefined; + +/** + * Return whether sunset announcements related to w3up-launch should be shown. + * An announcement date must be explicitly configured via env var, and now must be after that date. + * @param {Date} at - time at which to return whether to show the announcement + * @param {Date|undefined} [announcementStartDate] - when to begin showing announcements. + * If not provided, always return false. + */ +export const shouldShowSunsetAnnouncement = (at = new Date(), announcementStartDate = sunsetAnnouncementStartDate) => { + return announcementStartDate && at > announcementStartDate; +}; + +export const w3upLaunchConfig = { + type: 'W3upLaunchConfig', + stages: { + sunsetAnnouncement: { + start: sunsetAnnouncementStartDate, + }, + sunset: { + start: sunsetStartDate, + }, + }, + shouldShowSunsetAnnouncement: shouldShowSunsetAnnouncement(), +}; + +/** + * copy for banner message across top of some web3.storage pages when w3up ships + */ +export const W3upMigrationRecommendationCopy = () => { + const createNewAccountHref = 'https://console.web3.storage/?intent=create-account'; + const learnWhatsNewHref = 'https://console.web3.storage/?intent=learn-new-web3storage-experience'; + const sunsetDateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: 'long' }); + return ( + <> + This web3.storage product will sunset on {sunsetDateFormatter.format(sunsetStartDate)}. We recommend migrating + your usage of web3.storage to the new web3.storage. +
+ Click here to create a new account and  + here to read about what’s awesome about the new web3.storage experience. + + ); +}; diff --git a/packages/website/modules/docs-theme/index.js b/packages/website/modules/docs-theme/index.js index 9de049f56c..3e3ca087e4 100644 --- a/packages/website/modules/docs-theme/index.js +++ b/packages/website/modules/docs-theme/index.js @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; import { MDXProvider } from '@mdx-js/react'; - import Head from 'next/head'; import { useRouter } from 'next/router'; import hljs from 'highlight.js/lib/core'; @@ -9,10 +8,13 @@ import powershell from 'highlight.js/lib/languages/powershell'; import bash from 'highlight.js/lib/languages/bash'; import go from 'highlight.js/lib/languages/go'; import json from 'highlight.js/lib/languages/json'; + import Sidebar from './sidebar/sidebar'; import Feedback from './feedback/feedback'; import Toc from './toc/toc'; import DocsPagination from './docspagination/docspagination'; +import { W3upMigrationRecommendationCopy, shouldShowSunsetAnnouncement } from '../../components/w3up-launch.js'; +import * as PageBannerPortal from '../../components/page-banner/page-banner-portal.js'; hljs.registerLanguage('javascript', javascript); hljs.registerLanguage('powershell', powershell); @@ -27,7 +29,7 @@ export default function Docs(props) { useEffect(() => { const pres = document.querySelectorAll('pre'); - pres.forEach((pre) => { + pres.forEach(pre => { const code = pre.firstElementChild?.innerHTML; if (code) { const button = document.createElement('button'); @@ -39,7 +41,7 @@ export default function Docs(props) { button.classList.add('code-copy-button'); document.body.appendChild(textarea); - button.addEventListener('click', (event) => { + button.addEventListener('click', event => { if (!navigator.clipboard) { textarea.focus({ preventScroll: true }); textarea.select(); @@ -63,7 +65,6 @@ export default function Docs(props) { pre.appendChild(button); } - }); hljs.highlightAll(); @@ -91,6 +92,11 @@ export default function Docs(props) { return function Layout({ children }) { return ( <> + {shouldShowSunsetAnnouncement() && ( + + + + )} {sharedHead}
diff --git a/packages/website/pages/_app.js b/packages/website/pages/_app.js index 80da76b4d1..1fae4f0d74 100644 --- a/packages/website/pages/_app.js +++ b/packages/website/pages/_app.js @@ -1,12 +1,17 @@ import '../styles/global.scss'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import clsx from 'clsx'; import Script from 'next/script'; import Metadata from 'components/general/metadata'; import RestrictedRoute from 'components/general/restrictedRoute'; import AppProviders from 'components/general/appProviders'; +import { + PageBannerPortal, + defaultPortalElementId, + PageBannerPortalContainerContext, +} from '../components/page-banner/page-banner-portal.js'; import MessageBanner from '../components/messagebanner/messagebanner.js'; import Navigation from '../components/navigation/navigation.js'; import Footer from '../components/footer/footer.js'; @@ -19,6 +24,7 @@ const App = ({ Component, pageProps }) => { const productRoutes = ['/login', '/account', '/account/payment', '/tokens', '/callback']; const productApp = productRoutes.includes(pathname); const pageClass = pathname.includes('docs') ? 'docs-site' : productApp ? 'product-app' : 'marketing-site'; + const [pageBannerPortalEl, setPageBannerPortalEl] = useState(); useEffect(() => { document.querySelector('body')?.classList.add(pageClass); @@ -41,13 +47,28 @@ const App = ({ Component, pageProps }) => { referrerPolicy="no-referrer-when-downgrade" /> -
- {productApp &&
} - - - -