diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx deleted file mode 100644 index 831b73b..0000000 --- a/app/(dashboard)/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from "next/navigation"; - -export default function RootPage() { - redirect("/overview"); -} diff --git a/app/globals.css b/app/globals.css index d4ecea5..665a1fc 100644 --- a/app/globals.css +++ b/app/globals.css @@ -76,3 +76,34 @@ body { border: 1px solid #1E1E1E !important; border-radius: 8px !important; } + +/* ─── Landing page motion ──────────────────────────────────────────────────── */ + +.bg-grain { + position: relative; +} +.bg-grain::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + opacity: 0.05; + mix-blend-mode: overlay; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} + +@keyframes drift { + 0%, 100% { transform: translate(-50%, 0) rotate(0deg) scale(1); } + 50% { transform: translate(-50%, 18px) rotate(8deg) scale(1.06); } +} +.animate-drift { + animation: drift 14s ease-in-out infinite; +} + +@keyframes marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +.animate-marquee { + animation: marquee 22s linear infinite; +} diff --git a/app/page.tsx b/app/page.tsx index 4bb2145..48534dc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,42 @@ import { redirect } from "next/navigation"; +import { auth } from "@/auth"; +import { LandingNavbar } from "@/components/landing/navbar"; +import { Hero } from "@/components/landing/hero"; +import { IntegrationsStrip } from "@/components/landing/integrations-strip"; +import { FeatureGrid } from "@/components/landing/feature-grid"; +import { InsightsBento } from "@/components/landing/insights-bento"; +import { HowItWorks } from "@/components/landing/how-it-works"; +import { CtaSection } from "@/components/landing/cta-section"; +import { LandingFooter } from "@/components/landing/footer"; -export default function Home() { - redirect("/overview"); +export default async function Home() { + const session = await auth(); + if (session) redirect("/overview"); + + return ( +
+
+
+
+ +
+ + +
+
+ + + + + + + +
+ ); } diff --git a/components/landing/cta-section.tsx b/components/landing/cta-section.tsx new file mode 100644 index 0000000..6ce1bae --- /dev/null +++ b/components/landing/cta-section.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { GithubIcon } from "@/components/landing/github-icon"; +import { Reveal } from "@/components/landing/reveal"; + +export function CtaSection() { + return ( +
+
+ +

+ Ship with confidence. +

+

+ Open source, self-hostable, and free to start. Find out about the incident + before your customers do. +

+
+ + + + + + +
+
+
+ ); +} diff --git a/components/landing/feature-grid.tsx b/components/landing/feature-grid.tsx new file mode 100644 index 0000000..c549ce5 --- /dev/null +++ b/components/landing/feature-grid.tsx @@ -0,0 +1,172 @@ +import { Sparkles, Zap } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Reveal } from "@/components/landing/reveal"; +import { SpotlightCard } from "@/components/landing/spotlight-card"; + +const FLAME_SPANS = [ + { service: "gateway", width: "100%", color: "#3B82F6", indent: 0 }, + { service: "auth", width: "22%", color: "#A855F7", indent: 1 }, + { service: "orders-api", width: "74%", color: "#06B6D4", indent: 1 }, + { service: "postgres", width: "58%", color: "#F59E0B", indent: 2 }, + { service: "payments", width: "31%", color: "#F43F5E", indent: 2 }, +]; + +const UPTIME_DAYS = Array.from({ length: 48 }, (_, i) => (i === 31 ? "yellow" : i === 32 ? "red" : "green")); + +const TOP_USERS = [ + { id: "u_2291", reqs: "12.4k", color: "#3B82F6" }, + { id: "u_0144", reqs: "9.8k", color: "#A855F7" }, + { id: "u_3387", reqs: "7.1k", color: "#06B6D4" }, +]; + +export function FeatureGrid() { + return ( +
+ + Platform +

+ Everything you need to know what your API is doing +

+

+ Not another generic APM dashboard — built around the questions you actually + ask at 2am. +

+
+ +
+ {/* Onboarding — small */} + + +
+ +

60-second onboarding

+

+ One line of middleware. No agents, no sidecars, no config files to maintain. +

+
+ $ npm install liveboard-sdk +
+
+
+
+ + {/* Distributed tracing — small with mock flame graph */} + + +
+ + + +

Distributed tracing

+

+ Trace IDs propagate across every service automatically. See exactly where time went. +

+
+ {FLAME_SPANS.map((span, i) => ( +
+
+
+ ))} +
+
+ + + + {/* AI incident summaries — featured, spans 2 rows on md+ */} + + +
+ +

AI incident summaries

+

+ When error rate or p99 latency breaks 3 standard deviations from baseline, + an LLM reads the surrounding traffic and writes the postmortem before you do. +

+ +
+
+ Critical + 2m ago +
+

+ p99 latency spike on /api/checkout +

+

+ Latency jumped 6.4x starting 14:02 UTC, correlated with a downstream timeout + from the payments provider. 312 requests affected. Recommend adding a circuit + breaker around the payment client. +

+
+ + Resolved in 4m +
+
+
+
+
+ + {/* User-centric — small */} + + +
+ + + + +

User-centric observability

+

+ Every event carries a user ID. Know exactly who an incident hits, not just which route. +

+
+ {TOP_USERS.map((u) => ( +
+ + {u.id} + {u.reqs} req +
+ ))} +
+
+
+
+ + {/* Status page — small */} + + +
+ + + + +

Auto-generated status page

+

+ A public, branded status page ships for free — fed by the same data, zero extra setup. +

+
+
+ 99.97% uptime + 48 days +
+
+ {UPTIME_DAYS.map((color, i) => ( + + ))} +
+
+
+
+
+
+
+ ); +} diff --git a/components/landing/footer.tsx b/components/landing/footer.tsx new file mode 100644 index 0000000..f6989e4 --- /dev/null +++ b/components/landing/footer.tsx @@ -0,0 +1,94 @@ +import Link from "next/link"; +import { LiveboardIcon } from "@/components/logo"; +import { GithubIcon } from "@/components/landing/github-icon"; + +interface FooterLink { + label: string; + href: string; + external?: boolean; +} + +const COLUMNS: { title: string; links: FooterLink[] }[] = [ + { + title: "Product", + links: [ + { label: "Features", href: "#features" }, + { label: "How it works", href: "#how-it-works" }, + { label: "Status page", href: "/status" }, + ], + }, + { + title: "Developers", + links: [ + { label: "GitHub", href: "https://github.com/ryzrr/liveboard", external: true }, + { label: "JS SDK (npm)", href: "https://github.com/ryzrr/liveboard/tree/main/packages/sdk-js", external: true }, + { label: "Python SDK (PyPI)", href: "https://github.com/ryzrr/liveboard/tree/main/packages/sdk-python", external: true }, + ], + }, + { + title: "Account", + links: [ + { label: "Sign in", href: "/auth/signin" }, + { label: "Dashboard", href: "/overview" }, + ], + }, +]; + +export function LandingFooter() { + return ( + + ); +} diff --git a/components/landing/github-icon.tsx b/components/landing/github-icon.tsx new file mode 100644 index 0000000..50c738f --- /dev/null +++ b/components/landing/github-icon.tsx @@ -0,0 +1,7 @@ +export function GithubIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/components/landing/hero.tsx b/components/landing/hero.tsx new file mode 100644 index 0000000..300a89b --- /dev/null +++ b/components/landing/hero.tsx @@ -0,0 +1,94 @@ +import Link from "next/link"; +import { ArrowUpRight, Play } from "lucide-react"; +import { Reveal } from "@/components/landing/reveal"; +import { NetworkGraph } from "@/components/landing/network-graph"; + +export function Hero() { + return ( +
+ {/* Misty ambient glow — built from the Liveboard mark's own palette */} +
+
+ + + +
+ + + + + + + + Real-time API observability + + + +

+ Know the moment +
+ your API breaks. +

+ +

+ One line of middleware. Real-time traces, error tracking, and AI incident summaries. +

+ +
+ + Open dashboard + + + + See how it works + + + {/* Falling light rays */} +
+ + + +
+
+
+
+ +
+ + + + + + 01 / 03 · Scroll down +
+ +
+ Service map +
+ + + +
+
+
+ ); +} diff --git a/components/landing/how-it-works.tsx b/components/landing/how-it-works.tsx new file mode 100644 index 0000000..cd1a13b --- /dev/null +++ b/components/landing/how-it-works.tsx @@ -0,0 +1,57 @@ +import { Reveal } from "@/components/landing/reveal"; +import { SpotlightCard } from "@/components/landing/spotlight-card"; + +const STEPS = [ + { + n: "01", + title: "Install the SDK", + body: "Pick your language. The JS SDK supports Express and Fastify; the Python SDK supports FastAPI, Django, and Flask.", + code: "npm install liveboard-sdk", + }, + { + n: "02", + title: "Add one line", + body: "Wire the middleware with your project's API key. No restart-the-world config, no separate collector to deploy.", + code: "app.use(liveboard.middleware({ apiKey }))", + }, + { + n: "03", + title: "Watch it live", + body: "Requests start streaming into your dashboard within seconds. Anomalies get flagged and summarized automatically.", + code: "→ liveboard.dev/overview", + }, +]; + +export function HowItWorks() { + return ( +
+
+ + Setup +

+ From zero to live data in under a minute +

+
+ +
+ {STEPS.map((step, i) => ( + + +
+ + {step.n} + +

{step.title}

+

{step.body}

+
+ {step.code} +
+
+
+
+ ))} +
+
+
+ ); +} diff --git a/components/landing/insights-bento.tsx b/components/landing/insights-bento.tsx new file mode 100644 index 0000000..d01f204 --- /dev/null +++ b/components/landing/insights-bento.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Sparkline } from "@/components/charts/sparkline"; +import { Reveal } from "@/components/landing/reveal"; +import { SpotlightCard } from "@/components/landing/spotlight-card"; + +const UPTIME_TREND = [99.91, 99.93, 99.95, 99.9, 99.96, 99.97, 99.94, 99.97, 99.98, 99.97]; + +const LATENCY_BARS = [ + { route: "/checkout", ms: 86, color: "#3B82F6" }, + { route: "/orders", ms: 41, color: "#06B6D4" }, + { route: "/users/:id", ms: 18, color: "#A855F7" }, + { route: "/auth", ms: 12, color: "#F59E0B" }, + { route: "/cart/:id", ms: 24, color: "#F43F5E" }, +]; + +const TOP_ENDPOINTS = [ + { route: "/api/checkout", pct: 92, color: "#3B82F6" }, + { route: "/api/orders", pct: 74, color: "#06B6D4" }, + { route: "/api/users/:id", pct: 58, color: "#A855F7" }, + { route: "/api/cart/:id", pct: 35, color: "#F59E0B" }, + { route: "/api/auth", pct: 22, color: "#F43F5E" }, +]; + +function jitter(value: number, spread: number): number { + return Math.max(0, value + (Math.random() - 0.5) * spread); +} + +export function InsightsBento() { + const [requests, setRequests] = useState(8204); + const [errorRate, setErrorRate] = useState(0.42); + + useEffect(() => { + const interval = setInterval(() => { + setRequests((r) => Math.round(jitter(r, 600))); + setErrorRate((e) => Math.round(jitter(e, 0.3) * 100) / 100); + }, 1800); + return () => clearInterval(interval); + }, []); + + return ( +
+ + Insights +

+ Meet your traffic copilot +

+

+ Real numbers from a live Liveboard project — every panel below updates on its own, + the same way your dashboard would. +

+
+ + + {/* Uptime — tall panel */} + +
+

Uptime

+
+
+ + + + +
+ 99.97% + 30 days +
+
+
+
+

Successful requests

+
+ +
+
+
+
+ + {/* Latency by endpoint — wide panel */} + +
+

Latency by endpoint

+
+ {LATENCY_BARS.map((bar) => ( +
+ {bar.ms}ms +
+ {bar.route} +
+ ))} +
+
+ + + {/* Live counters */} + +
+
+

Requests / min

+
+ {requests.toLocaleString()} + +
+
+
+

Error rate

+
+ {errorRate.toFixed(2)}% + within budget +
+
+
+
+ + {/* Top endpoints by volume */} + +
+

Top endpoints by volume

+
+ {TOP_ENDPOINTS.map((ep) => ( +
+ {ep.route} +
+
+
+
+ ))} +
+
+ + +
+ ); +} diff --git a/components/landing/integrations-strip.tsx b/components/landing/integrations-strip.tsx new file mode 100644 index 0000000..6018cda --- /dev/null +++ b/components/landing/integrations-strip.tsx @@ -0,0 +1,26 @@ +import { Reveal } from "@/components/landing/reveal"; + +const STACKS = ["Express", "Fastify", "FastAPI", "Django", "Flask", "Node.js", "Python"]; +const LOOP = [...STACKS, ...STACKS]; + +export function IntegrationsStrip() { + return ( + +

+ Instruments your stack +

+
+
+ {LOOP.map((name, i) => ( + + {name} + + ))} +
+
+
+ ); +} diff --git a/components/landing/navbar.tsx b/components/landing/navbar.tsx new file mode 100644 index 0000000..9069b34 --- /dev/null +++ b/components/landing/navbar.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { ArrowUpRight, Menu, User, X } from "lucide-react"; +import { LiveboardIcon } from "@/components/logo"; +import { GithubIcon } from "@/components/landing/github-icon"; + +const LINKS = [ + { label: "Features", href: "#features" }, + { label: "Insights", href: "#insights" }, + { label: "How it works", href: "#how-it-works" }, +]; + +const GITHUB_URL = "https://github.com/ryzrr/liveboard"; + +export function LandingNavbar() { + const [open, setOpen] = useState(false); + + return ( +
+ + + {open && ( +
+
+ {LINKS.map((link) => ( + setOpen(false)} + className="text-sm text-[#888] hover:text-[#F5F5F5] transition-colors" + > + {link.label} + + ))} + setOpen(false)} className="text-sm text-[#888] hover:text-[#F5F5F5] transition-colors"> + Status page + + + GitHub + + + Get started + + +
+
+ )} +
+ ); +} diff --git a/components/landing/network-graph.tsx b/components/landing/network-graph.tsx new file mode 100644 index 0000000..34b5f22 --- /dev/null +++ b/components/landing/network-graph.tsx @@ -0,0 +1,66 @@ +const ICONS: Record = { + gateway: ( + + + + ), + auth: ( + + + + + ), + orders: ( + + + + + ), + payments: ( + + + + ), +}; + +const NODES = [ + { id: "gateway", label: "gateway", stat: "12.4k req", x: 13, y: 24 }, + { id: "auth", label: "auth-service", stat: "3.2k req", x: 87, y: 20 }, + { id: "orders", label: "orders-api", stat: "41ms p99", x: 10, y: 76 }, + { id: "payments", label: "payments", stat: "0.42% err", x: 90, y: 72 }, +]; + +export function NetworkGraph() { + return ( +
+ + {NODES.map((node) => ( + + ))} + + + {NODES.map((node) => ( +
+ + {ICONS[node.id]} + +
+

{node.label}

+

{node.stat}

+
+
+ ))} +
+ ); +} diff --git a/components/landing/reveal.tsx b/components/landing/reveal.tsx new file mode 100644 index 0000000..256110a --- /dev/null +++ b/components/landing/reveal.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { motion } from "framer-motion"; + +interface RevealProps { + children: React.ReactNode; + delay?: number; + className?: string; + y?: number; +} + +export function Reveal({ children, delay = 0, className, y = 16 }: RevealProps) { + return ( + + {children} + + ); +} diff --git a/components/landing/spotlight-card.tsx b/components/landing/spotlight-card.tsx new file mode 100644 index 0000000..cabf63f --- /dev/null +++ b/components/landing/spotlight-card.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useRef } from "react"; +import { cn } from "@/lib/utils"; + +interface SpotlightCardProps { + children: React.ReactNode; + className?: string; +} + +export function SpotlightCard({ children, className }: SpotlightCardProps) { + const ref = useRef(null); + + function handleMouseMove(e: React.MouseEvent) { + const el = ref.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + el.style.setProperty("--x", `${e.clientX - rect.left}px`); + el.style.setProperty("--y", `${e.clientY - rect.top}px`); + } + + return ( +
+
+ {children} +
+ ); +}