Initial-commit

This commit is contained in:
Kismet Hasanaj
2026-05-02 23:53:52 +02:00
parent dbf4c80804
commit ec83a0d879
36 changed files with 19889 additions and 1 deletions
+166
View File
@@ -0,0 +1,166 @@
# Editing The Website
This is a friendly guide for changing the words on the Novarix Networks website. You don't need to be a developer to use it.
## What You Need
- A text editor — `VS Code`, `Sublime Text`, or even `Notepad++` are all fine.
- The repo cloned to your computer (ask if you don't have it yet).
That's it. You don't need Node.js installed locally if you only want to change text — the build runs on the Ubuntu Nginx VM.
## The Only File You Need To Touch
```text
content.ts
```
This file lives at the top of the project, next to `README.md`. Open it in your text editor.
You will see sections like this:
```ts
hero: {
badge: "Onboarding new accounts",
eyebrow: "Network Consulting · Remote Support · Engineering",
...
},
```
To change a piece of text, change what's inside the `"double quotes"`. **Keep the quotes**. **Keep the comma at the end of the line.**
## Common Edits
### Change the headline
Find this block near the top of `content.ts`:
```ts
headlineBefore: "Network expertise, on tap, for teams running",
headlineAccent: "production",
headlineAfter: "infrastructure.",
```
The headline is split into three parts so the middle word can be coloured with the brand gradient. Change any of the three pieces.
### Change a service description
Find the `services` section. Inside the square brackets `[ ... ]` you will see three blocks like this:
```ts
{
index: "01",
title: "Network Consulting",
description: "Architecture review, design, and second-opinion engineering...",
},
```
Edit the `title` or `description`. The `index` is just the small "01 / 02 / 03" label on the card.
### Add a fourth service
Copy one of the existing service blocks (the whole thing from `{` to `},`), paste it inside the same `[ ... ]`, and change the text. Don't forget the comma after the closing `}`. Example:
```ts
items: [
{ index: "01", title: "Network Consulting", description: "..." },
{ index: "02", title: "Remote Network Support", description: "..." },
{ index: "03", title: "Architecture & Design", description: "..." },
{ index: "04", title: "Your New Service", description: "Your description here." },
],
```
### Remove a service
Delete one of the blocks (from `{` all the way through `},`). Make sure each remaining block still ends with a comma.
### Change the contact email
Find the `contact` and `footer` sections and update the `email` field in both:
```ts
email: "hello@novarix.uk",
```
### Change the company number
Find the `footer` section:
```ts
registered: "Registered in England · Company No. 17047180",
```
### Hide the "Onboarding new accounts" pill
Set the `badge` field to an empty pair of quotes:
```ts
badge: "",
```
### Hide the capabilities line
Same trick — set `capabilities` to an empty pair of quotes:
```ts
capabilities: "",
```
### Add or remove a navigation link
Find the `nav` section:
```ts
nav: [
{ label: "Services", href: "#services" },
{ label: "How we engage", href: "#engage" },
],
```
Each link has a `label` (the text shown) and an `href` (where it jumps to). For links to sections of this page, use `"#services"`, `"#engage"`, or `"#contact"`. For external links use a full URL like `"https://example.com"`.
## Special Characters
To put a hyphen-like dot between words use the middle dot `·` (option-shift-9 on Mac, alt+0183 on Windows). To use a curly apostrophe, type `` not `'` — both work but the curly one looks better.
If you need an em dash, use `—` not `--`.
## Saving And Publishing
After editing `content.ts`, save the file and:
1. Open a terminal in the project folder.
2. Commit your changes:
```bash
git add content.ts
git commit -m "Update homepage copy"
git push
```
3. SSH into the Nginx VM and run:
```bash
cd /var/www/novarix.uk
./deploy.sh
```
The site will be rebuilt and reloaded — usually within 30 seconds.
## If You Want To Preview Changes Locally First
You'll need Node.js 20 or newer installed once. Then in the project folder:
```bash
npm install
npm run dev
```
Open `http://localhost:3000` in your browser. The page updates as you save `content.ts`.
## When Things Go Wrong
If the build fails after you save, you have probably:
- Removed a quote `"` somewhere
- Removed a comma at the end of a line
- Removed a closing brace `}` or square bracket `]`
The error message in the terminal will usually tell you which line in `content.ts` is the problem. Open the file, find that line, and look for one of the things above. If in doubt, undo your change with `git checkout content.ts` and start again with one small change at a time.
+198 -1
View File
@@ -1,2 +1,199 @@
# novarix-uk # Novarix Networks Website
`Next.js 16` + `Tailwind CSS v4` website for `Novarix Networks`, built as a fully static site.
The site is designed for this workflow:
- edit locally
- push changes to your internal `Gitea` repo
- run one deploy command on the Ubuntu Nginx VM
- serve the static `out/` directory from `/var/www/novarix.uk/out`
- expose it publicly through `Nginx Proxy Manager`
## Recommended Repo Name
```text
novarix-networks-homepage
```
Keep all the Novarix domains in repos under the same convention so paths and clone URLs stay predictable, e.g.
```text
http://10.10.10.11:3000/kismet.hasanaj/novarix-networks-homepage.git
```
## Project Structure
- `content.ts` **all the editable text on the website lives here**
- `app/page.tsx` page layout and styling (React + Tailwind utilities)
- `app/layout.tsx` site-wide metadata, fonts, html shell
- `app/globals.css` Tailwind v4 entrypoint and design tokens
- `app/sitemap.ts` generates `/sitemap.xml` at build time
- `app/robots.ts` generates `/robots.txt` at build time
- `public/` static assets (logos, branding, favicon)
- `next.config.ts` configured for static export to `out/`
- `EDITING.md` plain-English guide for changing content
- `deploy.sh` server-side update script
- `ops/nginx/novarix.uk.conf.example` example Nginx config
## Important Tailwind / Next.js Note
This project uses the current Tailwind CSS v4 setup via `@tailwindcss/postcss`, configured entirely inside `app/globals.css` (no `tailwind.config.js`).
The build is a **static export**. `npm run build` produces a fully self-contained `out/` directory. Nginx serves files from that directory directly — there is no Node runtime on the production server.
## Ubuntu 24.04 Nginx Server Setup
Install Nginx and Git:
```bash
sudo apt update
sudo apt install -y nginx git
sudo systemctl enable nginx
sudo systemctl start nginx
```
Install Node.js 20 or newer. NodeSource currently supports Ubuntu 24.04 with Node 22, which is a good choice for this server:
```bash
sudo apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
```
After installing, confirm:
```bash
node --version
npm --version
```
Create the web directory and make your normal sudo user the owner:
```bash
sudo mkdir -p /var/www
sudo chown -R $USER:$USER /var/www
```
Clone the Gitea repo:
```bash
git clone http://10.10.10.11:3000/kismet.hasanaj/novarix-networks-homepage.git /var/www/novarix.uk
```
Install the website dependencies and build the static site:
```bash
cd /var/www/novarix.uk
npm install --no-package-lock
npm run build
```
After the build completes, the static site lives in:
```text
/var/www/novarix.uk/out
```
Use the Nginx config in:
```text
ops/nginx/novarix.uk.conf.example
```
The important Nginx root is:
```nginx
root /var/www/novarix.uk/out;
```
Enable the site:
```bash
sudo cp ops/nginx/novarix.uk.conf.example /etc/nginx/sites-available/novarix.uk
sudo ln -s /etc/nginx/sites-available/novarix.uk /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```
## Updating The Live Website
After pushing changes to Gitea from your local machine, SSH into the Nginx VM and run:
```bash
cd /var/www/novarix.uk
./deploy.sh
```
The deploy script does this:
```bash
git pull origin main
npm install --no-package-lock
npm run build
sudo nginx -t
sudo systemctl reload nginx
```
If the script is not executable yet, run this once:
```bash
chmod +x /var/www/novarix.uk/deploy.sh
```
## Editing The Site
For text content (headlines, services, contact details), edit:
```text
content.ts
```
For visual styling, layout, or new sections, edit:
```text
app/page.tsx
app/globals.css
```
The friendly walk-through for non-developers is:
```text
EDITING.md
```
## Local Development
For previewing changes on your own machine before pushing:
```bash
npm install
npm run dev
```
Then open `http://localhost:3000` in your browser. The page reloads automatically as you save files.
## Nginx Proxy Manager
Create a proxy host:
- Domain: `novarix.uk`
- Scheme: `http`
- Forward hostname/IP: private IP of the Ubuntu Nginx VM
- Forward port: `80`
- SSL: request certificate and force SSL
- Enable `Block Common Exploits`
If you also want to redirect `www.novarix.uk` and any defensive domains (e.g. `novarixnet.com`), add them as additional proxy hosts pointing at the same backend, or use NPM's redirect host feature.
## Canonical Domain
Recommended primary domain:
```text
novarix.uk
```
Redirect any other Novarix domains to the primary domain once DNS and Nginx Proxy Manager are ready.
+217
View File
@@ -0,0 +1,217 @@
// =============================================================================
// Cookie & Privacy Policy
// =============================================================================
//
// A plain, honest policy page describing exactly what (very little) is
// stored in the visitor's browser. Update the `lastUpdated` field below
// whenever the policy changes.
//
// The wording is deliberately written in first-person plain English rather
// than legalese — Novarix is small enough to be transparent and brief.
// =============================================================================
import type { Metadata } from "next";
import Link from "next/link";
import { site } from "@/content";
const lastUpdated = "2 May 2026";
export const metadata: Metadata = {
title: "Cookie & Privacy Policy",
description:
"How Novarix Networks uses cookies and browser storage on this website.",
};
export default function CookiePolicyPage() {
return (
<main className="site-shell min-h-screen">
{/* Ambient background, matching the homepage */}
<div
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden"
aria-hidden="true"
>
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
</div>
{/* Lightweight header — just a back-link */}
<header className="border-b border-[var(--border)]">
<div className="mx-auto flex h-20 w-full max-w-3xl items-center px-6 sm:px-8">
<Link
href="/"
className="inline-flex items-center gap-2 text-sm text-[var(--text-soft)] transition-colors hover:text-[var(--text)]"
>
<span aria-hidden="true"></span>
Back to Novarix Networks
</Link>
</div>
</header>
<article className="mx-auto w-full max-w-3xl px-6 py-16 sm:px-8 sm:py-24">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
Legal
</p>
<h1 className="mt-3 text-[clamp(2rem,4vw,3rem)] leading-[1.05] font-semibold tracking-[-0.03em]">
Cookie &amp; Privacy Policy
</h1>
<p className="mt-4 text-sm text-[var(--text-soft)]">
Last updated {lastUpdated}
</p>
<div className="mt-12 space-y-10 text-base leading-relaxed text-[var(--text-soft)]">
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
In short
</h2>
<p className="mt-3">
This website does not run analytics, advertising, or any
third-party tracking. We do not sell, share, or transfer
personal data to anyone. The only things we store in your
browser are listed below and you can decline them using the
banner that appears on your first visit.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
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:
</p>
<div className="mt-6 space-y-4">
<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-cookie-consent
</p>
<p className="mt-2 text-sm">
<span className="font-semibold text-[var(--text)]">
Strictly necessary.
</span>{" "}
Stored in <code>localStorage</code>. Records your choice
from the cookie banner so we don&apos;t ask you again on
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>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
What we don&apos;t do
</h2>
<p className="mt-3">
We don&apos;t set tracking cookies. We don&apos;t run Google
Analytics, Plausible, Fathom, Matomo, or any equivalent. We
don&apos;t serve advertising. We don&apos;t embed third-party
scripts that profile you across websites. We don&apos;t use
session-replay tools.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Server logs
</h2>
<p className="mt-3">
Our web server keeps short-lived access logs (IP address,
request path, timestamp, user agent) for the operational
purposes of running the site diagnosing errors, blocking
abuse, and basic capacity planning. Logs are retained for no
longer than 30 days and are not used to build profiles of
individual visitors.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Changing your mind
</h2>
<p className="mt-3">
Use the{" "}
<span className="font-semibold text-[var(--text)]">
Cookie preferences
</span>{" "}
link in the footer of any page to re-open the banner and
change your decision. Or clear this site&apos;s storage in
your browser settings to be asked again from scratch.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Who we are
</h2>
<p className="mt-3">
{site.footer.company}, {site.footer.registered.toLowerCase()}.
For privacy questions or to exercise your rights under UK GDPR
(access, correction, deletion, portability, objection), email
us at{" "}
<a
href={`mailto:${site.footer.email}`}
className="font-medium text-[var(--text)] underline-offset-4 hover:underline"
>
{site.footer.email}
</a>
.
</p>
</section>
<section>
<h2 className="text-xl font-semibold tracking-tight text-[var(--text)]">
Complaints
</h2>
<p className="mt-3">
If you believe we&apos;ve mishandled your personal data, you
have the right to complain to the UK&apos;s data protection
authority, the Information Commissioner&apos;s Office (ICO),
at{" "}
<a
href="https://ico.org.uk/make-a-complaint/"
className="font-medium text-[var(--text)] underline-offset-4 hover:underline"
target="_blank"
rel="noreferrer"
>
ico.org.uk
</a>
.
</p>
</section>
</div>
</article>
<footer className="border-t border-[var(--border)] py-12">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-2 px-6 text-sm text-[var(--text-soft)] sm:flex-row sm:items-center sm:justify-between sm:px-8">
<span>
© {new Date().getFullYear()} {site.footer.company}
</span>
<Link
href="/"
className="transition-colors hover:text-[var(--accent)]"
>
Back to home
</Link>
</div>
</footer>
</main>
);
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

+338
View File
@@ -0,0 +1,338 @@
/* ============================================================================
Novarix Networks — Global stylesheet
============================================================================
This file controls the visual *theme* of the site (colours, fonts,
background effects, animations). Most page layout is done with Tailwind
utility classes directly inside `app/page.tsx` — this file only handles
things that don't fit neatly into utilities.
What's in here, top to bottom:
1. Design tokens — brand colours, font, shadows (@theme block)
2. Theme variables — light + dark mode CSS variables
3. Base styles — body font, focus rings, text selection
4. Site shell + ambient — background gradient, floating orbs, grid
5. Brand wordmark swap — light/dark logo switcher
6. Intro overlay — first-visit animated logo
7. Animations — keyframes used above
8. Reduced motion — respect "prefers-reduced-motion" setting
To change the brand colour palette, edit the `--color-brand-*` lines in
the @theme block below. Everything else cascades from those.
============================================================================ */
@import "tailwindcss";
/* ---------------------------------------------------------------------------
1. Design tokens — registered with Tailwind v4 via @theme.
These become utility classes (e.g. `bg-brand-500`, `text-ink-900`) and
custom CSS variables you can use anywhere in the stylesheet.
--------------------------------------------------------------------------- */
@theme {
--font-sans:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
--color-brand-50: oklch(0.97 0.015 240);
--color-brand-100: oklch(0.93 0.04 240);
--color-brand-200: oklch(0.86 0.07 240);
--color-brand-300: oklch(0.78 0.11 240);
--color-brand-400: oklch(0.7 0.14 240);
--color-brand-500: oklch(0.62 0.16 240);
--color-brand-600: oklch(0.54 0.18 245);
--color-brand-700: oklch(0.46 0.18 250);
--color-ink-50: oklch(0.985 0.003 250);
--color-ink-100: oklch(0.96 0.005 250);
--color-ink-200: oklch(0.92 0.008 250);
--color-ink-300: oklch(0.84 0.012 250);
--color-ink-400: oklch(0.65 0.018 250);
--color-ink-500: oklch(0.5 0.02 250);
--color-ink-600: oklch(0.38 0.022 250);
--color-ink-700: oklch(0.28 0.024 250);
--color-ink-800: oklch(0.2 0.026 250);
--color-ink-900: oklch(0.13 0.028 255);
--color-ink-950: oklch(0.08 0.03 260);
--radius-card: 1.25rem;
--shadow-card: 0 1px 0 0 oklch(1 0 0 / 0.04) inset, 0 12px 32px -12px oklch(0 0 0 / 0.18);
}
/* ---------------------------------------------------------------------------
2. Theme variables (light + dark)
These are the runtime CSS variables read by both this file and
app/page.tsx (via classes like `bg-[var(--surface)]`). Each variable has
a light value here and a dark override further down inside the
`@media (prefers-color-scheme: dark)` block.
--------------------------------------------------------------------------- */
:root {
color-scheme: light dark;
--bg: var(--color-ink-50);
--surface: oklch(1 0 0 / 0.7);
--surface-strong: oklch(1 0 0 / 0.92);
--text: var(--color-ink-900);
--text-soft: var(--color-ink-500);
--border: oklch(0.13 0.028 255 / 0.1);
--border-strong: oklch(0.13 0.028 255 / 0.18);
--accent: var(--color-brand-500);
--accent-soft: oklch(0.62 0.16 240 / 0.12);
--ring: oklch(0.62 0.16 240 / 0.4);
--grid: oklch(0.5 0.02 250 / 0.08);
--button-bg: var(--color-ink-900);
--button-fg: var(--color-ink-50);
--button-hover: var(--color-ink-800);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: var(--color-ink-950);
--surface: oklch(0.13 0.028 255 / 0.55);
--surface-strong: oklch(0.13 0.028 255 / 0.85);
--text: var(--color-ink-100);
--text-soft: var(--color-ink-400);
--border: oklch(1 0 0 / 0.08);
--border-strong: oklch(1 0 0 / 0.16);
--accent: var(--color-brand-400);
--accent-soft: oklch(0.7 0.14 240 / 0.18);
--ring: oklch(0.7 0.14 240 / 0.5);
--grid: oklch(0.84 0.012 250 / 0.06);
--button-bg: var(--color-ink-100);
--button-fg: var(--color-ink-950);
--button-hover: var(--color-ink-50);
}
}
/* ---------------------------------------------------------------------------
3. Base styles — applied to plain HTML elements before any classes hit.
--------------------------------------------------------------------------- */
@layer base {
html {
scroll-behavior: smooth;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
::selection {
background: var(--accent-soft);
color: var(--text);
}
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
border-radius: 4px;
}
}
/* ---------------------------------------------------------------------------
4. Site shell + ambient effects
The .site-shell class is on the <main> element in page.tsx. The two
radial gradients here follow the user's mouse via the --mx / --my CSS
variables that page.tsx writes on every mousemove.
--------------------------------------------------------------------------- */
.site-shell {
position: relative;
isolation: isolate;
min-height: 100vh;
background:
radial-gradient(
circle at var(--mx, 50%) var(--my, 22%),
oklch(0.62 0.16 240 / 0.12),
transparent 20%
),
radial-gradient(
circle at calc(var(--mx, 50%) * 0.65) calc(var(--my, 22%) * 1.2),
oklch(0.6 0.14 280 / 0.09),
transparent 24%
),
var(--bg);
}
.ambient-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, var(--grid) 1px, transparent 1px),
linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: linear-gradient(to bottom, oklch(0 0 0 / 0.5), transparent 80%);
}
.ambient-orb {
position: absolute;
border-radius: 9999px;
filter: blur(80px);
opacity: 0.22;
animation: drift 18s ease-in-out infinite;
will-change: transform;
}
.ambient-orb-a {
width: 32rem;
height: 32rem;
top: 4rem;
right: -10rem;
background: linear-gradient(
135deg,
oklch(0.7 0.14 240 / 0.55),
oklch(0.6 0.14 200 / 0.1)
);
}
.ambient-orb-b {
width: 26rem;
height: 26rem;
top: 28rem;
left: -8rem;
background: linear-gradient(
135deg,
oklch(0.6 0.14 280 / 0.35),
oklch(0.78 0.11 200 / 0.12)
);
animation-duration: 22s;
}
/* ---------------------------------------------------------------------------
5. Brand wordmark colour-scheme swap
The header logo has two PNG variants — a colour one for light mode and a
white one for dark mode. The `brand-light` / `brand-dark` classes (set
in page.tsx) live on both <Image> tags; only the matching one is shown.
--------------------------------------------------------------------------- */
.brand-light {
display: block;
}
.brand-dark {
display: none;
}
@media (prefers-color-scheme: dark) {
.brand-light {
display: none;
}
.brand-dark {
display: block;
}
}
/* ---------------------------------------------------------------------------
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 fades out at the
3-second mark so the SVG completes before it disappears. Total intro
length is matched in page.tsx's setTimeout (3600ms).
When .intro-running is on the <main> element, the page content
underneath is hidden so it doesn't flash through during the animation.
--------------------------------------------------------------------------- */
.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 600ms ease 3s forwards;
}
.intro-backdrop {
position: absolute;
inset: 0;
background: 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;
}
.intro-running .site-header,
.intro-running main > section,
.intro-running main > footer {
opacity: 0;
}
@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.
--------------------------------------------------------------------------- */
@keyframes drift {
0%,
100% {
transform: translate3d(0, 0, 0) scale(1);
}
50% {
transform: translate3d(0, 18px, 0) scale(1.06);
}
}
@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
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.
--------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.ambient-orb,
.intro-overlay,
.intro-wordmark {
animation: none !important;
}
*,
*::before,
*::after {
transition-duration: 0ms !important;
animation-duration: 0ms !important;
}
}
+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata, Viewport } from "next";
import "./globals.css";
import CookieBanner from "@/components/CookieBanner";
export const metadata: Metadata = {
metadataBase: new URL("https://novarix.uk"),
title: {
default: "Novarix Networks",
template: "%s | Novarix Networks",
},
description:
"Novarix Networks provides network consulting, remote network support, and architecture services for organisations running production network infrastructure.",
applicationName: "Novarix Networks",
keywords: [
"Novarix Networks",
"ISP",
"Managed Service Provider",
"MSP",
"Network Consulting",
"Internet Connectivity",
"BGP",
"IXP",
"CDN Edge",
"Network Engineering",
],
authors: [{ name: "Novarix Networks" }],
creator: "Novarix Networks",
publisher: "Novarix Networks",
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
url: "https://novarix.uk",
siteName: "Novarix Networks",
title: "Novarix Networks",
description:
"Engineering-led network consulting, remote support, and architecture for production networks.",
},
twitter: {
card: "summary_large_image",
title: "Novarix Networks",
description:
"Engineering-led network consulting, remote support, and architecture for production networks.",
},
robots: {
index: true,
follow: true,
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#020617" },
],
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en-GB" suppressHydrationWarning>
<body>
{children}
<CookieBanner />
</body>
</html>
);
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
+499
View File
@@ -0,0 +1,499 @@
"use client";
// =============================================================================
// Novarix Networks — Homepage
// =============================================================================
//
// This file controls the LAYOUT and STYLING of the homepage.
// All editable TEXT lives in `/content.ts` at the project root.
//
// 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)
//
// 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).
// =============================================================================
import Image from "next/image";
import Link from "next/link";
import { useEffect, 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 };
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.
// ---------------------------------------------------------------------------
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. We remember they've seen it using
// sessionStorage so it doesn't replay on every navigation. The intro lasts
// ~3.2 seconds, then fades out.
// ---------------------------------------------------------------------------
useEffect(() => {
try {
const introSeen = window.sessionStorage.getItem("novarix-intro-seen");
// Only set the "seen" flag if the user has accepted cookies. Without
// consent we still play the intro — we just don't remember it played.
const consent = window.localStorage.getItem("novarix-cookie-consent");
if (!introSeen) {
// Sync once with sessionStorage on first client mount.
// eslint-disable-next-line react-hooks/set-state-in-effect
setShowIntro(true);
if (consent === "ack") {
window.sessionStorage.setItem("novarix-intro-seen", "true");
}
// The SVG's built-in stroke + fill animation lasts ~3s.
// We hold for an extra ~600ms so the CSS fade-out can complete.
const timer = window.setTimeout(() => {
setShowIntro(false);
}, 3600);
return () => window.clearTimeout(timer);
}
} catch {
/* storage unavailable — silently skip the intro */
}
}, []);
// ---------------------------------------------------------------------------
// BACKGROUND POSITION
// Translates the current mouse pointer into two CSS variables (--mx, --my)
// that the .site-shell background gradient reads. Memoised so React doesn't
// rebuild the style object on every render.
// ---------------------------------------------------------------------------
const backgroundStyle = useMemo(
() =>
({
["--mx"]: `${pointer.x}%`,
["--my"]: `${pointer.y}%`,
}) as React.CSSProperties,
[pointer.x, pointer.y]
);
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 ~3.6s
(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 });
}}
>
{/* -------------------------------------------------------------------
AMBIENT LAYER
Two soft floating "orbs" and a faint grid behind everything.
Purely decorative. All styles live in globals.css under
.ambient-orb / .ambient-grid. Sits at z-index -10 so it's
under the page content.
------------------------------------------------------------------- */}
<div
className="pointer-events-none fixed inset-0 -z-10 overflow-hidden"
aria-hidden="true"
>
<div className="ambient-orb ambient-orb-a" />
<div className="ambient-orb ambient-orb-b" />
<div className="ambient-grid" />
</div>
{/* ===================================================================
HEADER
Sticky at the top of the page. Contains the wordmark logo (links
back to top) and the primary navigation pulled from
content.ts -> site.nav. Two logo images swap automatically for
light/dark mode.
=================================================================== */}
<header className="site-header sticky top-0 z-50 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--bg)_82%,transparent)] backdrop-blur-xl">
<div className="mx-auto flex h-20 w-full max-w-6xl items-center justify-between gap-6 px-6 sm:h-24 sm:px-8">
{/* Logo / wordmark */}
<a
href="#top"
aria-label="Novarix Networks — back to top"
className="inline-flex shrink-0 items-center"
>
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-light h-9 w-auto max-w-[42vw] object-contain sm:h-12"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-dark h-9 w-auto max-w-[42vw] object-contain sm:h-12"
/>
</a>
{/* Desktop navigation — hidden on small screens (sm:flex) */}
<nav
aria-label="Primary"
className="hidden items-center gap-7 text-sm text-[var(--text-soft)] sm:flex"
>
{site.nav.map((item) => (
<a
key={item.href}
href={item.href}
className="transition-colors hover:text-[var(--text)]"
>
{item.label}
</a>
))}
{/* Pill-style Contact button on the right */}
<a
href="#contact"
className="inline-flex items-center gap-2 rounded-full border border-[var(--border-strong)] px-4 py-1.5 text-[var(--text)] transition-all hover:border-[var(--accent)] hover:text-[var(--accent)]"
>
Contact
<span aria-hidden="true"></span>
</a>
</nav>
{/* Mobile-only Contact pill — replaces the full nav on small screens */}
<a
href="#contact"
className="inline-flex items-center rounded-full border border-[var(--border-strong)] px-3 py-1.5 text-xs text-[var(--text)] transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)] sm:hidden"
>
Contact
</a>
</div>
</header>
{/* ===================================================================
HERO
The first thing visitors see: status pill, eyebrow, big headline,
descriptive paragraph, and two call-to-action buttons.
All text comes from content.ts -> site.hero.
=================================================================== */}
<section className="relative pt-24 pb-20 sm:pt-32 sm:pb-28">
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
<div className="max-w-4xl">
{/* Optional pulsing-dot pill. Hidden if site.hero.badge is "" */}
{site.hero.badge && (
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-3 py-1 text-xs font-medium text-[var(--text-soft)] backdrop-blur">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent)] opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--accent)]" />
</span>
{site.hero.badge}
</span>
)}
{/* Small uppercase eyebrow */}
<p className="mt-7 text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.hero.eyebrow}
</p>
{/* Main headline. The middle word ("accent") is shown in a
brand-coloured gradient. Font size scales with viewport. */}
<h1 className="mt-5 text-[clamp(2.5rem,6vw,4.8rem)] leading-[1.04] font-semibold tracking-[-0.04em] text-balance">
{site.hero.headlineBefore}{" "}
<span className="bg-gradient-to-r from-brand-500 to-brand-700 bg-clip-text text-transparent dark:from-brand-300 dark:to-brand-500">
{site.hero.headlineAccent}
</span>{" "}
{site.hero.headlineAfter}
</h1>
{/* Supporting paragraph */}
<p className="mt-7 max-w-2xl text-lg leading-relaxed text-[var(--text-soft)] sm:text-xl">
{site.hero.description}
</p>
{/* Call-to-action buttons */}
<div className="mt-10 flex flex-wrap items-center gap-3">
{/* Primary button — solid background */}
<a
href={site.hero.primaryCta.href}
className="group inline-flex h-12 items-center gap-2 rounded-xl bg-[var(--button-bg)] px-5 text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
{site.hero.primaryCta.label}
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-0.5"
>
</span>
</a>
{/* Secondary button — outlined */}
<a
href={site.hero.secondaryCta.href}
className="inline-flex h-12 items-center rounded-xl border border-[var(--border-strong)] bg-[var(--surface)] px-5 text-sm font-semibold text-[var(--text)] backdrop-blur transition-all hover:-translate-y-0.5 hover:border-[var(--accent)] hover:text-[var(--accent)]"
>
{site.hero.secondaryCta.label}
</a>
</div>
</div>
</div>
</section>
{/* ===================================================================
SERVICES
"What we do" — section heading then a 3-column grid of cards
(1 column on mobile). The number of cards comes from
content.ts -> site.services.items, so adding a fourth card there
automatically appears here. Below the grid sits an optional
dashed-border "Capabilities" line.
=================================================================== */}
<section
id="services"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
{/* Section heading */}
<div className="max-w-3xl">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.services.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.services.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.services.description}
</p>
</div>
{/* Service cards grid — 1 column on mobile, 3 columns on md+ */}
<div className="mt-12 grid gap-5 sm:gap-6 md:grid-cols-3">
{site.services.items.map((service) => (
<article
key={service.title}
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-7 backdrop-blur transition-all hover:-translate-y-1 hover:border-[var(--border-strong)] hover:shadow-[0_20px_40px_-20px_oklch(0_0_0/0.25)]"
>
{/* Thin gradient hairline at the top of each card on hover */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-[var(--border-strong)] to-transparent opacity-0 transition-opacity group-hover:opacity-100"
/>
{/* Card top row: index number + arrow that appears on hover */}
<div className="flex items-center justify-between">
<span className="font-mono text-xs tracking-widest text-[var(--text-soft)]">
{service.index}
</span>
<span
aria-hidden="true"
className="text-[var(--text-soft)] opacity-0 transition-all group-hover:translate-x-0.5 group-hover:opacity-100"
>
</span>
</div>
{/* Card title + body */}
<h3 className="mt-5 text-lg font-semibold tracking-tight">
{service.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--text-soft)]">
{service.description}
</p>
</article>
))}
</div>
{/* Optional capabilities strip. Hidden if capabilities is "". */}
{site.services.capabilities && (
<div className="mt-10 rounded-2xl border border-dashed border-[var(--border)] p-5 text-sm leading-relaxed text-[var(--text-soft)] sm:p-6">
<span className="font-semibold tracking-wide text-[var(--text)] uppercase">
Capabilities ·
</span>{" "}
{site.services.capabilities}
</div>
)}
</div>
</section>
{/* ===================================================================
HOW WE ENGAGE
Three engagement-model cards. Same shape as Services but the
cards are simpler (no number, no hover arrow). Cards come from
content.ts -> site.engage.items.
=================================================================== */}
<section
id="engage"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-6xl px-6 sm:px-8">
{/* Section heading */}
<div className="max-w-3xl">
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.engage.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.engage.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.engage.description}
</p>
</div>
{/* Engagement cards grid */}
<div className="mt-12 grid gap-5 sm:gap-6 md:grid-cols-3">
{site.engage.items.map((engagement) => (
<div
key={engagement.title}
className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-7 backdrop-blur"
>
<h3 className="text-lg font-semibold tracking-tight">
{engagement.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--text-soft)]">
{engagement.description}
</p>
</div>
))}
</div>
</div>
</section>
{/* ===================================================================
CONTACT
A single dark, rounded "callout" card with the contact email and
response-time note. The button uses a `mailto:` link so clicking
opens the visitor's mail app pre-addressed to us.
=================================================================== */}
<section
id="contact"
className="relative border-t border-[var(--border)] py-24 sm:py-28"
>
<div className="mx-auto w-full max-w-4xl px-6 sm:px-8">
<div className="overflow-hidden rounded-3xl border border-[var(--border)] bg-[var(--surface)] p-8 backdrop-blur sm:p-12">
{/* Section heading inside the card */}
<p className="text-xs font-semibold tracking-[0.18em] text-[var(--accent)] uppercase">
{site.contact.eyebrow}
</p>
<h2 className="mt-3 text-[clamp(1.9rem,3.2vw,2.75rem)] leading-tight font-semibold tracking-[-0.03em]">
{site.contact.title}
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-[var(--text-soft)] sm:text-lg">
{site.contact.description}
</p>
{/* Email button + small response-time note */}
<div className="mt-8 flex flex-wrap items-center gap-3">
<a
href={`mailto:${site.contact.email}`}
className="group inline-flex h-12 items-center gap-2 rounded-xl bg-[var(--button-bg)] px-5 text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
{site.contact.email}
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-0.5"
>
</span>
</a>
<span className="text-sm text-[var(--text-soft)]">
{site.contact.note}
</span>
</div>
</div>
</div>
</section>
{/* ===================================================================
FOOTER
Two columns on desktop: company details on the left, tagline +
copyright on the right. Stacks to one column on mobile.
All text comes from content.ts -> site.footer.
=================================================================== */}
<footer className="border-t border-[var(--border)] py-12">
<div className="mx-auto grid w-full max-w-6xl gap-8 px-6 sm:grid-cols-[1fr_auto] sm:px-8">
{/* Left column: company name, registration, contact email */}
<div className="text-sm leading-relaxed text-[var(--text-soft)]">
<p className="font-semibold text-[var(--text)]">
{site.footer.company}
</p>
<p className="mt-1">{site.footer.registered}</p>
<p className="mt-1">
<a
href={`mailto:${site.footer.email}`}
className="transition-colors hover:text-[var(--accent)]"
>
{site.footer.email}
</a>
</p>
</div>
{/* Right column: tagline, cookie links, copyright */}
<div className="flex flex-col gap-2 text-sm text-[var(--text-soft)] sm:items-end sm:text-right">
<span className="text-xs tracking-[0.14em] uppercase">
{site.footer.tagline}
</span>
{/* Cookie & privacy links — keep them small and unobtrusive */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs">
<Link
href={site.footer.cookiePolicyHref}
className="transition-colors hover:text-[var(--accent)]"
>
{site.footer.cookiePolicyLabel}
</Link>
<button
type="button"
onClick={openCookieBanner}
className="cursor-pointer text-left transition-colors hover:text-[var(--accent)]"
>
{site.footer.cookiePrefsLabel}
</button>
</div>
<span>
© {new Date().getFullYear()} {site.footer.company}
</span>
</div>
</div>
</footer>
</main>
</>
);
}
+154
View File
@@ -0,0 +1,154 @@
import Image from "next/image";
const services = [
{
title: "Internet Connectivity",
description:
"Business internet access with resilient routing, clear service boundaries, and engineering-led deployment.",
},
{
title: "Managed Network Services",
description:
"Operational support for routing, switching, firewalls, and production network infrastructure.",
},
{
title: "Network Consulting",
description:
"Architecture, troubleshooting, and design support for organisations and service providers.",
},
];
export default function HomePage() {
return (
<main id="top">
<header className="site-header">
<div className="container header-inner">
<a href="#top" className="brand-link" aria-label="Go to top">
<Image
src="/branding/novarix-wordmark-colour.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-image brand-image-light"
/>
<Image
src="/branding/novarix-wordmark-white.png"
alt="Novarix Networks"
width={2048}
height={430}
priority
className="brand-image brand-image-dark"
/>
</a>
<nav className="nav">
<a href="#services">Services</a>
<a href="#direction">Direction</a>
<a href="#contact">Contact</a>
</nav>
</div>
</header>
<section className="hero">
<div className="container">
<div className="hero-copy">
<p className="eyebrow">Connectivity Managed Services Network Engineering</p>
<h1>Network infrastructure services built for reliability and growth.</h1>
<p className="hero-text">
Novarix Networks provides internet connectivity, managed network
services, and engineering expertise for organisations that need
dependable infrastructure and clear technical ownership.
</p>
<div className="hero-actions">
<a href="#contact" className="button button-primary">
Contact
</a>
<a href="#services" className="button button-secondary">
Services
</a>
</div>
</div>
</div>
</section>
<section id="services" className="section section-border">
<div className="container">
<div className="section-heading">
<h2>Services</h2>
<p>
Focus the first version of the business on what can be delivered
credibly now.
</p>
</div>
<div className="card-grid">
{services.map((service) => (
<article key={service.title} className="card">
<h3>{service.title}</h3>
<p>{service.description}</p>
</article>
))}
</div>
</div>
</section>
<section id="direction" className="section section-border">
<div className="container narrow">
<div className="section-heading">
<h2>Platform Direction</h2>
<p>
The platform is designed to expand beyond connectivity and managed
services toward deeper interconnection capabilities, including
exchange participation and edge infrastructure where commercially
justified.
</p>
</div>
<div className="direction-list">
<div className="direction-item">
<span className="direction-phase">Phase 1</span>
<h3>Connectivity, support, consulting</h3>
<p>Lead with services that are operationally clear and saleable immediately.</p>
</div>
<div className="direction-item">
<span className="direction-phase">Phase 2</span>
<h3>Interconnect and peering</h3>
<p>Add exchange participation and partner interconnection as the footprint matures.</p>
</div>
<div className="direction-item">
<span className="direction-phase">Phase 3</span>
<h3>Edge infrastructure</h3>
<p>Introduce selective CDN edge or regional platform capability only where demand supports it.</p>
</div>
</div>
</div>
</section>
<section id="contact" className="section section-border">
<div className="container narrow">
<div className="section-heading">
<h2>Contact</h2>
<p>
For enquiries regarding connectivity, managed network support, or
consulting services:
</p>
</div>
<a className="contact-link" href="mailto:hello@novarix.network">
hello@novarix.network
</a>
</div>
</section>
<footer className="footer section-border">
<div className="container footer-inner">
<span>© {new Date().getFullYear()} Novarix Networks</span>
</div>
</footer>
</main>
);
}
+11
View File
@@ -0,0 +1,11 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://novarix.uk/sitemap.xml",
};
}
+20
View File
@@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
return [
{
url: "https://novarix.uk",
lastModified,
changeFrequency: "monthly",
priority: 1,
},
{
url: "https://novarix.uk/cookies",
lastModified,
changeFrequency: "yearly",
priority: 0.3,
},
];
}
+135
View File
@@ -0,0 +1,135 @@
"use client";
// =============================================================================
// CookieBanner
// =============================================================================
//
// A small, non-blocking cookie / consent banner shown on first visit.
// Two buttons: ACK (accept) / RST (reject) — network-engineer humour.
//
// 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).
// - "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.
// - The footer "Cookie preferences" link dispatches a window event that
// re-opens this banner so users can change their mind.
//
// All visible text comes from `/content.ts` -> site.cookies.
// =============================================================================
import Link from "next/link";
import { useEffect, 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";
type Consent = "unknown" | "ack" | "rst";
export default function CookieBanner() {
// Start hidden on the server and on first client paint to avoid a flash.
const [visible, setVisible] = useState(false);
// Read the stored consent on mount and decide whether to show the banner.
useEffect(() => {
try {
const stored = window.localStorage.getItem(CONSENT_KEY) as Consent | null;
if (stored !== "ack" && stored !== "rst") {
// No decision yet — show the banner. We delay slightly so the page
// settles in first; feels less aggressive than appearing instantly.
const t = window.setTimeout(() => setVisible(true), 600);
return () => window.clearTimeout(t);
}
} catch {
// localStorage unavailable (private mode, blocked, etc.) — show the
// banner so the user is still told. Sync-once-on-mount is a legitimate
// use of setState in an effect.
// eslint-disable-next-line react-hooks/set-state-in-effect
setVisible(true);
}
}, []);
// Listen for the "re-open" event from the footer "Cookie preferences" link.
useEffect(() => {
const handler = () => setVisible(true);
window.addEventListener(REOPEN_EVENT, handler);
return () => window.removeEventListener(REOPEN_EVENT, handler);
}, []);
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);
}
} catch {
/* storage unavailable — nothing to do */
}
setVisible(false);
}
if (!visible) return null;
return (
<div
role="dialog"
aria-live="polite"
aria-label="Cookie consent"
className="fixed inset-x-0 bottom-0 z-[1500] flex justify-center p-3 sm:p-5"
>
<div className="pointer-events-auto w-full max-w-3xl rounded-2xl border border-[var(--border-strong)] bg-[var(--surface-strong)] p-4 shadow-[0_24px_60px_-20px_oklch(0_0_0/0.45)] backdrop-blur-xl sm:p-5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
{/* Message + policy link */}
<p className="text-sm leading-relaxed text-[var(--text)]">
{site.cookies.message}{" "}
<Link
href={site.cookies.policyHref}
className="font-medium text-[var(--accent)] underline-offset-4 hover:underline"
>
{site.cookies.policyLabel}
</Link>
</p>
{/* Buttons */}
<div className="flex flex-shrink-0 gap-2">
<button
type="button"
onClick={() => decide("rst")}
aria-label={site.cookies.reject.ariaLabel}
title={site.cookies.reject.tooltip}
className="group inline-flex h-11 min-w-[5.5rem] flex-col items-center justify-center rounded-xl border border-[var(--border-strong)] bg-transparent px-4 font-mono text-sm font-semibold text-[var(--text)] transition-all hover:-translate-y-0.5 hover:border-[var(--text)]"
>
<span className="leading-none">{site.cookies.reject.label}</span>
<span className="mt-0.5 text-[0.65rem] font-normal tracking-wide text-[var(--text-soft)] uppercase">
{site.cookies.reject.subtitle}
</span>
</button>
<button
type="button"
onClick={() => decide("ack")}
aria-label={site.cookies.accept.ariaLabel}
title={site.cookies.accept.tooltip}
className="group inline-flex h-11 min-w-[5.5rem] flex-col items-center justify-center rounded-xl bg-[var(--button-bg)] px-4 font-mono text-sm font-semibold text-[var(--button-fg)] shadow-sm transition-all hover:-translate-y-0.5 hover:bg-[var(--button-hover)] hover:shadow-md"
>
<span className="leading-none">{site.cookies.accept.label}</span>
<span className="mt-0.5 text-[0.65rem] font-normal tracking-wide opacity-70 uppercase">
{site.cookies.accept.subtitle}
</span>
</button>
</div>
</div>
</div>
</div>
);
}
// Helper used by the footer "Cookie preferences" link to re-open the banner.
export function openCookieBanner() {
window.dispatchEvent(new Event(REOPEN_EVENT));
}
+179
View File
@@ -0,0 +1,179 @@
// =============================================================================
// Novarix Networks — Website Content
// =============================================================================
//
// This is the ONLY file you need to edit to change the words on the website.
//
// Editing rules:
// 1. Anything inside "double quotes" is text shown on the website.
// 2. Keep the quotes around every piece of text.
// 3. Each line ends with a comma — leave the commas alone.
// 4. To add a new service or engagement, copy a whole "{ ... }" block,
// paste it inside the same square brackets [ ... ], and edit the text.
// 5. Save the file, then run `npm run build` to publish the changes.
//
// See EDITING.md in this folder for a friendlier walk-through with examples.
// =============================================================================
export const site = {
// ---------------------------------------------------------------------------
// HERO — the big section at the very top of the page
// ---------------------------------------------------------------------------
hero: {
// Small pill above the headline. Set to "" (empty) to hide.
badge: "Onboarding new accounts",
// Small uppercase line above the headline.
eyebrow: "Network Consulting · Remote Support · Engineering",
// The big headline is split into three parts so one word can be coloured.
// headlineBefore -> normal text on the left
// headlineAccent -> the word shown in the brand gradient
// headlineAfter -> normal text on the right
headlineBefore: "Network expertise, on tap, for teams running",
headlineAccent: "production",
headlineAfter: "infrastructure.",
// Paragraph under the headline.
description:
"Novarix Networks provides engineering-led consulting, remote support, and architecture services for organisations and service providers that need clear technical ownership of their network.",
// Two buttons. `href` can be a section anchor (e.g. "#contact") or a URL.
primaryCta: { label: "Get in touch", href: "#contact" },
secondaryCta: { label: "Explore services", href: "#services" },
},
// ---------------------------------------------------------------------------
// SERVICES — three cards under "What we do"
// ---------------------------------------------------------------------------
services: {
eyebrow: "What we do",
title: "Services",
description:
"We focus on the work we can deliver credibly today — engineering-led consulting, hands-on remote support, and network design for organisations that need an experienced second pair of eyes.",
// List of service cards. To add a fourth service, copy one block,
// paste it inside the [ ... ] and edit. Keep the commas between blocks.
items: [
{
index: "01",
title: "Network Consulting",
description:
"Architecture review, design, and second-opinion engineering for organisations and service providers running production networks.",
},
{
index: "02",
title: "Remote Network Support",
description:
"Hands-on remote support for routing, switching, and firewall environments — incident response, change work, and ongoing operations.",
},
{
index: "03",
title: "Architecture & Design",
description:
"New-build network design, refresh planning, and migration support — from single-site refreshes to multi-site routing topologies.",
},
],
// Dashed-border line under the cards.
// Set to "" (empty) to hide it entirely.
capabilities:
"BGP, OSPF, IS-IS, MPLS, segment routing, IPv4 / IPv6, firewalls (Palo Alto, FortiGate, pfSense), routing platforms (Cisco IOS-XE / IOS-XR, Juniper Junos, Arista EOS, MikroTik RouterOS), and Linux-based networking.",
},
// ---------------------------------------------------------------------------
// ENGAGEMENTS — three cards under "How we engage"
// ---------------------------------------------------------------------------
engage: {
eyebrow: "Working with us",
title: "How we engage",
description:
"Three engagement models, depending on what you need. Most engagements start with a short scoping conversation — no obligation, no charge.",
items: [
{
title: "Scoped projects",
description:
"Fixed-deliverable engagements with a clear statement of work — designs, audits, migrations, refreshes.",
},
{
title: "Monthly retainer",
description:
"An agreed block of remote engineering hours each month for ongoing operations, change work, and on-call cover.",
},
{
title: "Ad-hoc support",
description:
"Incident-driven engagements for one-off troubleshooting, escalations, and short pieces of design work.",
},
],
},
// ---------------------------------------------------------------------------
// CONTACT — the dark contact card near the bottom
// ---------------------------------------------------------------------------
contact: {
eyebrow: "Get in touch",
title: "Lets talk infrastructure.",
description:
"For consulting enquiries, remote support, or scoping a piece of design work — reach out directly. We respond to all business enquiries within one working day.",
// The button shows your email address; it also opens the user's mail app.
email: "hello@novarix.uk",
// Small grey line shown next to the email button.
note: "Replies within one working day.",
},
// ---------------------------------------------------------------------------
// NAV — links shown in the top header (in order, left to right)
// ---------------------------------------------------------------------------
nav: [
{ label: "Services", href: "#services" },
{ label: "How we engage", href: "#engage" },
],
// ---------------------------------------------------------------------------
// FOOTER — at the very bottom of the page
// ---------------------------------------------------------------------------
footer: {
company: "Novarix Networks Limited",
registered: "Registered in England · Company No. 17047180",
email: "hello@novarix.uk",
tagline: "Engineered in the UK",
// Small link in the footer that re-opens the cookie banner.
cookiePrefsLabel: "Cookie preferences",
// Link to the cookie policy page.
cookiePolicyLabel: "Cookie & Privacy Policy",
cookiePolicyHref: "/cookies",
},
// ---------------------------------------------------------------------------
// COOKIE BANNER — the bar that appears at the bottom on first visit
// ---------------------------------------------------------------------------
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.
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.",
// The two buttons. Labels are small jokes for network folk; subtitles
// and aria-labels make them clear to everyone else.
accept: {
label: "ACK",
subtitle: "Allow",
ariaLabel: "Accept cookies",
tooltip: "Acknowledged — preference saved",
},
reject: {
label: "RST",
subtitle: "Decline",
ariaLabel: "Reject cookies",
tooltip: "Connection reset — preference declined",
},
// Inline link to the policy page from the banner.
policyLabel: "Read the policy",
policyHref: "/cookies",
},
} as const;
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Novarix Networks website — deploy script
# -----------------------------------------------------------------------------
# Run this on the Ubuntu Nginx VM after pushing changes to Gitea:
#
# cd /var/www/novarix.uk
# ./deploy.sh
#
# It pulls the latest code, rebuilds the static site into out/, and reloads
# Nginx. Run once with `chmod +x deploy.sh` if it isn't executable yet.
# -----------------------------------------------------------------------------
set -euo pipefail
echo "==> Pulling latest from Gitea"
git pull origin main
echo "==> Installing dependencies"
npm install --no-package-lock
echo "==> Building static site into out/"
npm run build
echo "==> Testing Nginx config"
sudo nginx -t
echo "==> Reloading Nginx"
sudo systemctl reload nginx
echo "==> Done. Site updated at $(date -Iseconds)"
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+17
View File
@@ -0,0 +1,17 @@
import type { NextConfig } from "next";
// Build the site as a fully static export so it can be served by plain
// nginx with no Node runtime on the server. The `npm run build` step
// produces an `out/` directory containing all the HTML, CSS, JS, and
// image assets.
const nextConfig: NextConfig = {
reactStrictMode: true,
output: "export",
images: {
// next/image cannot use the optimisation server in static export mode,
// so images are served as-is from /public.
unoptimized: true,
},
};
export default nextConfig;
+78
View File
@@ -0,0 +1,78 @@
# -----------------------------------------------------------------------------
# Novarix Networks — Nginx site config
#
# Place at: /etc/nginx/sites-available/novarix.uk
# Then: sudo ln -s /etc/nginx/sites-available/novarix.uk \
# /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
#
# This server listens on plain HTTP only — TLS termination happens upstream
# in Nginx Proxy Manager. Adjust if you front it with something else.
# -----------------------------------------------------------------------------
server {
listen 80;
listen [::]:80;
server_name novarix.uk www.novarix.uk;
# Static export from `npm run build` — this directory must exist after
# the first deploy.
root /var/www/novarix.uk/out;
index index.html;
# Don't leak nginx version
server_tokens off;
# Reasonable defaults
charset utf-8;
client_max_body_size 1M;
# ---------------------------------------------------------------------
# Routing for Next.js static export
# ---------------------------------------------------------------------
# Pretty URLs: /services -> /services.html, falling back to 404.html
location / {
try_files $uri $uri.html $uri/index.html =404;
}
# ---------------------------------------------------------------------
# Caching
# ---------------------------------------------------------------------
# Hashed Next.js assets (JS/CSS/fonts) — cache forever, immutable
location /_next/static/ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Other static assets in /public — sensible long cache
location ~* \.(?:ico|css|js|gif|jpe?g|png|webp|svg|woff2?|ttf|eot|json)$ {
access_log off;
add_header Cache-Control "public, max-age=2592000";
}
# robots / sitemap should not be cached aggressively
location = /robots.txt {
add_header Cache-Control "public, max-age=300";
}
location = /sitemap.xml {
add_header Cache-Control "public, max-age=300";
}
# ---------------------------------------------------------------------
# Hardening
# ---------------------------------------------------------------------
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Custom error page (Next.js generates this at build time)
error_page 404 /404.html;
# ---------------------------------------------------------------------
# Don't serve hidden files
# ---------------------------------------------------------------------
location ~ /\.(?!well-known) {
deny all;
}
}
+6613
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "novarix-site",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"lottie-react": "^2.4.1",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 349 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}
File diff suppressed because one or more lines are too long