Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

@tailwind base;
@tailwind components;
@tailwind utilities;
Expand All @@ -20,7 +18,7 @@ html,
body {
background: #0A0A0A;
color: #F5F5F5;
font-family: 'Inter', Arial, sans-serif;
font-family: var(--font-geist-sans), system-ui, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Expand Down Expand Up @@ -107,3 +105,105 @@ body {
.animate-marquee {
animation: marquee 22s linear infinite;
}

/* ─── Linear/Vercel-grade surface system ───────────────────────────────────── */

/* Faint architectural grid, fades out toward edges */
.grid-lines {
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
background-size: 64px 64px;
}
.grid-fade {
-webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%);
mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%);
}

/* Hairline surface — flat translucent fill + 1px border (no gradient). */
.hairline {
background: rgba(255, 255, 255, 0.021);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.hairline-hover {
transition: border-color 0.3s ease, background 0.3s ease;
}
.hairline-hover:hover {
border-color: rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.035);
}

/* Solid headline colors (no gradient) */
.gradient-text { color: #f4f4f5; }
.gradient-accent { color: #57a5f2; }

/* Top-edge highlight line used on chrome / hero frame */
.edge-light {
position: relative;
}
.edge-light::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent);
}

/* Soft vertical rise-in used sparingly */
@keyframes float-y {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.animate-float-y { animation: float-y 6s ease-in-out infinite; }

/* Live blink for status dots */
@keyframes live-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.animate-live { animation: live-blink 1.6s ease-in-out infinite; }

/* Scanning sweep for the hero chart line */
@keyframes sweep {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
.animate-sweep { animation: sweep 3.2s ease-in-out infinite; }

/* Conic border spin (announcement pill) */
@keyframes spin-slow {
to { transform: rotate(1turn); }
}
.animate-spin-slow { animation: spin-slow 6s linear infinite; }

/* Radar — concentric "ping" rings expanding from the hero center */
@keyframes radar-ping {
0% { transform: scale(0.18); opacity: 0; }
12% { opacity: 0.5; }
100% { transform: scale(1); opacity: 0; }
}
.radar-ping {
position: absolute;
inset: 0;
border-radius: 9999px;
border: 1px solid rgba(255, 255, 255, 0.35);
animation: radar-ping 6s cubic-bezier(0.16, 0.84, 0.44, 1) infinite;
}
/* Slow rotation for the radar sweep arm */
@keyframes radar-spin { to { transform: rotate(360deg); } }
.radar-spin { animation: radar-spin 14s linear infinite; }

/* Slow shimmer sweep across product frames */
@keyframes frame-shine {
0% { transform: translateX(-120%) skewX(-12deg); opacity: 0; }
12% { opacity: 0.5; }
30% { transform: translateX(220%) skewX(-12deg); opacity: 0; }
100% { transform: translateX(220%) skewX(-12deg); opacity: 0; }
}
.animate-frame-shine { animation: frame-shine 7s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
.radar-ping, .radar-spin,
.animate-frame-shine, .animate-drift, .animate-marquee,
.animate-float-y, .animate-sweep, .animate-live { animation: none !important; }
}
17 changes: 9 additions & 8 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { Metadata } from "next";
import { Space_Grotesk, Inter } from "next/font/google";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { Sora } from "next/font/google";
import "./globals.css";
import { SessionProvider } from "@/components/providers/session-provider";
import { ProjectProvider } from "@/components/providers/project-provider";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const spaceGrotesk = Space_Grotesk({
const sora = Sora({
subsets: ["latin"],
weight: ["700"],
variable: "--font-space-grotesk",
weight: ["500", "600", "700"],
variable: "--font-sora",
});
import { SessionProvider } from "@/components/providers/session-provider";
import { ProjectProvider } from "@/components/providers/project-provider";

export const metadata: Metadata = {
title: "Liveboard — API Observability",
Expand All @@ -18,7 +19,7 @@ export const metadata: Metadata = {

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${inter.variable} ${spaceGrotesk.variable}`}>
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable} ${sora.variable}`}>
<body className="bg-[#0A0A0A] text-[#F5F5F5] antialiased font-sans">
<SessionProvider>
<ProjectProvider>
Expand Down
85 changes: 66 additions & 19 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { redirect } from "next/navigation";
import { auth } from "@/auth";
import { LandingNavbar } from "@/components/landing/navbar";
import { ScrollProgress } from "@/components/landing/scroll-progress";
import { Hero } from "@/components/landing/hero";
import { IntegrationsStrip } from "@/components/landing/integrations-strip";
import { Showcase } from "@/components/landing/showcase";
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";
Expand All @@ -14,26 +15,72 @@ export default async function Home() {
if (session) redirect("/overview");

return (
<main className="bg-grain min-h-screen overflow-x-hidden bg-[#050505]">
<div className="relative px-3 pt-3 sm:px-5 sm:pt-5">
<div
className="pointer-events-none absolute -top-16 -left-16 -z-10 h-64 w-64 rounded-full opacity-20 blur-[100px]"
style={{ background: "#3B82F6" }}
/>
<div
className="pointer-events-none absolute -bottom-16 -right-16 -z-10 h-64 w-64 rounded-full opacity-20 blur-[100px]"
style={{ background: "#A855F7" }}
/>

<div className="relative mx-auto max-w-[1400px] overflow-hidden rounded-[28px] border border-[#1A1A1A] bg-[#0A0A0A]">
<LandingNavbar />
<Hero />
</div>
</div>

<main className="bg-grain relative min-h-screen overflow-x-hidden bg-black">
<ScrollProgress />
<LandingNavbar />
<Hero />
<IntegrationsStrip />

<Showcase
id="tracing"
index="01"
eyebrow="Distributed tracing"
title="Follow one request across every hop"
body="A single slow checkout can hide a timeout three services deep. Liveboard stitches every span into one waterfall — flame or service-map view — so the culprit is obvious at a glance."
points={[
"Trace IDs propagate across every service automatically",
"Flame graph and critical-path views to see where time went",
"Errored spans bubble up to the exact downstream cause",
]}
image={{ src: "/shots/traces.png", alt: "Distributed trace flame graph across gateway, auth, product and payment services", url: "app.liveboard.dev/traces", width: 3040, height: 1900 }}
/>

<Showcase
id="endpoints"
index="02"
eyebrow="Endpoints"
title="Per-endpoint health, ranked"
body="Every route you serve, sorted by traffic — with p50 / p95 / p99, error rate and a rolling health score. Spot the one endpoint dragging your SLO before it pages you."
points={[
"Latency percentiles and error rate for each route",
"Health score surfaces degraded endpoints instantly",
"Filter by method, status class or p99 threshold",
]}
image={{ src: "/shots/endpoints.png", alt: "Endpoints table with request volume, error rate, latency percentiles and health scores", url: "app.liveboard.dev/endpoints", width: 3040, height: 1900 }}
reversed
/>

<Showcase
id="alerts"
index="03"
eyebrow="Alerts & routing"
title="Alerts that reach the right channel"
body="Build a rule in seconds — metric, threshold, window, severity — and route it to Slack, PagerDuty, Discord or a webhook. Anomalies get caught, summarized and delivered where your team already is."
points={[
"Point-and-click rule builder, no query language",
"Route by severity to Slack, PagerDuty, Discord or webhook",
"Firing state and delivery history at a glance",
]}
image={{ src: "/shots/alerts.png", alt: "Alert rules list and rule builder routing to Slack, PagerDuty and Discord", url: "app.liveboard.dev/alerts", width: 3040, height: 1900 }}
/>

<FeatureGrid />
<InsightsBento />

<Showcase
id="status-showcase"
index="04"
eyebrow="Status page"
title="A public status page, for free"
body="The same telemetry powers a branded, real-time status page with 90-day uptime history per component — no extra setup, no second tool to run."
points={[
"90-day uptime bars for every component",
"Live operational / degraded / outage states",
"Shares the data you already collect — zero config",
]}
image={{ src: "/shots/status.png", alt: "Public Liveboard status page with per-component 90-day uptime history", url: "status.liveboard.dev", width: 2480, height: 2000 }}
reversed
/>

<HowItWorks />
<CtaSection />
<LandingFooter />
Expand Down
46 changes: 46 additions & 0 deletions components/landing/browser-frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Image from "next/image";

interface BrowserFrameProps {
src: string;
alt: string;
url?: string;
width: number;
height: number;
priority?: boolean;
live?: boolean;
}

export function BrowserFrame({ src, alt, url = "app.liveboard.dev", width, height, priority, live }: BrowserFrameProps) {
return (
<div className="edge-light overflow-hidden rounded-xl border border-white/10 bg-[#0A0A0B] shadow-2xl shadow-black/70">
<div className="flex items-center gap-2 border-b border-white/[0.07] bg-white/[0.02] px-3.5 py-2.5">
<div className="flex gap-1.5">
<span className="h-2.5 w-2.5 rounded-full bg-[#2c2c31]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#2c2c31]" />
<span className="h-2.5 w-2.5 rounded-full bg-[#2c2c31]" />
</div>
<div className="mx-auto flex items-center gap-1.5 rounded-md bg-black/40 px-3 py-1">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" className="text-[#55555c]">
<rect x="4" y="10" width="16" height="11" rx="2" stroke="currentColor" strokeWidth="2" />
<path d="M8 10V7a4 4 0 118 0v3" stroke="currentColor" strokeWidth="2" />
</svg>
<span className="font-mono text-[10px] text-[#6b6b74]">{url}</span>
</div>
{live && (
<div className="flex items-center gap-1.5 rounded-full border border-[#22C55E]/25 bg-[#22C55E]/10 px-2 py-0.5">
<span className="h-1.5 w-1.5 rounded-full bg-[#22C55E] animate-live" />
<span className="text-[10px] font-medium text-[#22C55E]">Live</span>
</div>
)}
</div>
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
className="block h-auto w-full"
/>
</div>
);
}
63 changes: 63 additions & 0 deletions components/landing/code-snippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { cn } from "@/lib/utils";

/* Lightweight, hand-tokenized syntax highlighting for the handful of code
snippets on the landing page. Palette tuned to the dark theme. */

export const tok = {
fn: "text-[#6aa9f0]", // functions / methods / commands
str: "text-[#5ac98a]", // strings, packages, urls
prop: "text-[#c795f0]", // properties / identifiers of note
txt: "text-[#d4d4dc]", // plain identifiers
punc: "text-[#5f5f68]", // punctuation / brackets
prompt: "text-[#4a4a52] select-none", // shell prompt / arrow
};

export function CodeSnippet({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<code className={cn("font-mono text-[11.5px] leading-relaxed", className)}>{children}</code>
);
}

// ── Prebuilt, reused snippets ────────────────────────────────────────────────

export function InstallSnippet({ prompt = true }: { prompt?: boolean }) {
return (
<CodeSnippet>
{prompt && <span className={tok.prompt}>$ </span>}
<span className={tok.fn}>npm</span>
<span className={tok.txt}> install </span>
<span className={tok.str}>liveboard-sdk</span>
</CodeSnippet>
);
}

export function MiddlewareSnippet() {
return (
<CodeSnippet>
<span className={tok.txt}>app</span>
<span className={tok.fn}>.use</span>
<span className={tok.punc}>(</span>
<span className={tok.txt}>liveboard</span>
<span className={tok.fn}>.middleware</span>
<span className={tok.punc}>({"{ "}</span>
<span className={tok.prop}>apiKey</span>
<span className={tok.punc}>{" }"})</span>
<span className={tok.punc}>)</span>
</CodeSnippet>
);
}

export function DashboardUrlSnippet() {
return (
<CodeSnippet>
<span className={tok.prompt}>→ </span>
<span className={tok.str}>liveboard.dev/overview</span>
</CodeSnippet>
);
}
Loading
Loading