541 lines
23 KiB
TypeScript
541 lines
23 KiB
TypeScript
/**
|
||
* 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 (0–100 %) 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 (2–3 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 (0–100)
|
||
y: number; // cursor Y as percentage of viewport height (0–100)
|
||
};
|
||
|
||
/* ── 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>
|
||
© {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>
|
||
</>
|
||
);
|
||
}
|