banner and intro fix
This commit is contained in:
@@ -109,6 +109,10 @@
|
|||||||
3. Base styles — applied to plain HTML elements before any classes hit.
|
3. Base styles — applied to plain HTML elements before any classes hit.
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
@layer base {
|
@layer base {
|
||||||
|
html {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
@@ -135,6 +139,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-ui-ready="0"] body {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
4. Site shell + ambient effects
|
4. Site shell + ambient effects
|
||||||
The .site-shell class is on the <main> element in page.tsx. The two
|
The .site-shell class is on the <main> element in page.tsx. The two
|
||||||
|
|||||||
+26
-1
@@ -62,7 +62,32 @@ export default function RootLayout({
|
|||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en-GB" suppressHydrationWarning>
|
<html lang="en-GB" suppressHydrationWarning data-ui-ready="0">
|
||||||
|
<head>
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
(function () {
|
||||||
|
var root = document.documentElement;
|
||||||
|
root.dataset.uiReady = "0";
|
||||||
|
try {
|
||||||
|
var consent = window.localStorage.getItem("novarix-cookie-consent");
|
||||||
|
var introSeen = window.sessionStorage.getItem("novarix-intro-seen");
|
||||||
|
var shouldShowBanner = consent !== "ack" && consent !== "rst";
|
||||||
|
var shouldPlayIntro = !shouldShowBanner && !introSeen;
|
||||||
|
|
||||||
|
root.dataset.showCookieBanner = shouldShowBanner ? "1" : "0";
|
||||||
|
root.dataset.showIntro = shouldPlayIntro ? "1" : "0";
|
||||||
|
} catch (error) {
|
||||||
|
root.dataset.showCookieBanner = "1";
|
||||||
|
root.dataset.showIntro = "0";
|
||||||
|
}
|
||||||
|
root.dataset.uiReady = "1";
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
|
|||||||
+54
-23
@@ -31,6 +31,8 @@ import { openCookieBanner } from "@/components/CookieBanner";
|
|||||||
|
|
||||||
// Type for the mouse-pointer position used to move the background glow.
|
// Type for the mouse-pointer position used to move the background glow.
|
||||||
type PointerState = { x: number; y: number };
|
type PointerState = { x: number; y: number };
|
||||||
|
const INTRO_SEEN_KEY = "novarix-intro-seen";
|
||||||
|
const INTRO_EVENT = "novarix:start-intro";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -40,43 +42,72 @@ export default function HomePage() {
|
|||||||
// pointer — the mouse position (in % of page width/height). Used by the
|
// pointer — the mouse position (in % of page width/height). Used by the
|
||||||
// soft glow that follows the cursor in the background.
|
// soft glow that follows the cursor in the background.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const [showIntro, setShowIntro] = useState(false);
|
const [showIntro, setShowIntro] = useState(() => {
|
||||||
|
if (typeof document === "undefined") return false;
|
||||||
|
return document.documentElement.dataset.showIntro === "1";
|
||||||
|
});
|
||||||
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
|
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// INTRO OVERLAY EFFECT
|
// INTRO OVERLAY EFFECT
|
||||||
// Plays the animated logo + wordmark the first time someone visits the
|
// Plays the animated logo + wordmark the first time someone visits the
|
||||||
// site in this browser tab. We remember they've seen it using
|
// site in this browser tab. We decide before hydration whether to play it,
|
||||||
// sessionStorage so it doesn't replay on every navigation. The intro lasts
|
// which avoids the homepage flashing briefly before the intro appears.
|
||||||
// a little under 4 seconds, with a softer fade into the main page.
|
// After the user accepts cookies on their first visit, we trigger the intro
|
||||||
|
// immediately from the banner close action.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
function playIntro() {
|
||||||
|
document.documentElement.dataset.showIntro = "1";
|
||||||
|
setShowIntro(true);
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(INTRO_SEEN_KEY, "true");
|
||||||
|
} catch {
|
||||||
|
/* storage unavailable — silently ignore */
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
document.documentElement.dataset.showIntro = "0";
|
||||||
|
setShowIntro(false);
|
||||||
|
}, 3950);
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timer: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const introSeen = window.sessionStorage.getItem("novarix-intro-seen");
|
const introSeen = window.sessionStorage.getItem(INTRO_SEEN_KEY);
|
||||||
// Only set the "seen" flag if the user has accepted cookies. Without
|
|
||||||
// consent we still play the intro — we just don't remember it played.
|
|
||||||
const consent = window.localStorage.getItem("novarix-cookie-consent");
|
const consent = window.localStorage.getItem("novarix-cookie-consent");
|
||||||
|
const shouldPlayNow =
|
||||||
|
document.documentElement.dataset.showIntro === "1" &&
|
||||||
|
consent === "ack" &&
|
||||||
|
!introSeen;
|
||||||
|
|
||||||
if (!introSeen) {
|
if (shouldPlayNow) {
|
||||||
// Sync once with sessionStorage on first client mount.
|
timer = playIntro();
|
||||||
|
} else {
|
||||||
|
document.documentElement.dataset.showIntro = "0";
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
setShowIntro(true);
|
setShowIntro(false);
|
||||||
if (consent === "ack") {
|
|
||||||
window.sessionStorage.setItem("novarix-intro-seen", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
// The SVG's built-in stroke + fill animation lasts ~3s.
|
|
||||||
// We hold a little longer so the page underneath can ease in while
|
|
||||||
// the overlay fades away.
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
setShowIntro(false);
|
|
||||||
}, 3950);
|
|
||||||
|
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* storage unavailable — silently skip the intro */
|
document.documentElement.dataset.showIntro = "0";
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setShowIntro(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStartIntro() {
|
||||||
|
if (timer !== null) window.clearTimeout(timer);
|
||||||
|
timer = playIntro();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(INTRO_EVENT, handleStartIntro);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timer !== null) window.clearTimeout(timer);
|
||||||
|
window.removeEventListener(INTRO_EVENT, handleStartIntro);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -28,28 +28,35 @@ import { site } from "@/content";
|
|||||||
const CONSENT_KEY = "novarix-cookie-consent";
|
const CONSENT_KEY = "novarix-cookie-consent";
|
||||||
const INTRO_SEEN_KEY = "novarix-intro-seen";
|
const INTRO_SEEN_KEY = "novarix-intro-seen";
|
||||||
const REOPEN_EVENT = "novarix:open-cookie-banner";
|
const REOPEN_EVENT = "novarix:open-cookie-banner";
|
||||||
|
const INTRO_EVENT = "novarix:start-intro";
|
||||||
|
|
||||||
type Consent = "unknown" | "ack" | "rst";
|
type Consent = "unknown" | "ack" | "rst";
|
||||||
|
|
||||||
export default function CookieBanner() {
|
export default function CookieBanner() {
|
||||||
// Start hidden on the server and on first client paint to avoid a flash.
|
// Start from the pre-hydration decision made in layout.tsx so the banner
|
||||||
const [visible, setVisible] = useState(false);
|
// can appear immediately on first visit without waiting for a delayed effect.
|
||||||
|
const [visible, setVisible] = useState(() => {
|
||||||
|
if (typeof document === "undefined") return false;
|
||||||
|
return document.documentElement.dataset.showCookieBanner === "1";
|
||||||
|
});
|
||||||
|
|
||||||
// Read the stored consent on mount and decide whether to show the banner.
|
// Read the stored consent on mount and decide whether to show the banner.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const stored = window.localStorage.getItem(CONSENT_KEY) as Consent | null;
|
const stored = window.localStorage.getItem(CONSENT_KEY) as Consent | null;
|
||||||
if (stored !== "ack" && stored !== "rst") {
|
if (stored !== "ack" && stored !== "rst") {
|
||||||
// No decision yet — show the banner. We delay slightly so the page
|
document.documentElement.dataset.showCookieBanner = "1";
|
||||||
// settles in first; feels less aggressive than appearing instantly.
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
const t = window.setTimeout(() => setVisible(true), 600);
|
setVisible(true);
|
||||||
return () => window.clearTimeout(t);
|
} else {
|
||||||
|
document.documentElement.dataset.showCookieBanner = "0";
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// localStorage unavailable (private mode, blocked, etc.) — show the
|
// localStorage unavailable (private mode, blocked, etc.) — show the
|
||||||
// banner so the user is still told. Sync-once-on-mount is a legitimate
|
// banner so the user is still told. Sync-once-on-mount is a legitimate
|
||||||
// use of setState in an effect.
|
// use of setState in an effect.
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
document.documentElement.dataset.showCookieBanner = "1";
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -67,10 +74,13 @@ export default function CookieBanner() {
|
|||||||
if (choice === "rst") {
|
if (choice === "rst") {
|
||||||
// Honour the rejection by clearing any non-consent storage.
|
// Honour the rejection by clearing any non-consent storage.
|
||||||
window.sessionStorage.removeItem(INTRO_SEEN_KEY);
|
window.sessionStorage.removeItem(INTRO_SEEN_KEY);
|
||||||
|
} else {
|
||||||
|
window.dispatchEvent(new Event(INTRO_EVENT));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* storage unavailable — nothing to do */
|
/* storage unavailable — nothing to do */
|
||||||
}
|
}
|
||||||
|
document.documentElement.dataset.showCookieBanner = "0";
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +91,7 @@ export default function CookieBanner() {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
aria-label="Cookie consent"
|
aria-label="Cookie consent"
|
||||||
className="fixed inset-x-0 bottom-0 z-[1500] flex justify-center p-3 sm:p-5"
|
className="fixed inset-x-0 bottom-0 z-[2500] flex justify-center p-3 sm:p-5"
|
||||||
>
|
>
|
||||||
<div className="pointer-events-auto w-full max-w-3xl rounded-2xl border border-[var(--border-strong)] bg-[var(--surface-strong)] p-4 shadow-[0_24px_60px_-20px_oklch(0_0_0/0.45)] backdrop-blur-xl sm:p-5">
|
<div className="pointer-events-auto w-full max-w-3xl rounded-2xl border border-[var(--border-strong)] bg-[var(--surface-strong)] p-4 shadow-[0_24px_60px_-20px_oklch(0_0_0/0.45)] backdrop-blur-xl sm:p-5">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user