.
This commit is contained in:
+540
@@ -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 (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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user