no-intro
This commit is contained in:
+2
-18
@@ -78,8 +78,8 @@ export default function CookiePolicyPage() {
|
|||||||
What we store
|
What we store
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3">
|
<p className="mt-3">
|
||||||
We use two small browser storage entries — both first-party,
|
We use one small browser storage entry. It is first-party,
|
||||||
both confined to your browser, neither shared with anyone:
|
confined to your browser, and never shared with anyone:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-6 space-y-4">
|
<div className="mt-6 space-y-4">
|
||||||
@@ -96,22 +96,6 @@ export default function CookiePolicyPage() {
|
|||||||
every page. Persists until you clear your browser data.
|
every page. Persists until you clear your browser data.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 backdrop-blur sm:p-6">
|
|
||||||
<p className="font-mono text-sm font-semibold text-[var(--text)]">
|
|
||||||
novarix-intro-seen
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-sm">
|
|
||||||
<span className="font-semibold text-[var(--text)]">
|
|
||||||
Preference.
|
|
||||||
</span>{" "}
|
|
||||||
Stored in <code>sessionStorage</code> only when you accept
|
|
||||||
cookies. Lets us skip the animated logo intro for the rest
|
|
||||||
of your browsing session. Cleared automatically when you
|
|
||||||
close the browser tab. If you decline cookies, this is
|
|
||||||
never set and the intro may play again on a fresh tab.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+4
-98
@@ -229,79 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
6. Intro overlay
|
6. Animations — keyframes used by the ambient orbs above.
|
||||||
The first-visit animated SVG wordmark. The SVG itself runs a 3-second
|
|
||||||
stroke-then-fill animation (defined inside
|
|
||||||
/public/branding/animated_logo_intro.svg). The overlay begins fading once
|
|
||||||
the wordmark finishes drawing, while the page underneath eases in. Total
|
|
||||||
intro length is matched in page.tsx's setTimeout (3950ms).
|
|
||||||
|
|
||||||
When .intro-running is on the <main> element, the page content
|
|
||||||
underneath is gently softened so it can reveal itself without a hard cut.
|
|
||||||
--------------------------------------------------------------------------- */
|
|
||||||
.intro-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 2000;
|
|
||||||
display: grid;
|
|
||||||
place-items: center; /* dead-centre the SVG in the viewport */
|
|
||||||
animation: intro-fade-out 900ms cubic-bezier(0.22, 1, 0.36, 1) 3.05s forwards;
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background:
|
|
||||||
radial-gradient(
|
|
||||||
circle at 50% 32%,
|
|
||||||
oklch(0.62 0.16 240 / 0.08),
|
|
||||||
transparent 30%
|
|
||||||
),
|
|
||||||
var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-svg {
|
|
||||||
position: relative; /* sits above .intro-backdrop in the stacking order */
|
|
||||||
display: block;
|
|
||||||
width: min(70vw, 900px);
|
|
||||||
max-height: 70vh;
|
|
||||||
height: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header,
|
|
||||||
main > section,
|
|
||||||
main > footer {
|
|
||||||
transition:
|
|
||||||
opacity 900ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
||||||
transform 900ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
||||||
filter 900ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
will-change: opacity, transform, filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro-running .site-header,
|
|
||||||
.intro-running main > section,
|
|
||||||
.intro-running main > footer {
|
|
||||||
opacity: 0.16;
|
|
||||||
transform: translateY(10px) scale(0.99);
|
|
||||||
filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.intro-svg {
|
|
||||||
width: min(86vw, 560px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.intro-svg {
|
|
||||||
width: 92vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
|
||||||
7. Animations — keyframes used by the orbs and the intro overlay above.
|
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
@keyframes drift {
|
@keyframes drift {
|
||||||
0%,
|
0%,
|
||||||
@@ -313,39 +241,17 @@ main > footer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes intro-wordmark {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(110%) scale(0.98);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes intro-fade-out {
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
8. Reduced motion
|
7. Reduced motion
|
||||||
Respects the user's OS-level "reduce motion" preference by disabling
|
Respects the user's OS-level "reduce motion" preference by disabling
|
||||||
the orb drift, the intro animation, and any transition/animation
|
the orb drift and any transition/animation durations across the site.
|
||||||
durations across the site.
|
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ambient-orb,
|
.ambient-orb {
|
||||||
.intro-overlay,
|
|
||||||
.intro-wordmark {
|
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-96
@@ -9,94 +9,36 @@
|
|||||||
//
|
//
|
||||||
// How this file is organised, top to bottom:
|
// How this file is organised, top to bottom:
|
||||||
// 1. Imports + setup
|
// 1. Imports + setup
|
||||||
// 2. Intro overlay (the animated logo shown on first visit)
|
// 2. Site shell (background gradient + ambient orbs + grid)
|
||||||
// 3. Site shell (background gradient + ambient orbs + grid)
|
// 3. Header (logo + navigation)
|
||||||
// 4. Header (logo + navigation)
|
// 4. Hero (big headline + buttons)
|
||||||
// 5. Hero (big headline + buttons)
|
// 5. Services (three cards under "What we do")
|
||||||
// 6. Services (three cards under "What we do")
|
// 6. How we engage (three cards under "Working with us")
|
||||||
// 7. How we engage (three cards under "Working with us")
|
// 7. Contact (the dark contact card)
|
||||||
// 8. Contact (the dark contact card)
|
// 8. Footer (company details + copyright)
|
||||||
// 9. Footer (company details + copyright)
|
|
||||||
//
|
//
|
||||||
// Each section is marked with a clear comment header you can search for.
|
// Each section is marked with a clear comment header you can search for.
|
||||||
// The "use client" line at the top tells Next.js this page runs in the
|
// The "use client" line at the top tells Next.js this page runs in the
|
||||||
// browser (needed for the animated intro and the mouse-tracking glow).
|
// browser (needed for the mouse-tracking glow).
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { site } from "@/content";
|
import { site } from "@/content";
|
||||||
import { openCookieBanner } from "@/components/CookieBanner";
|
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() {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// STATE
|
// STATE
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// showIntro — true while the animated logo intro is playing.
|
|
||||||
// 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 [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
|
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// INTRO OVERLAY EFFECT
|
|
||||||
// Plays the animated logo + wordmark the first time someone visits the
|
|
||||||
// site in this browser tab. The initial check runs in useLayoutEffect so
|
|
||||||
// the intro can mount before the first browser paint, avoiding a visible
|
|
||||||
// flash of the homepage underneath.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
function playIntro() {
|
|
||||||
setShowIntro(true);
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(INTRO_SEEN_KEY, "true");
|
|
||||||
} catch {
|
|
||||||
/* storage unavailable — silently ignore */
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
setShowIntro(false);
|
|
||||||
}, 3950);
|
|
||||||
|
|
||||||
return timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
let timer: number | null = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const introSeen = window.sessionStorage.getItem(INTRO_SEEN_KEY);
|
|
||||||
const consent = window.localStorage.getItem("novarix-cookie-consent");
|
|
||||||
const shouldPlayNow = consent === "ack" && !introSeen;
|
|
||||||
|
|
||||||
if (shouldPlayNow) {
|
|
||||||
timer = playIntro();
|
|
||||||
} else {
|
|
||||||
setShowIntro(false);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BACKGROUND POSITION
|
// BACKGROUND POSITION
|
||||||
// Translates the current mouse pointer into two CSS variables (--mx, --my)
|
// Translates the current mouse pointer into two CSS variables (--mx, --my)
|
||||||
@@ -113,36 +55,9 @@ export default function HomePage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
{/* =====================================================================
|
|
||||||
INTRO OVERLAY
|
|
||||||
A single self-contained animated SVG of the Novarix wordmark,
|
|
||||||
drawn dead-centre in the viewport. The SVG itself contains all the
|
|
||||||
animation (strokes draw over 2s, fill in over the next 1s — see
|
|
||||||
/public/branding/animated_logo_intro.svg). Hidden after ~4s
|
|
||||||
(see the useEffect above + the CSS fade-out timing).
|
|
||||||
===================================================================== */}
|
|
||||||
{showIntro && (
|
|
||||||
<div className="intro-overlay" aria-hidden="true">
|
|
||||||
<div className="intro-backdrop" />
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src="/branding/animated_logo_intro.svg"
|
|
||||||
alt=""
|
|
||||||
className="intro-svg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* =====================================================================
|
|
||||||
SITE SHELL
|
|
||||||
The <main> wraps the whole page. It tracks the mouse so the
|
|
||||||
background glow can follow the cursor, and applies the .site-shell
|
|
||||||
class (defined in globals.css) for the background gradient.
|
|
||||||
===================================================================== */}
|
|
||||||
<main
|
<main
|
||||||
id="top"
|
id="top"
|
||||||
className={`site-shell ${showIntro ? "intro-running" : ""}`}
|
className="site-shell"
|
||||||
style={backgroundStyle}
|
style={backgroundStyle}
|
||||||
onMouseMove={(event) => {
|
onMouseMove={(event) => {
|
||||||
const rect = event.currentTarget.getBoundingClientRect();
|
const rect = event.currentTarget.getBoundingClientRect();
|
||||||
@@ -513,6 +428,5 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,9 @@
|
|||||||
// Behaviour:
|
// Behaviour:
|
||||||
// - On first load, if no consent decision is stored, the banner appears.
|
// - On first load, if no consent decision is stored, the banner appears.
|
||||||
// - "ACK" stores `novarix-cookie-consent = "ack"` in localStorage and the
|
// - "ACK" stores `novarix-cookie-consent = "ack"` in localStorage and the
|
||||||
// banner closes. The site then behaves as before (intro animation
|
// banner closes.
|
||||||
// remembers it has played using sessionStorage).
|
|
||||||
// - "RST" stores `novarix-cookie-consent = "rst"` in localStorage and the
|
// - "RST" stores `novarix-cookie-consent = "rst"` in localStorage and the
|
||||||
// banner closes. We also clear the intro-seen flag so we don't keep any
|
// banner closes.
|
||||||
// non-consent storage around.
|
|
||||||
// - The footer "Cookie preferences" link dispatches a window event that
|
// - The footer "Cookie preferences" link dispatches a window event that
|
||||||
// re-opens this banner so users can change their mind.
|
// re-opens this banner so users can change their mind.
|
||||||
//
|
//
|
||||||
@@ -26,9 +24,7 @@ import { useEffect, useLayoutEffect, useState } from "react";
|
|||||||
import { site } from "@/content";
|
import { site } from "@/content";
|
||||||
|
|
||||||
const CONSENT_KEY = "novarix-cookie-consent";
|
const CONSENT_KEY = "novarix-cookie-consent";
|
||||||
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";
|
||||||
|
|
||||||
@@ -61,12 +57,6 @@ export default function CookieBanner() {
|
|||||||
function decide(choice: "ack" | "rst") {
|
function decide(choice: "ack" | "rst") {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(CONSENT_KEY, choice);
|
window.localStorage.setItem(CONSENT_KEY, choice);
|
||||||
if (choice === "rst") {
|
|
||||||
// Honour the rejection by clearing any non-consent storage.
|
|
||||||
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 */
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -153,9 +153,9 @@ export const site = {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
cookies: {
|
cookies: {
|
||||||
// Short message shown in the banner. Keep it honest — this site does not
|
// Short message shown in the banner. Keep it honest — this site does not
|
||||||
// currently use any tracking cookies, only one preference for the intro.
|
// currently use any tracking cookies, only one preference for consent.
|
||||||
message:
|
message:
|
||||||
"Heads up — this site stores one tiny browser preference to remember you’ve seen the intro animation. We don’t run analytics, advertising, or third-party tracking.",
|
"Heads up — this site stores one tiny browser preference to remember your cookie choice. We don’t run analytics, advertising, or third-party tracking.",
|
||||||
|
|
||||||
// The two buttons. Labels are small jokes for network folk; subtitles
|
// The two buttons. Labels are small jokes for network folk; subtitles
|
||||||
// and aria-labels make them clear to everyone else.
|
// and aria-labels make them clear to everyone else.
|
||||||
|
|||||||
Reference in New Issue
Block a user