diff --git a/src/components/RenderATL/RenderATL.jsx b/src/components/RenderATL/RenderATL.jsx new file mode 100644 index 000000000000..a5c2e246e6f1 --- /dev/null +++ b/src/components/RenderATL/RenderATL.jsx @@ -0,0 +1,283 @@ +import PropTypes from "prop-types"; +import { useEffect, useState, useSyncExternalStore } from "react"; +import CloseIcon from "../../styles/icons/cross.svg"; +import testLocalStorage from "../../utilities/test-local-storage.js"; +import Container from "../Container/Container.jsx"; +import Link from "../Link/Link.jsx"; + +// Compared against the day after the conference closes, US Eastern. +const isOver = () => new Date() >= new Date("2026-08-14T00:00:00-04:00"); + +const PRICING = [ + { from: null, price: 750 }, // "Late" + { from: "2026-08-04T00:00:00-04:00", price: 1000 }, // "Final" +]; + +const started = ({ from }) => from === null || new Date(from) <= new Date(); +const currentTier = () => PRICING.findLast(started); +const nextTier = () => PRICING.find((tier) => started(tier) === false); + +const money = (amount) => `$${amount.toLocaleString("en-US")}`; + +// "rises to $1,000 on August 4", or just the price once the final tier lands. +const priceSentence = () => { + const next = nextTier(); + + if (next === undefined) { + return `General admission is ${money(currentTier().price)}.`; + } + + const on = new Date(next.from).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + timeZone: "America/New_York", + }); + + return `General admission is ${money(currentTier().price)} and rises to ${money(next.price)} on ${on}.`; +}; + +const UTM = "utm_source=webpack&utm_medium=banner&utm_campaign=renderatl-2026"; +const TICKETS_URL = `https://www.renderatl.com/tickets?${UTM}`; + +const DATES = "Aug 12–13, 2026"; +const CITY = "Atlanta, GA"; + +const DISMISS_KEY = "renderatl-2026-dismissed"; +const localStorageIsEnabled = testLocalStorage() !== false; + +const mono = + "[font-family:'Source_Code_Pro',ui-monospace,monospace] font-semibold uppercase"; + +const subscribe = () => () => {}; +const useMounted = () => + useSyncExternalStore( + subscribe, + () => true, + () => false, + ); + +// Shared brand mark. Webpack's light blue is the one loud accent on every +// surface (colors follow the site palette in tailwind.config.js). +const Wordmark = ({ className = "" }) => ( + Render(ATL) +); + +Wordmark.propTypes = { + className: PropTypes.string, +}; + +const shortDate = (iso) => + new Date(iso).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + timeZone: "America/New_York", + }); + +// Slim dismissible strip docked to the bottom of the viewport (the site +// header is fixed, so the top of the page is spoken for). +export function RenderATLBanner() { + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (isOver()) return; + if (localStorageIsEnabled && localStorage.getItem(DISMISS_KEY) === "1") { + return; + } + // eslint-disable-next-line react-hooks/set-state-in-effect + setVisible(true); + }, []); + + const dismiss = () => { + if (localStorageIsEnabled) { + localStorage.setItem(DISMISS_KEY, "1"); + } + setVisible(false); + }; + + if (visible === false) return null; + + return ( +
+
+ {priceSentence()} +
+ + + Get tickets + + + ++ {CITY} · {DATES} +
+
+
+ 2026
+
+ Admit one + + {money(currentTier().price)} + +
+