Initial-commit
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 & 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'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't do
|
||||
</h2>
|
||||
<p className="mt-3">
|
||||
We don't set tracking cookies. We don't run Google
|
||||
Analytics, Plausible, Fathom, Matomo, or any equivalent. We
|
||||
don't serve advertising. We don't embed third-party
|
||||
scripts that profile you across websites. We don'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'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've mishandled your personal data, you
|
||||
have the right to complain to the UK's data protection
|
||||
authority, the Information Commissioner'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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 548 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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: "Let’s 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 you’ve seen the intro animation. We don’t 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;
|
||||
@@ -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)"
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 349 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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"]
|
||||
}
|
||||