From d1486d5ec8992afe17b04c8b82878922ba0850b1 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Fri, 25 Sep 2026 13:19:06 +0200 Subject: [PATCH] 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 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitignore | 1 + AGENTS.md | 3 +- landing/.gitignore | 2 + landing/{ => app}/fonts/geist-mono-var.woff2 | Bin landing/{ => app}/fonts/geist-var.woff2 | Bin landing/app/globals.css | 67 + landing/{assets/dash.svg => app/icon.svg} | 0 landing/app/layout.tsx | 45 + landing/app/page.tsx | 270 +++ landing/assets/board.png | Bin 96422 -> 0 bytes landing/assets/domains.png | Bin 54454 -> 0 bytes landing/assets/monitors.png | Bin 73379 -> 0 bytes landing/assets/status-page.png | Bin 66345 -> 0 bytes landing/components/Board.tsx | 268 +++ landing/components/CopyButton.tsx | 35 + landing/components/Install.tsx | 38 + landing/components/Mocks.tsx | 241 +++ landing/components/Nav.tsx | 51 + landing/components/RendererDemo.tsx | 143 ++ landing/components/Reveal.tsx | 26 + landing/css.d.ts | 1 + landing/index.html | 465 ----- landing/next-env.d.ts | 6 + landing/next.config.mjs | 10 + landing/package-lock.json | 1834 ++++++++++++++++++ landing/package.json | 23 + landing/postcss.config.mjs | 5 + landing/public/dash.svg | 9 + landing/tsconfig.json | 21 + oxlint.config.ts | 1 + 30 files changed, 3099 insertions(+), 466 deletions(-) rename landing/{ => app}/fonts/geist-mono-var.woff2 (100%) rename landing/{ => app}/fonts/geist-var.woff2 (100%) create mode 100644 landing/app/globals.css rename landing/{assets/dash.svg => app/icon.svg} (100%) create mode 100644 landing/app/layout.tsx create mode 100644 landing/app/page.tsx delete mode 100644 landing/assets/board.png delete mode 100644 landing/assets/domains.png delete mode 100644 landing/assets/monitors.png delete mode 100644 landing/assets/status-page.png create mode 100644 landing/components/Board.tsx create mode 100644 landing/components/CopyButton.tsx create mode 100644 landing/components/Install.tsx create mode 100644 landing/components/Mocks.tsx create mode 100644 landing/components/Nav.tsx create mode 100644 landing/components/RendererDemo.tsx create mode 100644 landing/components/Reveal.tsx create mode 100644 landing/css.d.ts delete mode 100644 landing/index.html create mode 100644 landing/next-env.d.ts create mode 100644 landing/next.config.mjs create mode 100644 landing/package-lock.json create mode 100644 landing/package.json create mode 100644 landing/postcss.config.mjs create mode 100644 landing/public/dash.svg create mode 100644 landing/tsconfig.json diff --git a/.gitignore b/.gitignore index 5e9b36a..b8deb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist/ .playwright-mcp/ .playwright-cli/ .vercel +.env* diff --git a/AGENTS.md b/AGENTS.md index 1aa429e..35e087d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,8 @@ Optional auth (off by default), no YAML. See `ROADMAP.md` (canonical) and - `apps/frontend` — React 18 + Vite + TS strict + Tailwind v4 + shadcn/ui - `apps/backend` — Go + Gin + zap, CGO-free, SQLite via `modernc.org/sqlite` - `packages/api-client` — generated TS client; `openapi.yaml` at repo root is the contract -- `landing/` — static marketing page, deployed to Vercel; no build step, plain HTML/CSS/JS +- `landing/` — Next.js 15 marketing site (own package.json, not a workspace); + Vercel project `dash`, root dir `landing`, git-connected to this repo - `infra/` — Dockerfile (multi-stage, one image) - `tools/oxlint/anti-slop` — vendored lint plugin (do not extend casually) - `data/` — runtime SQLite + uploaded icons (gitignored) diff --git a/landing/.gitignore b/landing/.gitignore index 245259b..a9ffab7 100644 --- a/landing/.gitignore +++ b/landing/.gitignore @@ -1,2 +1,4 @@ .vercel .env* +node_modules/ +.next/ diff --git a/landing/fonts/geist-mono-var.woff2 b/landing/app/fonts/geist-mono-var.woff2 similarity index 100% rename from landing/fonts/geist-mono-var.woff2 rename to landing/app/fonts/geist-mono-var.woff2 diff --git a/landing/fonts/geist-var.woff2 b/landing/app/fonts/geist-var.woff2 similarity index 100% rename from landing/fonts/geist-var.woff2 rename to landing/app/fonts/geist-var.woff2 diff --git a/landing/app/globals.css b/landing/app/globals.css new file mode 100644 index 0000000..87de80a --- /dev/null +++ b/landing/app/globals.css @@ -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; + } +} diff --git a/landing/assets/dash.svg b/landing/app/icon.svg similarity index 100% rename from landing/assets/dash.svg rename to landing/app/icon.svg diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx new file mode 100644 index 0000000..38ecb66 --- /dev/null +++ b/landing/app/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/landing/app/page.tsx b/landing/app/page.tsx new file mode 100644 index 0000000..bd4daaf --- /dev/null +++ b/landing/app/page.tsx @@ -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 ( +
+

+ {label} +

+ + {tag} +
+ ); +} + +function Feature({ + flip = false, + title, + children, + mock, +}: { + flip?: boolean; + title: string; + children: React.ReactNode; + mock: React.ReactNode; +}) { + return ( + +
+
+

{title}

+

{children}

+
+
{mock}
+
+
+ ); +} + +export default function Page() { + return ( + <> +