This commit is contained in:
Kismet Hasanaj
2026-05-02 20:07:02 +02:00
parent ce8672e283
commit 34dc9aec52
9428 changed files with 1733330 additions and 0 deletions
+540
View File
@@ -0,0 +1,540 @@
/**
* page.tsx — Novarix Networks home page
*
* This is the only route in the site ("/"). It renders a single-page layout
* with the following sections:
*
* 1. Intro overlay — first-visit only splash (Lottie + wordmark)
* 2. Header — sticky nav with brand wordmark
* 3. Hero — headline, sub-text, CTA buttons
* 4. Services — three service cards
* 5. Direction — three-phase platform roadmap
* 6. Contact — mailto link
* 7. Footer — copyright line
*
* ─── State overview ──────────────────────────────────────────────────────
*
* showIntro Controls whether the splash overlay is rendered. Starts
* false (SSR safe), is set to true on first mount if the
* "novarix-intro-seen" sessionStorage key is absent, then
* reverts to false after the intro duration elapses.
*
* mounted Prevents the intro overlay from rendering during SSR / the
* hydration pass, which would cause a server/client mismatch.
*
* pointer Tracks normalised cursor position (0100 %) for the ambient
* radial-gradient background effect.
*
* ─── Adding / editing content ────────────────────────────────────────────
*
* • Update the `services` array to change service cards.
* • Update the direction items inline in the JSX.
* • The intro animation JSON lives at /public/branding/animated_logo_intro.json
* • Wordmark images live at /public/branding/novarix-wordmark-{colour,white}.png
*
* ─── Dependencies ────────────────────────────────────────────────────────
*
* next/image — optimised image component (lazy-loading, AVIF/WebP)
* lottie-react — renders the Bodymovin / Lottie JSON animation
*
* Install lottie-react if not already present:
* npm install lottie-react
*/
"use client";
import Image from "next/image";
import dynamic from "next/dynamic";
import { useEffect, useMemo, useRef, useState } from "react";
/**
* Lottie is dynamically imported so neither the library (~150 KB) nor the
* animation JSON (261 KB) are included in the initial page bundle. They are
* fetched only on first visit when the intro overlay is needed.
*/
const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
/* ── Service card data ─────────────────────────────────────────────────── */
/**
* Each entry renders one card in the Services section.
* Add, remove, or reorder entries here without touching the JSX.
*
* Fields:
* title — card heading
* description — body copy (23 sentences recommended)
* icon — Unicode emoji or inline SVG string used as the card icon
*/
const services = [
{
eyebrow: "ISP",
title: "Business Connectivity",
description:
"Routed internet access for organisations that need clear ownership, static addressing, and a provider willing to talk through the real topology.",
points: ["Business internet access", "Static IP addressing", "Routed handoff options"],
},
{
eyebrow: "MSP",
title: "Managed Network Services",
description:
"Managed edge, routing, firewall, and switching support for teams that want stronger control without carrying every operational detail alone.",
points: ["Managed edge infrastructure", "Change and migration support", "Operational visibility"],
},
{
eyebrow: "Transit",
title: "Transit and Interconnect",
description:
"Transit and interconnection planning for operators, platforms, and technical environments where routing posture matters.",
points: ["IP transit readiness", "Peering strategy", "Carrier engagement"],
},
] as const;
const networkSignals = [
{ label: "Operating focus", value: "ISP / MSP / Transit" },
{ label: "Designed for", value: "Business-critical networks" },
{ label: "Built around", value: "Practical engineering" },
] as const;
const platformPhases = [
{
phase: "Phase 1",
title: "Connectivity and managed support",
description:
"Lead with services that can be delivered credibly from day one: internet access, managed network support, and engineering advisory work.",
},
{
phase: "Phase 2",
title: "Transit, peering, and interconnect",
description:
"Grow toward exchange participation, partner interconnection, and clearer transit propositions as the network footprint matures.",
},
{
phase: "Phase 3",
title: "Selective edge infrastructure",
description:
"Introduce regional edge or platform capability only where real demand, operational control, and commercial return justify it.",
},
] as const;
/* ── Types ─────────────────────────────────────────────────────────────── */
type PointerState = {
x: number; // cursor X as percentage of viewport width (0100)
y: number; // cursor Y as percentage of viewport height (0100)
};
/* ── Constants ─────────────────────────────────────────────────────────── */
/**
* Total duration the intro overlay is visible (milliseconds).
* Must be long enough to cover:
* • Lottie animation length
* • Wordmark slide-in (350 ms delay + ~1 000 ms animation = ~1 350 ms)
* • CSS fade-out at 2 450 ms (620 ms duration)
*
* The JS timer uses 3 200 ms so it matches the full CSS sequence before
* React removes the overlay from the DOM.
*/
const INTRO_DURATION_MS = 3200;
/** sessionStorage key used to gate the intro to one play per browser tab */
const INTRO_STORAGE_KEY = "novarix-intro-seen";
/* ── Component ─────────────────────────────────────────────────────────── */
export default function HomePage() {
// Prevents intro from rendering during SSR / hydration mismatch
const [mounted, setMounted] = useState(false);
// Whether to show the intro overlay
const [showIntro, setShowIntro] = useState(false);
// Lazily-loaded Lottie animation data (null until fetched)
const [introAnimation, setIntroAnimation] = useState<Record<string, unknown> | null>(null);
// Normalised cursor position for the ambient gradient
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
// Ref to the Lottie instance — can be used to imperatively control playback
const lottieRef = useRef(null);
/* ── Side effects ── */
useEffect(() => {
setMounted(true);
let timer: number | undefined;
try {
const introSeen = window.sessionStorage.getItem(INTRO_STORAGE_KEY);
if (!introSeen) {
window.sessionStorage.setItem(INTRO_STORAGE_KEY, "true");
/*
* Dynamically import the 261 KB animation JSON only on first visit.
* This keeps it out of the initial page bundle entirely.
*/
import("@/public/branding/animated_logo_intro.json").then((mod) => {
setIntroAnimation(mod.default as Record<string, unknown>);
setShowIntro(true);
timer = window.setTimeout(() => setShowIntro(false), INTRO_DURATION_MS);
});
}
} catch {
/*
* sessionStorage may be unavailable (private browsing restrictions,
* storage quota exceeded, or cross-origin iframes). Silently skip
* the intro rather than crashing.
*/
}
return () => {
if (timer) window.clearTimeout(timer);
};
}, []);
/* ── Derived values ── */
/**
* Memoised inline style object for the ambient background.
* Only recalculated when pointer.x or pointer.y change, preventing
* object identity churn on every render.
*/
const backgroundStyle = useMemo<React.CSSProperties>(
() => ({
"--mx": `${pointer.x}%`,
"--my": `${pointer.y}%`,
} as React.CSSProperties),
[pointer.x, pointer.y]
);
/* ── Handlers ── */
/**
* Updates the pointer state on mouse movement.
* Coordinates are normalised to the bounding rect of the main element
* (rather than the window) to avoid edge cases near fixed headers.
*/
function handleMouseMove(event: React.MouseEvent<HTMLElement>) {
const rect = event.currentTarget.getBoundingClientRect();
setPointer({
x: ((event.clientX - rect.left) / rect.width) * 100,
y: ((event.clientY - rect.top) / rect.height) * 100,
});
}
/* ── Render ── */
return (
<>
{/* ── Intro overlay ───────────────────────────────────────────────
Rendered only:
(a) after hydration (`mounted`)
(b) on the first visit within the session (`showIntro`)
aria-hidden="true" hides it from screen readers; the main content
is focusable without waiting for the intro to finish.
─────────────────────────────────────────────────────────────────── */}
{mounted && showIntro && (
<div className="intro-overlay" aria-hidden="true">
<div className="intro-backdrop" />
<div className="intro-shell">
{/* Lottie logo animation — renders once animationData has loaded */}
<div className="intro-lottie-wrap">
{introAnimation && (
<Lottie
lottieRef={lottieRef}
animationData={introAnimation}
loop={false}
autoplay
className="intro-lottie"
rendererSettings={{
preserveAspectRatio: "xMidYMid meet",
}}
/>
)}
</div>
{/* Wordmark — colour in light mode, white in dark mode */}
<div className="intro-wordmark-wrap">
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
sizes="(max-width: 720px) 86vw, (max-width: 980px) 72vw, 864px"
className="intro-wordmark intro-wordmark-light"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
sizes="(max-width: 720px) 86vw, (max-width: 980px) 72vw, 864px"
className="intro-wordmark intro-wordmark-dark"
/>
</div>
</div>
</div>
)}
{/* ── Page shell ──────────────────────────────────────────────────
`intro-running` class hides section content while the intro
overlay is active, preventing a flash of content beneath it.
─────────────────────────────────────────────────────────────────── */}
<main
id="top"
className={`site-shell${showIntro ? " intro-running" : ""}`}
style={backgroundStyle}
onMouseMove={handleMouseMove}
>
{/* ── Ambient decorative layer (aria-hidden) ─────────────────── */}
<div className="ambient-layer" aria-hidden="true">
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
</div>
{/* ════════════════════════════════════════════════════════════
HEADER
════════════════════════════════════════════════════════════ */}
<header className="site-header">
<div className="container header-inner">
{/* Brand wordmark — links back to the top of the page */}
<a href="#top" className="brand-link" aria-label="Novarix Networks — go to top">
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
sizes="(max-width: 560px) 50vw, (max-width: 720px) 54vw, (max-width: 980px) 45vw, 250px"
className="brand-image brand-image-light"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
sizes="(max-width: 560px) 50vw, (max-width: 720px) 54vw, (max-width: 980px) 45vw, 250px"
className="brand-image brand-image-dark"
/>
</a>
{/* Primary navigation */}
<nav className="nav" aria-label="Primary navigation">
<a href="#services">Services</a>
<a href="#network">Network</a>
<a href="#direction">Direction</a>
<a href="#contact">Contact</a>
</nav>
</div>
</header>
{/* ════════════════════════════════════════════════════════════
HERO
════════════════════════════════════════════════════════════ */}
<section className="hero" aria-labelledby="hero-heading">
<div className="hero-mark" aria-hidden="true" />
<div className="container">
<div className="hero-copy">
<p className="eyebrow" aria-hidden="true">
Independent ISP · Managed Services · Transit
</p>
<h1 id="hero-heading">
Agile network infrastructure for organisations that cannot wait on slow providers.
</h1>
<p className="hero-text">
Novarix Networks is being built as an engineering-led ISP, MSP,
and transit provider for businesses, operators, and demanding
projects that need direct answers, dependable routing, and room
to grow.
</p>
<div className="hero-actions">
<a href="#contact" className="button button-primary">
Discuss a requirement
</a>
<a href="#services" className="button button-secondary">
Explore services
</a>
</div>
</div>
<div className="signal-strip" aria-label="Novarix Networks positioning">
{networkSignals.map((signal) => (
<div key={signal.label} className="signal-item">
<span>{signal.label}</span>
<strong>{signal.value}</strong>
</div>
))}
</div>
</div>
</section>
<section className="statement-section section-border" aria-label="Company statement">
<div className="container">
<p>
Novarix exists for the moments where connectivity is not a commodity purchase:
new sites, awkward migrations, routed environments, managed edge,
transit conversations, and the technical work that sits between them.
</p>
</div>
</section>
{/* ════════════════════════════════════════════════════════════
SERVICES
To add a service, append an entry to the `services` array
at the top of this file.
════════════════════════════════════════════════════════════ */}
<section
id="services"
className="section section-border"
aria-labelledby="services-heading"
>
<div className="container">
<div className="section-heading">
<h2 id="services-heading">Services</h2>
<p>
A focused offer for customers who need practical delivery now,
with a network strategy that can deepen over time.
</p>
</div>
<div className="card-grid">
{services.map((service) => (
<article key={service.title} className="card">
<span className="card-eyebrow">{service.eyebrow}</span>
<h3>{service.title}</h3>
<p>{service.description}</p>
<ul className="card-points">
{service.points.map((point) => (
<li key={point}>{point}</li>
))}
</ul>
</article>
))}
</div>
</div>
</section>
<section
id="network"
className="network-section section-border"
aria-labelledby="network-heading"
>
<div className="container network-layout">
<div className="section-heading">
<h2 id="network-heading">Built for the current climate of networking.</h2>
<p>
The site should signal that Novarix is technical, independent,
and pragmatic: capable of working across access, managed
services, transit, and the messy edges where real networks live.
</p>
</div>
<div className="network-stack" aria-label="Network operating principles">
<div className="network-stack-item">
<span>01</span>
<strong>Direct technical conversation</strong>
<p>Requirements are shaped with the people who understand routing, handoff, risk, and operational reality.</p>
</div>
<div className="network-stack-item">
<span>02</span>
<strong>Lean delivery model</strong>
<p>Keep the path between problem, decision, and change short enough to stay useful.</p>
</div>
<div className="network-stack-item">
<span>03</span>
<strong>Expansion without theatre</strong>
<p>Grow into peering, transit, and edge services when the demand and network maturity support it.</p>
</div>
</div>
</div>
</section>
{/* ════════════════════════════════════════════════════════════
PLATFORM DIRECTION
Three-phase roadmap. Edit phases inline below.
════════════════════════════════════════════════════════════ */}
<section
id="direction"
className="section section-border"
aria-labelledby="direction-heading"
>
<div className="container narrow">
<div className="section-heading">
<h2 id="direction-heading">Platform Direction</h2>
<p>
The public message stays credible today while leaving a clear
route toward deeper interconnection and operator-grade services.
</p>
</div>
<div className="direction-list">
{platformPhases.map((item) => (
<div className="direction-item" key={item.phase}>
<span className="direction-phase">{item.phase}</span>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
</div>
</section>
{/* ════════════════════════════════════════════════════════════
CONTACT
Update the mailto address and any supporting copy here.
════════════════════════════════════════════════════════════ */}
<section
id="contact"
className="section section-border"
aria-labelledby="contact-heading"
>
<div className="container narrow">
<div className="section-heading">
<h2 id="contact-heading">Contact</h2>
<p>
For connectivity, managed networking, transit, or early project
discussions, start with a direct email. Keep it technical if
that is what the requirement needs.
</p>
</div>
<a
className="contact-link"
href="mailto:contact@novarixnet.com"
aria-label="Send an email to contact@novarixnet.com"
>
contact@novarixnet.com
</a>
</div>
</section>
{/* ════════════════════════════════════════════════════════════
FOOTER
════════════════════════════════════════════════════════════ */}
<footer className="footer section-border">
<div className="container footer-inner">
<span>
&copy; {new Date().getFullYear()} Novarix Networks Limited
</span>
{/*
* Optional: add secondary footer links here, e.g.:
* <nav aria-label="Footer navigation">
* <a href="/privacy">Privacy</a>
* </nav>
*/}
</div>
</footer>
</main>
</>
);
}