mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-29 08:04:58 +00:00
feat(landing): rebuild as Next.js app with live product mocks
Replaces the static HTML page with a Next.js 15 + Tailwind v4 + Motion app. The hero renders the dashboard itself - live tiles, ticking clock, wandering metrics, pulsing status dots - instead of screenshots. Adds a playable four-renderer switcher (bento/cards/index/console) with layout animations, live monitor/domain/system mocks, and a cycling alert feed. Vercel project dash is now git-connected with root dir landing/; pushes deploy automatically. Canonical alias: dash-homelab.vercel.app. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
e88429514b
commit
d1486d5ec8
30 files changed
+3099
-466
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Product tokens — same palette as the app (apps/frontend/src/index.css). */
|
||||
@theme {
|
||||
--color-bg: #0a0a0a;
|
||||
--color-surface: #111111;
|
||||
--color-surface-hover: #161616;
|
||||
--color-line: rgba(255, 255, 255, 0.08);
|
||||
--color-line-strong: rgba(255, 255, 255, 0.14);
|
||||
--color-text: #fafafa;
|
||||
--color-dim: #a1a1aa;
|
||||
--color-faint: #52525b;
|
||||
--color-accent: #3b82f6;
|
||||
--color-up: #22c55e;
|
||||
--color-down: #f59e0b;
|
||||
--color-destructive: #ef4444;
|
||||
--color-iconbg: #1c1c1e;
|
||||
|
||||
--ease-snap: cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
--font-sans: var(--font-geist), ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
|
||||
|
||||
--animate-pulse-dot: pulse-dot 2.4s ease-in-out infinite;
|
||||
--animate-float: float 7s ease-in-out infinite;
|
||||
--animate-scan: scan 5s linear infinite;
|
||||
--animate-draw: draw 2.2s var(--ease-snap) forwards;
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.35); }
|
||||
50% { opacity: 0.75; box-shadow: 0 0 0 4px rgba(34, 197, 94, 0); }
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
@keyframes scan {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
@keyframes draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after {
|
||||
animation: none !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="104" fill="#0a0a0a"/>
|
||||
<g fill="#3b82f6">
|
||||
<rect x="120" y="120" width="124" height="124" rx="28"/>
|
||||
<rect x="268" y="120" width="124" height="124" rx="28" fill="#6b9ef8"/>
|
||||
<rect x="120" y="268" width="124" height="124" rx="28" fill="#1d4fd7"/>
|
||||
<rect x="268" y="268" width="124" height="124" rx="28"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,45 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
|
||||
const geist = localFont({
|
||||
src: "./fonts/geist-var.woff2",
|
||||
variable: "--font-geist",
|
||||
weight: "100 900",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = localFont({
|
||||
src: "./fonts/geist-mono-var.woff2",
|
||||
variable: "--font-geist-mono",
|
||||
weight: "100 900",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://dash-homelab.vercel.app"),
|
||||
title: "Dash - Self-hosted homelab dashboard",
|
||||
description:
|
||||
"Service boards, uptime monitors, domain watch, and system metrics in one container. A single static Go binary with an embedded React UI. No YAML, no external database.",
|
||||
openGraph: {
|
||||
title: "Dash - Self-hosted homelab dashboard",
|
||||
description:
|
||||
"Service boards, uptime monitors, domain watch, and system metrics in one container. One static Go binary, no YAML.",
|
||||
type: "website",
|
||||
images: ["/dash.svg"],
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#0a0a0a",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
|
||||
<body className="font-sans">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { Nav } from "../components/Nav";
|
||||
import { Board } from "../components/Board";
|
||||
import { RendererDemo } from "../components/RendererDemo";
|
||||
import { MonitorMock, DomainMock, SystemMock, StatusBand } from "../components/Mocks";
|
||||
import { Install } from "../components/Install";
|
||||
import { CopyButton } from "../components/CopyButton";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
|
||||
function SectionHead({ label, tag }: { label: string; tag: string }) {
|
||||
return (
|
||||
<div className="mb-7 flex items-center gap-3">
|
||||
<h2 className="font-mono text-[11px] font-semibold uppercase tracking-[0.14em] text-dim">
|
||||
{label}
|
||||
</h2>
|
||||
<span className="h-px flex-1 bg-line" />
|
||||
<span className="font-mono text-[11px] text-faint">{tag}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Feature({
|
||||
flip = false,
|
||||
title,
|
||||
children,
|
||||
mock,
|
||||
}: {
|
||||
flip?: boolean;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
mock: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Reveal>
|
||||
<div className="grid items-center gap-8 md:grid-cols-[5fr_7fr]">
|
||||
<div className={flip ? "md:order-2" : ""}>
|
||||
<h3 className="text-[22px] font-medium tracking-tight">{title}</h3>
|
||||
<p className="mt-2.5 max-w-[46ch] text-[14.5px] leading-relaxed text-dim">{children}</p>
|
||||
</div>
|
||||
<div className={flip ? "md:order-1" : ""}>{mock}</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
|
||||
{/* ---------- hero ---------- */}
|
||||
<header className="mx-auto max-w-5xl px-5 pt-20 sm:pt-24">
|
||||
<Reveal>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-line bg-surface px-3 py-1 font-mono text-[11.5px] text-dim">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-up animate-pulse-dot" />
|
||||
self-hosted · open source · MIT
|
||||
</span>
|
||||
</Reveal>
|
||||
<Reveal delay={0.06}>
|
||||
<h1 className="mt-6 max-w-[16ch] text-[42px] font-medium leading-[1.04] tracking-[-0.035em] sm:text-[64px]">
|
||||
Your homelab, on one calm screen.
|
||||
</h1>
|
||||
</Reveal>
|
||||
<Reveal delay={0.12}>
|
||||
<p className="mt-5 max-w-[58ch] text-[16.5px] leading-relaxed text-dim">
|
||||
Service boards, uptime monitors, domain watch, and system metrics - a
|
||||
single static Go binary with the UI baked in. No YAML, no external
|
||||
database, no mandatory accounts.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={0.18} className="mt-8 flex flex-wrap items-center gap-2.5">
|
||||
<a
|
||||
href="#install"
|
||||
className="flex h-10 items-center rounded-lg bg-text px-4 text-[14px] font-medium text-bg transition-transform duration-100 active:scale-[0.97]"
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/Dvorinka/Dash"
|
||||
className="flex h-10 items-center rounded-lg border border-line bg-surface px-4 text-[14px] font-medium text-dim transition-colors hover:border-line-strong hover:text-text"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<span className="flex h-10 items-center gap-1 rounded-lg border border-line bg-surface pl-3.5 pr-1.5 font-mono text-[12.5px]">
|
||||
<span className="text-faint">$</span>
|
||||
<code>docker run -p 3000:3000 ghcr.io/dvorinka/dash</code>
|
||||
<CopyButton
|
||||
text="docker run -d -v ./data:/data -p 3000:3000 ghcr.io/dvorinka/dash:latest"
|
||||
label="Copy docker command"
|
||||
/>
|
||||
</span>
|
||||
</Reveal>
|
||||
|
||||
{/* the product, rendered live */}
|
||||
<Reveal delay={0.24} className="relative mt-14">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -inset-x-8 -top-10 h-40 bg-[radial-gradient(60%_100%_at_50%_0%,rgba(59,130,246,0.10),transparent)]"
|
||||
/>
|
||||
<Board />
|
||||
</Reveal>
|
||||
|
||||
<Reveal className="mt-14 grid grid-cols-2 overflow-hidden rounded-xl border border-line sm:grid-cols-4">
|
||||
{[
|
||||
["Deploy", "one static binary"],
|
||||
["Database", "SQLite, embedded"],
|
||||
["Config", "UI-managed, no YAML"],
|
||||
["License", "MIT"],
|
||||
].map(([k, v], i) => (
|
||||
<div key={k} className={`px-5 py-4 ${i > 0 ? "border-l border-line" : ""} ${i >= 2 ? "max-sm:border-t max-sm:border-line" : ""} ${i === 2 ? "max-sm:border-l-0" : ""}`}>
|
||||
<div className="font-mono text-[10px] uppercase tracking-[0.12em] text-faint">{k}</div>
|
||||
<div className="mt-1.5 text-[14.5px] font-medium">{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</Reveal>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto max-w-5xl px-5">
|
||||
{/* ---------- renderers ---------- */}
|
||||
<section id="renderers" className="scroll-mt-24 pt-24">
|
||||
<SectionHead label="Boards" tag="the differentiator" />
|
||||
<div className="grid items-center gap-8 md:grid-cols-[4fr_8fr]">
|
||||
<Reveal>
|
||||
<h3 className="text-[22px] font-medium tracking-tight">
|
||||
One board state. Four skins.
|
||||
</h3>
|
||||
<p className="mt-2.5 max-w-[42ch] text-[14.5px] leading-relaxed text-dim">
|
||||
Bento, Cards, Index, and Console renderers share the same drag-drop,
|
||||
popovers, and state. Switching the look is a setting, not a migration.
|
||||
Try it - this demo is live.
|
||||
</p>
|
||||
</Reveal>
|
||||
<Reveal delay={0.1}>
|
||||
<RendererDemo />
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---------- monitors ---------- */}
|
||||
<section id="monitors" className="scroll-mt-24 pt-24">
|
||||
<SectionHead label="Monitors" tag="checks on a schedule" />
|
||||
<Feature title="Uptime that watches itself" mock={<MonitorMock />}>
|
||||
HTTP, TCP, ping, DNS, keyword, JSON-path, and push checks on an
|
||||
in-process scheduler. Heartbeat history, latency graphs, and alert
|
||||
rules per monitor - down means you hear about it, not discover it.
|
||||
</Feature>
|
||||
</section>
|
||||
|
||||
{/* ---------- domains ---------- */}
|
||||
<section className="pt-24">
|
||||
<SectionHead label="Domains" tag="rdap + whois + tls" />
|
||||
<Feature
|
||||
flip
|
||||
title="Every domain, accounted for"
|
||||
mock={<DomainMock />}
|
||||
>
|
||||
Expiry countdowns, TLS certificate chains, DNS records attributed to
|
||||
their provider, and CT-log subdomain discovery. Field-level history on
|
||||
every change.
|
||||
</Feature>
|
||||
</section>
|
||||
|
||||
{/* ---------- systems ---------- */}
|
||||
<section className="pt-24">
|
||||
<SectionHead label="Systems" tag="push-based agent" />
|
||||
<Feature title="Hosts that report in" mock={<SystemMock />}>
|
||||
A tiny <code className="rounded border border-line bg-surface px-1.5 py-0.5 font-mono text-[12.5px]">dash-agent</code>{" "}
|
||||
pushes CPU, memory, disk, network, temperatures, and per-container
|
||||
stats - SMART, ZFS, and GPU when the tooling exists. One binary per
|
||||
host, nothing else.
|
||||
</Feature>
|
||||
</section>
|
||||
|
||||
{/* ---------- status + alerts ---------- */}
|
||||
<section className="pt-24">
|
||||
<SectionHead label="Status & alerts" tag="public pages, real transports" />
|
||||
<Feature
|
||||
flip
|
||||
title="Tell users before they ask"
|
||||
mock={<StatusBand />}
|
||||
>
|
||||
Automatic incidents on monitor down, maintenance windows, public
|
||||
status pages, and SVG badges. Alerts dispatch through webhook, Slack,
|
||||
Discord, Telegram, Gotify, ntfy, or SMTP.
|
||||
</Feature>
|
||||
</section>
|
||||
|
||||
{/* ---------- stack ---------- */}
|
||||
<section id="stack" className="scroll-mt-24 pt-24">
|
||||
<SectionHead label="Stack" tag="boring, on purpose" />
|
||||
<Reveal>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
"go 1.24",
|
||||
"gin",
|
||||
"sqlite (modernc)",
|
||||
"embed.FS",
|
||||
"openapi contract",
|
||||
"react 18",
|
||||
"docker · ~34 MB",
|
||||
"goreleaser binaries",
|
||||
"prometheus /api/metrics",
|
||||
"pwa",
|
||||
].map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded-md border border-line bg-surface px-3 py-1.5 font-mono text-[12px] text-dim"
|
||||
>
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.08}>
|
||||
<p className="mt-5 max-w-[62ch] text-[14.5px] leading-relaxed text-dim">
|
||||
It needs a persistent scheduler, WHOIS on port 43, ICMP, and SMTP
|
||||
egress - none of which exist on serverless. So the deploy story is{" "}
|
||||
<code className="rounded border border-line bg-surface px-1.5 py-0.5 font-mono text-[12.5px]">docker run</code>
|
||||
, and this page is the only part of Dash hosted here.
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* ---------- install ---------- */}
|
||||
<section id="install" className="scroll-mt-24 pt-24">
|
||||
<SectionHead label="Install" tag="~1 min" />
|
||||
<Install />
|
||||
<Reveal delay={0.1}>
|
||||
<p className="mt-5 max-w-[62ch] text-[14.5px] leading-relaxed text-dim">
|
||||
Everything is configured from the UI at{" "}
|
||||
<code className="rounded border border-line bg-surface px-1.5 py-0.5 font-mono text-[12.5px]">localhost:3000</code>
|
||||
. All state lives in one directory. Importers for Homepage, Homarr,
|
||||
and Dashy configs are built in.
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
|
||||
{/* ---------- bottom cta ---------- */}
|
||||
<Reveal className="mt-24">
|
||||
<div className="flex flex-wrap items-center justify-between gap-6 rounded-2xl border border-line bg-gradient-to-b from-surface to-bg p-10">
|
||||
<div>
|
||||
<h2 className="text-[26px] font-medium tracking-tight">Run it in one command.</h2>
|
||||
<p className="mt-2 text-[14.5px] text-dim">MIT licensed. Self-hosted. Yours.</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/Dvorinka/Dash"
|
||||
className="flex h-11 items-center rounded-lg bg-text px-5 text-[14px] font-medium text-bg transition-transform duration-100 active:scale-[0.97]"
|
||||
>
|
||||
Star on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</Reveal>
|
||||
</main>
|
||||
|
||||
<footer className="mt-24 border-t border-line">
|
||||
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-5 px-5 py-8 text-[13px] text-faint">
|
||||
<span className="flex items-center gap-2">
|
||||
<img src="/dash.svg" alt="" width={18} height={18} className="rounded-[4px]" />
|
||||
Dash
|
||||
</span>
|
||||
<span>© 2026 · MIT License</span>
|
||||
<div className="ml-auto flex gap-5">
|
||||
<a className="text-dim transition-colors hover:text-text" href="https://github.com/Dvorinka/Dash">GitHub</a>
|
||||
<a className="text-dim transition-colors hover:text-text" href="https://github.com/Dvorinka/Dash/tree/main/docs">Docs</a>
|
||||
<a className="text-dim transition-colors hover:text-text" href="https://github.com/Dvorinka/Dash/releases">Releases</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user