This repository has been archived on 2026-05-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
novarix-networks-homepage/app/page.tsx
T
Kismet Hasanaj 34dc9aec52 .
2026-05-02 20:07:02 +02:00

541 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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>
</>
);
}