From 033edf59ce6e03e192f3f9f80b4507b801f9f22e Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Fri, 31 Jul 2026 13:19:42 -0700 Subject: [PATCH] chore: renderatl --- src/components/RenderATL/RenderATL.jsx | 283 +++++++++++++++++++++++++ src/components/Site/Site.jsx | 3 + src/components/Splash/Splash.jsx | 3 + src/components/Sponsors/Sponsors.jsx | 3 + 4 files changed, 292 insertions(+) create mode 100644 src/components/RenderATL/RenderATL.jsx 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 ( +
+
+

+ 2026 + · Atlanta · Aug 12–13 +

+ +

+ {priceSentence()} +

+ + + Get tickets + + + +
+
+ ); +} + +// Splash-page section: the promo composed as an admission ticket — webpack-blue +// face, perforation, and an unthemed paper stub carrying the actual pricing +// data. +export function RenderATLPromo() { + const mounted = useMounted(); + + if (mounted === false || isOver()) return null; + + const next = nextTier(); + + return ( + + ); +} + +// Sidebar card: a miniature of the same ticket, sized to the sponsor column. +export function RenderATLCard() { + const mounted = useMounted(); + + if (mounted === false || isOver()) return null; + + return ( +
+ +
+

+ {CITY} · {DATES} +

+

+ +
+ 2026 +

+
+ +
+ + ); +} diff --git a/src/components/Site/Site.jsx b/src/components/Site/Site.jsx index 20686fb6d286..49daf4fd5d2b 100644 --- a/src/components/Site/Site.jsx +++ b/src/components/Site/Site.jsx @@ -33,6 +33,7 @@ import OfflineBanner from "../OfflineBanner/OfflineBanner.jsx"; import Page from "../Page/Page.jsx"; import PageNotFound from "../PageNotFound/PageNotFound.jsx"; import ReadingProgress from "../ReadingProgress/ReadingProgress.jsx"; +import { RenderATLBanner } from "../RenderATL/RenderATL.jsx"; import ScrollToTop from "../ScrollToTop/ScrollToTop.jsx"; import Sidebar from "../Sidebar/Sidebar.jsx"; import SidebarMobile from "../SidebarMobile/SidebarMobile.jsx"; @@ -302,6 +303,8 @@ function Site(props) { {location.pathname !== "/" && }
+ + {mounted ? ( import("../Support/Support.jsx")); @@ -55,6 +56,8 @@ const Splash = () => {
+ +
diff --git a/src/components/Sponsors/Sponsors.jsx b/src/components/Sponsors/Sponsors.jsx index 9063e7399a91..dc19d2ea42f7 100644 --- a/src/components/Sponsors/Sponsors.jsx +++ b/src/components/Sponsors/Sponsors.jsx @@ -4,6 +4,7 @@ import AGLogoDark from "../../assets/ag-grid-logo-dark.png"; import AGLogo from "../../assets/ag-grid-logo.png"; import WebpackIcon from "../../assets/icon-square-small.svg"; import Link from "../Link/Link.jsx"; +import { RenderATLCard } from "../RenderATL/RenderATL.jsx"; // Tailwind CSS is used for styling @@ -56,6 +57,8 @@ const Sponsors = () => (
+ + {/* Webpack Sponsor */}