This commit is contained in:
Kismet Hasanaj
2026-05-03 01:16:43 +02:00
parent c22d54abb0
commit 74a878bc62
5 changed files with 33 additions and 239 deletions
+2 -18
View File
@@ -78,8 +78,8 @@ export default function CookiePolicyPage() {
What we store
</h2>
<p className="mt-3">
We use two small browser storage entries both first-party,
both confined to your browser, neither shared with anyone:
We use one small browser storage entry. It is first-party,
confined to your browser, and never shared with anyone:
</p>
<div className="mt-6 space-y-4">
@@ -96,22 +96,6 @@ export default function CookiePolicyPage() {
every page. Persists until you clear your browser data.
</p>
</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>
</section>
+4 -98
View File
@@ -229,79 +229,7 @@
}
/* ---------------------------------------------------------------------------
6. Intro overlay
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.
6. Animations — keyframes used by the ambient orbs above.
--------------------------------------------------------------------------- */
@keyframes drift {
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
the orb drift, the intro animation, and any transition/animation
durations across the site.
the orb drift and any transition/animation durations across the site.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.ambient-orb,
.intro-overlay,
.intro-wordmark {
.ambient-orb {
animation: none !important;
}
+23 -109
View File
@@ -9,94 +9,36 @@
//
// How this file is organised, top to bottom:
// 1. Imports + setup
// 2. Intro overlay (the animated logo shown on first visit)
// 3. Site shell (background gradient + ambient orbs + grid)
// 4. Header (logo + navigation)
// 5. Hero (big headline + buttons)
// 6. Services (three cards under "What we do")
// 7. How we engage (three cards under "Working with us")
// 8. Contact (the dark contact card)
// 9. Footer (company details + copyright)
// 2. Site shell (background gradient + ambient orbs + grid)
// 3. Header (logo + navigation)
// 4. Hero (big headline + buttons)
// 5. Services (three cards under "What we do")
// 6. How we engage (three cards under "Working with us")
// 7. Contact (the dark contact card)
// 8. Footer (company details + copyright)
//
// 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
// browser (needed for the animated intro and the mouse-tracking glow).
// browser (needed for the mouse-tracking glow).
// =============================================================================
import Image from "next/image";
import Link from "next/link";
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { site } from "@/content";
import { openCookieBanner } from "@/components/CookieBanner";
// Type for the mouse-pointer position used to move the background glow.
type PointerState = { x: number; y: number };
const INTRO_SEEN_KEY = "novarix-intro-seen";
const INTRO_EVENT = "novarix:start-intro";
export default function HomePage() {
// ---------------------------------------------------------------------------
// STATE
// ---------------------------------------------------------------------------
// showIntro — true while the animated logo intro is playing.
// pointer — the mouse position (in % of page width/height). Used by the
// soft glow that follows the cursor in the background.
// pointer — the mouse position (in % of page width/height). Used by the
// soft glow that follows the cursor in the background.
// ---------------------------------------------------------------------------
const [showIntro, setShowIntro] = useState(false);
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
// Translates the current mouse pointer into two CSS variables (--mx, --my)
@@ -113,44 +55,17 @@ export default function HomePage() {
);
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
id="top"
className={`site-shell ${showIntro ? "intro-running" : ""}`}
style={backgroundStyle}
onMouseMove={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
setPointer({ x, y });
}}
>
<main
id="top"
className="site-shell"
style={backgroundStyle}
onMouseMove={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 100;
const y = ((event.clientY - rect.top) / rect.height) * 100;
setPointer({ x, y });
}}
>
{/* -------------------------------------------------------------------
AMBIENT LAYER
Two soft floating "orbs" and a faint grid behind everything.
@@ -512,7 +427,6 @@ export default function HomePage() {
</div>
</div>
</footer>
</main>
</>
</main>
);
}
+2 -12
View File
@@ -10,11 +10,9 @@
// Behaviour:
// - On first load, if no consent decision is stored, the banner appears.
// - "ACK" stores `novarix-cookie-consent = "ack"` in localStorage and the
// banner closes. The site then behaves as before (intro animation
// remembers it has played using sessionStorage).
// banner closes.
// - "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
// non-consent storage around.
// banner closes.
// - The footer "Cookie preferences" link dispatches a window event that
// re-opens this banner so users can change their mind.
//
@@ -26,9 +24,7 @@ import { useEffect, useLayoutEffect, useState } from "react";
import { site } from "@/content";
const CONSENT_KEY = "novarix-cookie-consent";
const INTRO_SEEN_KEY = "novarix-intro-seen";
const REOPEN_EVENT = "novarix:open-cookie-banner";
const INTRO_EVENT = "novarix:start-intro";
type Consent = "unknown" | "ack" | "rst";
@@ -61,12 +57,6 @@ export default function CookieBanner() {
function decide(choice: "ack" | "rst") {
try {
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 {
/* storage unavailable — nothing to do */
}
+2 -2
View File
@@ -153,9 +153,9 @@ export const site = {
// ---------------------------------------------------------------------------
cookies: {
// 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:
"Heads up — this site stores one tiny browser preference to remember youve seen the intro animation. We dont run analytics, advertising, or third-party tracking.",
"Heads up — this site stores one tiny browser preference to remember your cookie choice. We dont run analytics, advertising, or third-party tracking.",
// The two buttons. Labels are small jokes for network folk; subtitles
// and aria-labels make them clear to everyone else.