From f6ca7bb91e169e7e620f4fe35fd4c4a1ab9e9e63 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Fri, 13 Feb 2026 16:57:27 +0100 Subject: [PATCH 1/4] feat: add maintenance notice modal and configuration options to enhance user experience during scheduled maintenance --- .env.example | 7 + app/components/AppLayout.tsx | 2 + app/components/MaintenanceNoticeModal.tsx | 182 ++++++++++++++++++++++ app/components/index.ts | 1 + app/globals.css | 1 + public/images/tokens.svg | 43 +++++ tailwind.config.ts | 3 + 7 files changed, 239 insertions(+) create mode 100644 app/components/MaintenanceNoticeModal.tsx create mode 100644 public/images/tokens.svg diff --git a/.env.example b/.env.example index db42dfa2..9b4764b3 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,13 @@ NEXT_PUBLIC_GOOGLE_VERIFICATION_CODE= # See docs/notice-banner.md NEXT_PUBLIC_NOTICE_BANNER_TEXT= +# Maintenance Notice Modal +# Set to any truthy value (e.g. "1") to show the maintenance notice overlay. +# SCHEDULE is the bold date/time string shown in the notice body. +# Changing the schedule auto-resets dismissals so users see the new notice. +NEXT_PUBLIC_MAINTENANCE_NOTICE_ENABLED=true +NEXT_PUBLIC_MAINTENANCE_SCHEDULE=Friday, February 13th, from 7:00 PM to 11:00 PM WAT + # Brevo Email Marketing # Get from: Brevo Dashboard → Settings → API Keys BREVO_API_KEY= diff --git a/app/components/AppLayout.tsx b/app/components/AppLayout.tsx index cbe2c33c..428d5168 100644 --- a/app/components/AppLayout.tsx +++ b/app/components/AppLayout.tsx @@ -11,6 +11,7 @@ import { PWAInstall, NoticeBanner, } from "./index"; +import { MaintenanceNoticeModal } from "./MaintenanceNoticeModal"; export default function AppLayout({ children }: { children: React.ReactNode }) { return ( @@ -27,6 +28,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { + {/* Brevo Chat Widget */} {/^[a-f0-9]{24}$/i.test(config.brevoConversationsId) && config.brevoConversationsGroupId && ( diff --git a/app/components/MaintenanceNoticeModal.tsx b/app/components/MaintenanceNoticeModal.tsx new file mode 100644 index 00000000..4475c311 --- /dev/null +++ b/app/components/MaintenanceNoticeModal.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Image from "next/image"; +import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react"; +import { motion, AnimatePresence } from "framer-motion"; + +export interface MaintenanceNoticeConfig { + /** Master toggle – when false the modal never renders. */ + enabled: boolean; + title: string; + titleEmphasis: string; + body: string; + footnote?: string; + buttonText: string; + noticeKey: string; +} + + +const SCHEDULE = + process.env.NEXT_PUBLIC_MAINTENANCE_SCHEDULE || ""; + +const ENABLED = !!process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE_ENABLED; + +/** Simple deterministic hash for the noticeKey so each schedule gets its own. */ +function simpleHash(str: string): string { + let h = 0; + for (let i = 0; i < str.length; i++) { + h = (Math.imul(31, h) + str.charCodeAt(i)) | 0; + } + return Math.abs(h).toString(36); +} + +export const DEFAULT_MAINTENANCE_CONFIG: MaintenanceNoticeConfig = { + enabled: ENABLED && SCHEDULE.length > 0, + title: "We are improving", + titleEmphasis: "your experience.", + body: `A scheduled maintenance will take place on **${SCHEDULE}** to help improve your experience. Some services may be temporarily unavailable during this time.`, + footnote: + "We appreciate your patience while we make these improvements.", + buttonText: "Got it", + noticeKey: `maintenance-${simpleHash(SCHEDULE)}`, +}; + + +const STORAGE_PREFIX = "noblocks_notice_dismissed_"; + +function isDismissed(key: string): boolean { + if (typeof window === "undefined") return false; + try { + return localStorage.getItem(`${STORAGE_PREFIX}${key}`) === "1"; + } catch { + return false; + } +} + +function markDismissed(key: string) { + try { + localStorage.setItem(`${STORAGE_PREFIX}${key}`, "1"); + } catch { + // Storage may be unavailable (private browsing etc.) + } +} + +/** Render a string where **bold** segments become . */ +function RichText({ text, className }: { text: string; className?: string }) { + const parts = text.split(/(\*\*[^*]+\*\*)/g); + return ( +

