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/.github/CODEOWNERS b/.github/CODEOWNERS
index 748c1c91..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 @jeremy0x be requested for
+# @chibie @onahprosper @5ran6 be requested for
# review when someone opens a pull request.
-* @chibie @jeremy0x
+* @chibie @onahprosper @5ran6
diff --git a/app/components/AppLayout.tsx b/app/components/AppLayout.tsx
index cbe2c33c..14d84856 100644
--- a/app/components/AppLayout.tsx
+++ b/app/components/AppLayout.tsx
@@ -11,6 +11,7 @@ import {
PWAInstall,
NoticeBanner,
} from "./index";
+import { MaintenanceNoticeModal, MaintenanceBanner } from "./MaintenanceNoticeModal";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
@@ -18,8 +19,12 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
- {config.noticeBannerText && (
-
+ {config.maintenanceEnabled ? (
+
+ ) : (
+ config.noticeBannerText && (
+
+ )
)}
}>
@@ -27,6 +32,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..4ecbdf1f
--- /dev/null
+++ b/app/components/MaintenanceNoticeModal.tsx
@@ -0,0 +1,239 @@
+"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}
+ ),
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// 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;
+}
+
+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 */}
+
+
+
+
+
+
+
+
+
+
+
+ {config.title}
+
+ {config.titleEmphasis}
+
+
+
+
+ {config.footnote && (
+
+ {config.footnote}
+
+ )}
+
+
+ {config.buttonText}
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/app/components/index.ts b/app/components/index.ts
index fcb264eb..65de5b74 100644
--- a/app/components/index.ts
+++ b/app/components/index.ts
@@ -54,4 +54,8 @@ export { FundWalletForm } from "./FundWalletForm";
export { TransferForm } from "./TransferForm";
export { default as NoticeBanner } from "./NoticeBanner";
+export {
+ MaintenanceNoticeModal,
+ MaintenanceBanner,
+} 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/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 = {
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: [],