feat: initial website implementation
React 18 + TypeScript + Vite + Tailwind CSS website for netOrk. Built by a parallel instance based on the product docs. Pages: Home (landing), Features, Drivers, Getting Started Components: Nav, Footer Assets: public/logo.png (1024x1024 RGBA) Docker: multi-stage Dockerfile (node:20 builder + nginx:alpine) Compose: netork-website service on proxy-net (Zoraxy integration) docs/DESIGN.md: Logo Assets section added (PNG only, SVG removed); wordmark pattern and <head> references documented. .gitignore: fixed (was literal \n, now proper newlines); excludes node_modules/, dist/, memory/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
00c483c2a2
commit
efa01b5f41
@@ -0,0 +1,450 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
ChartBarIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
PuzzlePieceIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
|
||||
const DRIVERS = [
|
||||
'OpenWRT', 'OPNsense', 'Proxmox VE', 'Linux', 'HP ProCurve / Aruba',
|
||||
'TP-Link Jetstream', 'Netgear', 'Fritz!Box', 'Zyxel', 'OpenMediaVault',
|
||||
'Sonos', 'Cisco IOS', 'Cisco IOS-XE', 'Cisco NX-OS', 'Arista EOS', 'Juniper JunOS',
|
||||
]
|
||||
|
||||
function BrowserFrame({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-700 overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center gap-1.5 border-b border-slate-700 bg-slate-800 px-4 py-2.5">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-red-500/70" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-yellow-500/70" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-green-500/70" />
|
||||
<span className="ml-4 text-xs text-slate-500 font-mono">{label}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: 'active' | 'warning' | 'offline' }) {
|
||||
const cls =
|
||||
status === 'active' ? 'bg-green-500/20 text-green-400' :
|
||||
status === 'warning' ? 'bg-yellow-500/20 text-yellow-400' :
|
||||
'bg-red-500/20 text-red-400'
|
||||
return (
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function MockDeviceList() {
|
||||
const rows = [
|
||||
{ name: 'fw-001.lan', ip: '10.0.0.1', driver: 'OPNsense', status: 'active' as const },
|
||||
{ name: 'sw-core.lan', ip: '10.0.0.2', driver: 'HP ProCurve', status: 'warning' as const },
|
||||
{ name: 'ap-001.lan', ip: '10.0.0.10', driver: 'OpenWRT', status: 'active' as const },
|
||||
{ name: 'ap-002.lan', ip: '10.0.0.11', driver: 'OpenWRT', status: 'warning' as const },
|
||||
{ name: 'pve-01.lan', ip: '10.0.0.20', driver: 'Proxmox VE', status: 'active' as const },
|
||||
{ name: 'nas-01.lan', ip: '10.0.0.30', driver: 'OpenMediaVault', status: 'active' as const },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="bg-slate-950 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-100">Devices</span>
|
||||
<span className="text-xs px-2.5 py-1 rounded-md bg-sky-600 text-white">+ Add device</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-800 bg-slate-900">
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-500">Hostname</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-500">IP</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-500 hidden sm:table-cell">Driver</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-slate-500">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.name} className="border-b border-slate-800 last:border-0 hover:bg-slate-800/40">
|
||||
<td className="px-3 py-2 font-mono text-sky-400">{r.name}</td>
|
||||
<td className="px-3 py-2 font-mono text-slate-400">{r.ip}</td>
|
||||
<td className="px-3 py-2 text-slate-400 hidden sm:table-cell">{r.driver}</td>
|
||||
<td className="px-3 py-2"><StatusBadge status={r.status} /></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MockDeviceDetail() {
|
||||
return (
|
||||
<div className="bg-slate-950 p-4">
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-4 mb-3">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-100">ap-001.lan</p>
|
||||
<p className="text-xs text-slate-500 font-mono">10.0.0.10</p>
|
||||
</div>
|
||||
<StatusBadge status="active" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div><span className="text-slate-500">Vendor</span><p className="text-slate-300">GL.iNet</p></div>
|
||||
<div><span className="text-slate-500">OS</span><p className="text-slate-300">OpenWRT 23.05</p></div>
|
||||
<div><span className="text-slate-500">Driver</span><p className="text-slate-300">openwrt</p></div>
|
||||
<div><span className="text-slate-500">Last poll</span><p className="text-slate-300">2 min ago</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs mb-3">
|
||||
{['Interfaces', 'LLDP', 'ARP', 'VLANs', 'Services'].map((t) => (
|
||||
<span key={t} className={`px-2 py-1 rounded text-xs ${t === 'Interfaces' ? 'bg-sky-600/20 text-sky-400' : 'text-slate-500 hover:text-slate-300'}`}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800 overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead><tr className="bg-slate-900 border-b border-slate-800">
|
||||
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">Interface</th>
|
||||
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">IP</th>
|
||||
<th className="px-3 py-1.5 text-left text-slate-500 font-medium">State</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{[['br-lan', '10.0.0.10/24', 'up'], ['wlan0', '—', 'up'], ['eth0', '—', 'up']].map(([iface, ip, state]) => (
|
||||
<tr key={iface} className="border-b border-slate-800 last:border-0">
|
||||
<td className="px-3 py-1.5 font-mono text-slate-300">{iface}</td>
|
||||
<td className="px-3 py-1.5 font-mono text-slate-400">{ip}</td>
|
||||
<td className="px-3 py-1.5"><span className="text-green-400">{state}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MockVlans() {
|
||||
return (
|
||||
<div className="bg-slate-950 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-100">VLANs</span>
|
||||
<span className="text-xs px-2.5 py-1 rounded-md bg-sky-600 text-white">+ Add VLAN</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ id: 10, name: 'management', devices: 6, drift: false },
|
||||
{ id: 20, name: 'iot', devices: 4, drift: true },
|
||||
{ id: 30, name: 'servers', devices: 3, drift: false },
|
||||
{ id: 40, name: 'guest', devices: 2, drift: false },
|
||||
].map((v) => (
|
||||
<div key={v.id} className="flex items-center justify-between rounded-lg border border-slate-800 bg-slate-900 px-3 py-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-mono text-slate-500 w-8">#{v.id}</span>
|
||||
<span className="text-xs text-slate-300">{v.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-500">{v.devices} devices</span>
|
||||
{v.drift
|
||||
? <span className="text-xs font-medium px-2 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400">drift</span>
|
||||
: <span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">clean</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
|
||||
<p className="text-xs text-yellow-400 font-medium mb-1">Drift detected — VLAN 20 (iot)</p>
|
||||
<p className="text-xs text-slate-400">ap-002.lan: VLAN name mismatch. Expected "iot", got "IoT-devices".</p>
|
||||
<button className="mt-2 text-xs px-2.5 py-1 rounded bg-sky-600 hover:bg-sky-500 text-white transition-colors">Fix now</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MockSecurity() {
|
||||
return (
|
||||
<div className="bg-slate-950 p-4">
|
||||
<div className="mb-3">
|
||||
<span className="text-xs font-semibold text-slate-100">Security — ap-001.lan</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-slate-300">Wazuh Agent</span>
|
||||
<StatusBadge status="active" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div className="text-center rounded bg-red-500/10 border border-red-500/20 py-1.5">
|
||||
<p className="text-red-400 font-semibold">2</p>
|
||||
<p className="text-slate-500">Critical</p>
|
||||
</div>
|
||||
<div className="text-center rounded bg-yellow-500/10 border border-yellow-500/20 py-1.5">
|
||||
<p className="text-yellow-400 font-semibold">7</p>
|
||||
<p className="text-slate-500">High</p>
|
||||
</div>
|
||||
<div className="text-center rounded bg-blue-500/10 border border-blue-500/20 py-1.5">
|
||||
<p className="text-blue-400 font-semibold">14</p>
|
||||
<p className="text-slate-500">Medium</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-300">Graylog syslog</span>
|
||||
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">forwarding</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">10.0.0.50:514 (UDP)</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-slate-300">CrowdSec</span>
|
||||
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-green-500/20 text-green-400">active</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">3 decisions · 0 bans (24 h)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
{/* 1 — Hero */}
|
||||
<section className="py-24 md:py-32">
|
||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-slate-100 leading-tight mb-6">
|
||||
Network orchestration<br />
|
||||
<span className="text-sky-400">for heterogeneous infrastructure.</span>
|
||||
</h1>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mx-auto mb-10">
|
||||
netOrk discovers, monitors, and manages your routers, switches, access points,
|
||||
firewalls, and servers from a single UI — regardless of vendor.
|
||||
No SaaS dependency. Runs on your infrastructure.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
<Link
|
||||
to="/docs/getting-started"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
|
||||
>
|
||||
Get started →
|
||||
</Link>
|
||||
<Link
|
||||
to="/features"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-slate-700 hover:border-slate-500 text-slate-300 hover:text-slate-100 transition-colors"
|
||||
>
|
||||
View features
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto px-6 mt-16">
|
||||
<BrowserFrame label="netork.local / devices">
|
||||
<MockDeviceList />
|
||||
</BrowserFrame>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 2 — Problem Statement */}
|
||||
<section className="py-16">
|
||||
<div className="max-w-3xl mx-auto px-6 text-center">
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
Managing a mixed network means juggling a different admin UI for every vendor —
|
||||
one for OPNsense, one for HP ProCurve, one for OpenWRT, one for Proxmox.
|
||||
Config changes happen directly on devices with no audit trail.
|
||||
You find out something drifted when it breaks.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 3 — Core Capabilities */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-sky-400" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">Discover everything on your network</h3>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
ICMP sweep, SNMP scan, and HTTP probing find devices before you add them.
|
||||
Fingerprinting identifies vendor and platform automatically.
|
||||
Adopt results into your inventory with a single click.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||
<ChartBarIcon className="h-5 w-5 text-sky-400" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">Poll device state continuously</h3>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
Every device is polled on a configurable interval via NAPALM.
|
||||
Interface status, ARP tables, DHCP leases, VLAN membership,
|
||||
Docker containers, and SNMP health metrics — all in one place.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-6">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||
<WrenchScrewdriverIcon className="h-5 w-5 text-sky-400" />
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-100">Detect drift. Fix it.</h3>
|
||||
<p className="text-sm text-slate-400 leading-relaxed">
|
||||
Define desired state in netOrk. On every poll, device config is compared against it.
|
||||
Drifted devices get a warning; a one-click fix stream applies the correction and
|
||||
shows you live SSH output.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 4 — Driver Grid */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Works with your hardware</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-xl mx-auto mb-10">
|
||||
netOrk ships with custom NAPALM drivers for 11 device types, plus all built-in NAPALM drivers.
|
||||
New drivers follow a documented registration pattern.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{DRIVERS.map((d) => (
|
||||
<span
|
||||
key={d}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full border border-slate-700 bg-slate-900 text-xs font-medium text-slate-300"
|
||||
>
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<Link to="/drivers" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
||||
Full driver reference →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 5 — Screenshot Walkthrough */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6 space-y-24">
|
||||
{/* Row 1 — left text, right screenshot */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Device detail at a glance</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
Hostname, IP, vendor, OS version, last poll time, and active warnings on one card.
|
||||
Tabbed detail view for interfaces, LLDP neighbors, ARP table, VLAN membership,
|
||||
packages, services, and scheduled jobs.
|
||||
</p>
|
||||
</div>
|
||||
<BrowserFrame label="netork.local / devices / ap-001">
|
||||
<MockDeviceDetail />
|
||||
</BrowserFrame>
|
||||
</div>
|
||||
|
||||
{/* Row 2 — right text, left screenshot */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<BrowserFrame label="netork.local / vlans">
|
||||
<MockVlans />
|
||||
</BrowserFrame>
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Intent-based VLAN and SSID management</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
Define VLAN names and SSID settings once. netOrk compares them against every polled
|
||||
device and pushes corrections automatically via UCI (OpenWRT) or the device's native API.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3 — left text, right screenshot */}
|
||||
<div className="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Security visibility per device</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
Wazuh agent status, CVE counts by severity, and recent alerts — all linked to the device
|
||||
record. One-click agent install if the agent is missing.
|
||||
Graylog syslog forwarding status with auto-fix.
|
||||
</p>
|
||||
</div>
|
||||
<BrowserFrame label="netork.local / devices / ap-001 / security">
|
||||
<MockSecurity />
|
||||
</BrowserFrame>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 6 — Plugin System */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-8 md:p-12">
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-sky-600/10">
|
||||
<PuzzlePieceIcon className="h-5 w-5 text-sky-400" />
|
||||
</div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Built to extend</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed max-w-2xl mb-6">
|
||||
Integrations (Wazuh, Graylog, CrowdSec, apt-cacher-ng) are plugins that register into
|
||||
the plugin system — they can be enabled or disabled per deployment without code changes.
|
||||
Adding a new integration follows a documented pattern with a hook bus, typed metadata,
|
||||
and a plugin registry.
|
||||
</p>
|
||||
<Link to="/plugins" className="text-sky-400 hover:text-sky-300 transition-colors text-sm font-medium">
|
||||
Plugin system docs →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 7 — Deployment */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid md:grid-cols-2 gap-12 items-start">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-4">Self-hosted. One command.</h2>
|
||||
<p className="text-base text-slate-400 leading-relaxed">
|
||||
netOrk runs in Docker Compose. Five containers: API, two worker pools, a Beat scheduler,
|
||||
and an nginx UI server. No external dependencies beyond Redis and PostgreSQL.
|
||||
</p>
|
||||
</div>
|
||||
<pre className="rounded-xl border border-slate-800 bg-slate-900 p-6 font-mono text-sm text-slate-300 overflow-x-auto">
|
||||
<code>{`# Clone + configure
|
||||
git clone https://gitea.example.com/netork/netork.git
|
||||
cp .env.example .env
|
||||
# edit .env (DB URL, Redis password, secret key)
|
||||
|
||||
# Deploy
|
||||
bash scripts/deploy.sh 192.168.1.10`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-t border-slate-800" />
|
||||
|
||||
{/* 8 — CTA Footer */}
|
||||
<section className="py-24">
|
||||
<div className="max-w-7xl mx-auto px-6 text-center">
|
||||
<h2 className="text-2xl md:text-3xl font-bold text-slate-100 mb-8">Start managing your network.</h2>
|
||||
<Link
|
||||
to="/docs/getting-started"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-sky-600 hover:bg-sky-500 text-white font-medium transition-colors"
|
||||
>
|
||||
Read the docs →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user