+ {parts.map((part, i) => + part.startsWith("**") && part.endsWith("**") ? ( + + {part.slice(2, -2)} + + ) : ( + {part} + ), + )} +

+ ); +} + +interface MaintenanceNoticeModalProps { + config?: MaintenanceNoticeConfig; +} + +export function MaintenanceNoticeModal({ + config = DEFAULT_MAINTENANCE_CONFIG, +}: MaintenanceNoticeModalProps) { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + if (!config.enabled) return; + if (isDismissed(config.noticeKey)) return; + // Small delay so it doesn't flash on initial page load + const timer = setTimeout(() => setIsOpen(true), 400); + return () => clearTimeout(timer); + }, [config.enabled, config.noticeKey]); + + const dismiss = useCallback(() => { + markDismissed(config.noticeKey); + setIsOpen(false); + }, [config.noticeKey]); + + if (!config.enabled) return null; + + return ( + + {isOpen && ( + + {/* Backdrop */} + + )} + + ); +} diff --git a/app/components/index.ts b/app/components/index.ts index fcb264eb..8ac2536b 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -54,4 +54,5 @@ export { FundWalletForm } from "./FundWalletForm"; export { TransferForm } from "./TransferForm"; export { default as NoticeBanner } from "./NoticeBanner"; +export { MaintenanceNoticeModal } from "./MaintenanceNoticeModal"; export { default as AppLayout } from "./AppLayout"; diff --git a/app/globals.css b/app/globals.css index 365569e4..5dd5b9cc 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,4 @@ +@import url('https://fonts.googleapis.com'); @tailwind base; @tailwind components; @tailwind utilities; diff --git a/public/images/tokens.svg b/public/images/tokens.svg new file mode 100644 index 00000000..5b2c4f8a --- /dev/null +++ b/public/images/tokens.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tailwind.config.ts b/tailwind.config.ts index 6a9e86db..a90556c2 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -91,6 +91,9 @@ const config: Config = { animation: { "rocket-shake": "rocket-shake 0.7s infinite", }, + fontFamily: { + 'inter': ['"Inter"', 'system-ui', 'arial', 'sans-serif'], + }, }, }, plugins: [], From 9480d2ec383e893a0d0df3a57d4fa2555f6e6618 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Fri, 13 Feb 2026 17:18:26 +0100 Subject: [PATCH 2/4] feat: implement maintenance banner and update configuration to manage maintenance notices --- app/components/AppLayout.tsx | 10 ++-- app/components/MaintenanceNoticeModal.tsx | 57 +++++++++++++++++++++++ app/components/index.ts | 5 +- app/lib/config.ts | 5 ++ app/types.ts | 2 + 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/app/components/AppLayout.tsx b/app/components/AppLayout.tsx index 428d5168..14d84856 100644 --- a/app/components/AppLayout.tsx +++ b/app/components/AppLayout.tsx @@ -11,7 +11,7 @@ import { PWAInstall, NoticeBanner, } from "./index"; -import { MaintenanceNoticeModal } from "./MaintenanceNoticeModal"; +import { MaintenanceNoticeModal, MaintenanceBanner } from "./MaintenanceNoticeModal"; export default function AppLayout({ children }: { children: React.ReactNode }) { return ( @@ -19,8 +19,12 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
- {config.noticeBannerText && ( - + {config.maintenanceEnabled ? ( + + ) : ( + config.noticeBannerText && ( + + ) )}
}> diff --git a/app/components/MaintenanceNoticeModal.tsx b/app/components/MaintenanceNoticeModal.tsx index 4475c311..4ecbdf1f 100644 --- a/app/components/MaintenanceNoticeModal.tsx +++ b/app/components/MaintenanceNoticeModal.tsx @@ -80,6 +80,63 @@ function RichText({ text, className }: { text: string; className?: string }) { ); } +// --------------------------------------------------------------------------- +// Persistent top-bar banner (does NOT dismiss — visible as long as enabled) +// --------------------------------------------------------------------------- + +export function MaintenanceBanner({ + config = DEFAULT_MAINTENANCE_CONFIG, +}: { + config?: MaintenanceNoticeConfig; +}) { + if (!config.enabled) return null; + + return ( + +
+ {/* Mobile illustration */} +
+ +
+ {/* Desktop illustration */} +
+ +
+ + {/* Copy */} +
+ +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Dismissible modal +// --------------------------------------------------------------------------- + interface MaintenanceNoticeModalProps { config?: MaintenanceNoticeConfig; } diff --git a/app/components/index.ts b/app/components/index.ts index 8ac2536b..65de5b74 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -54,5 +54,8 @@ export { FundWalletForm } from "./FundWalletForm"; export { TransferForm } from "./TransferForm"; export { default as NoticeBanner } from "./NoticeBanner"; -export { MaintenanceNoticeModal } from "./MaintenanceNoticeModal"; +export { + MaintenanceNoticeModal, + MaintenanceBanner, +} from "./MaintenanceNoticeModal"; export { default as AppLayout } from "./AppLayout"; diff --git a/app/lib/config.ts b/app/lib/config.ts index 6a79aa76..4c9fd2b0 100644 --- a/app/lib/config.ts +++ b/app/lib/config.ts @@ -13,6 +13,11 @@ const config: Config = { brevoConversationsGroupId: process.env.NEXT_PUBLIC_BREVO_CONVERSATIONS_GROUP_ID || "", blockfestEndDate: process.env.NEXT_PUBLIC_BLOCKFEST_END_DATE || "2025-10-11T23:59:00+01:00", + maintenanceEnabled: + !!process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE_ENABLED && + !!(process.env.NEXT_PUBLIC_MAINTENANCE_SCHEDULE || "").trim(), + maintenanceSchedule: + process.env.NEXT_PUBLIC_MAINTENANCE_SCHEDULE || "", }; export default config; diff --git a/app/types.ts b/app/types.ts index 78f59334..757edb23 100644 --- a/app/types.ts +++ b/app/types.ts @@ -260,6 +260,8 @@ export type Config = { brevoConversationsId: string; // Brevo chat widget ID brevoConversationsGroupId?: string; // Brevo chat widget group ID for routing blockfestEndDate: string; // BlockFest campaign end date + maintenanceEnabled: boolean; // Maintenance notice modal + banner toggle + maintenanceSchedule: string; // e.g. "Friday, February 13th, from 7:00 PM to 11:00 PM WAT" }; export type Network = { From 22f60b4663f1bfcc34ee312cebaa394f390c2082 Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Fri, 13 Feb 2026 17:52:16 +0100 Subject: [PATCH 3/4] chore: update CODEOWNERS to change review request from @jeremy0x to @onahprosper --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 748c1c91..4cba3690 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -# @chibie @jeremy0x be requested for +# @chibie @onahprosper be requested for # review when someone opens a pull request. -* @chibie @jeremy0x +* @chibie @onahprosper From e207036a8957cbc201d111c258506c621fadcd7d Mon Sep 17 00:00:00 2001 From: Isaac Onyemaechi Date: Fri, 13 Feb 2026 20:17:53 +0100 Subject: [PATCH 4/4] chore: update CODEOWNERS to include @5ran6 as a reviewer for pull requests --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4cba3690..5a8a7fd6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -# @chibie @onahprosper be requested for +# @chibie @onahprosper @5ran6 be requested for # review when someone opens a pull request. -* @chibie @onahprosper +* @chibie @onahprosper @5ran6