From e43b8a65bbd0ce0b6186cd9240b7e79ac1ddaad7 Mon Sep 17 00:00:00 2001 From: JonnieSparkles Date: Mon, 27 Apr 2026 08:43:59 -0400 Subject: [PATCH] feat: add AnnouncementBanner component for migration notification - Introduced a new AnnouncementBanner component to display migration information for ar.io users. - Integrated the banner into the main layout with customizable props for mobile text, badge, and call-to-action link. - The banner supports dismissal and persists the user's choice in local storage. --- src/app/layout.tsx | 12 +++ src/components/announcement-banner.tsx | 132 +++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/components/announcement-banner.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 02c132494..83ffaf252 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import Script from "next/script"; import SearchDialog from "@/components/search"; import { AskArieProvider } from "@/components/ask-arie/AskArieContext"; import { AskArieWidget } from "@/components/ask-arie/AskArieWidget"; +import { AnnouncementBanner } from "@/components/announcement-banner"; import { Plus_Jakarta_Sans } from "next/font/google"; const plusJakartaSans = Plus_Jakarta_Sans({ @@ -50,6 +51,17 @@ export default function Layout({ children }: LayoutProps<"/">) { }} > + + The ar.io smart contract and token are migrating to Solana. + Register now to migrate your assets before the deadline. + {children} diff --git a/src/components/announcement-banner.tsx b/src/components/announcement-banner.tsx new file mode 100644 index 000000000..d8b2ba88e --- /dev/null +++ b/src/components/announcement-banner.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { X } from "lucide-react"; +import Link from "next/link"; +import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; + +type AnnouncementBannerProps = { + storageKey: string; + children: ReactNode; + mobileText?: string; + badgeText?: string; + ctaHref?: string; + ctaLabel?: string; + dismissAriaLabel?: string; +}; + +export function AnnouncementBanner({ + storageKey, + children, + mobileText, + badgeText, + ctaHref, + ctaLabel, + dismissAriaLabel = "Dismiss announcement", +}: AnnouncementBannerProps) { + const [isVisible, setIsVisible] = useState(false); + const bannerRef = useRef(null); + const dismissKey = useMemo( + () => `announcement-banner-dismissed:${storageKey}`, + [storageKey], + ); + + useEffect(() => { + const isDismissed = localStorage.getItem(dismissKey) === "true"; + setIsVisible(!isDismissed); + }, [dismissKey]); + + const dismissBanner = () => { + localStorage.setItem(dismissKey, "true"); + setIsVisible(false); + }; + + useEffect(() => { + if (!isVisible) { + document.documentElement.style.setProperty("--fd-banner-height", "0px"); + return; + } + + const bannerElement = bannerRef.current; + if (!bannerElement) { + return; + } + + const updateBannerHeight = () => { + document.documentElement.style.setProperty( + "--fd-banner-height", + `${bannerElement.offsetHeight}px`, + ); + }; + + updateBannerHeight(); + + const resizeObserver = new ResizeObserver(updateBannerHeight); + resizeObserver.observe(bannerElement); + + return () => { + resizeObserver.disconnect(); + document.documentElement.style.setProperty("--fd-banner-height", "0px"); + }; + }, [isVisible]); + + if (!isVisible) { + return null; + } + + return ( +
+
+ {badgeText ? ( + + {badgeText} + + ) : null} +

+ {mobileText ? {mobileText} : null} + {children} +

+ {ctaHref && ctaLabel ? ( + + {ctaLabel} → + + ) : null} +
+ {ctaHref && ctaLabel ? ( + + {ctaLabel} → + + ) : null} + +
+ +
+
+ ); +}