From 3778ed5afea8c8fe0bea71f206c47f418b27c65a Mon Sep 17 00:00:00 2001 From: ryzrr Date: Wed, 5 Aug 2026 15:33:09 +0530 Subject: [PATCH 1/3] redesign: rebuild landing page in a sharp, Vercel-style UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full rebuild of the marketing landing page and a couple of related auth fixes found along the way. Landing page (components/landing/*, app/page.tsx): - Complete visual rebuild: sharp rectangular frames throughout, no border-radius anywhere, single blue accent on a near-black base, Geist Sans/Mono (matches the rest of the app's tokens). - New sections built from the real product: hero with the live Overview dashboard screenshot, an SDK/framework logo marquee, a two-column install snippet (JS/Python tabs, working copy button, line numbers), a feature bento grid (traces/endpoints/alerts, real screenshots), an AI anomaly-detection panel, a status-page showcase, an honest stats row, and two CTA tiles. - Floating sharp navbar; shared blue hover-fill treatment on every button-shaped CTA. - Footer wordmark: giant faded "LIVEBOARD" with the actual logo mark swapped in for the "O". - Copy was fact-checked against the codebase, not assumed: dropped an unverified "MIT licensed" claim (no LICENSE file exists), dropped "hosted platform" framing (production deploy step is unchecked in the project's own checklist), and traced the self-hosting/RLS claim through docker-compose.yml, auth.ts, core/config.py, and the RLS migration before keeping it. - Removed every old landing component in favor of freshly written ones (no reuse of the prior rounded/glow-heavy design). Auth (auth.ts, app/auth/signin/page.tsx): - Fixed a real data-loss bug: authorize() was defaulting a blank display name to the email prefix before it ever reached the backend, which defeated the backend's COALESCE-based name preservation on repeat sign-ins — every sign-in silently overwrote the user's display name. Blank now passes through as null. - Added DISABLE_DEV_LOGIN as a second, explicit kill-switch for the dev-login credentials bypass, independent of NODE_ENV. - Restyled the sign-in page to match the new theme; fixed "Welcome back" copy that presumed an existing account (there is no separate sign-up step — first sign-in provisions the account). --- app/auth/signin/page.tsx | 176 +++++++++--------- app/page.tsx | 87 ++------- auth.ts | 18 +- components/landing/animated-counter.tsx | 36 ++++ components/landing/browser-frame.tsx | 46 ----- components/landing/button-styles.ts | 21 +++ components/landing/channel-marks.tsx | 35 ++++ components/landing/code-snippet.tsx | 63 ------- components/landing/count-up.tsx | 36 ---- components/landing/cta-section.tsx | 79 +++++--- components/landing/feature-grid.tsx | 216 ++++++---------------- components/landing/footer.tsx | 80 ++++---- components/landing/github-icon.tsx | 7 - components/landing/hero.tsx | 148 +++++---------- components/landing/how-it-works.tsx | 72 -------- components/landing/incident-section.tsx | 51 +++++ components/landing/install-snippet.tsx | 117 ++++++++++++ components/landing/integrations-strip.tsx | 33 ---- components/landing/marquee-strip.tsx | 42 +++++ components/landing/navbar.tsx | 184 +++++++++--------- components/landing/network-graph.tsx | 66 ------- components/landing/radar-field.tsx | 65 ------- components/landing/reveal-on-scroll.tsx | 28 +++ components/landing/reveal.tsx | 24 --- components/landing/screenshot-frame.tsx | 24 +++ components/landing/scroll-indicator.tsx | 15 ++ components/landing/scroll-progress.tsx | 15 -- components/landing/showcase-image.tsx | 46 ----- components/landing/showcase.tsx | 53 ------ components/landing/spotlight-card.tsx | 41 ---- components/landing/stats-section.tsx | 46 +++++ components/landing/status-section.tsx | 28 +++ components/landing/tech-logos.tsx | 12 -- components/landing/tech-marks.tsx | 31 ++++ 34 files changed, 878 insertions(+), 1163 deletions(-) create mode 100644 components/landing/animated-counter.tsx delete mode 100644 components/landing/browser-frame.tsx create mode 100644 components/landing/button-styles.ts create mode 100644 components/landing/channel-marks.tsx delete mode 100644 components/landing/code-snippet.tsx delete mode 100644 components/landing/count-up.tsx delete mode 100644 components/landing/github-icon.tsx delete mode 100644 components/landing/how-it-works.tsx create mode 100644 components/landing/incident-section.tsx create mode 100644 components/landing/install-snippet.tsx delete mode 100644 components/landing/integrations-strip.tsx create mode 100644 components/landing/marquee-strip.tsx delete mode 100644 components/landing/network-graph.tsx delete mode 100644 components/landing/radar-field.tsx create mode 100644 components/landing/reveal-on-scroll.tsx delete mode 100644 components/landing/reveal.tsx create mode 100644 components/landing/screenshot-frame.tsx create mode 100644 components/landing/scroll-indicator.tsx delete mode 100644 components/landing/scroll-progress.tsx delete mode 100644 components/landing/showcase-image.tsx delete mode 100644 components/landing/showcase.tsx delete mode 100644 components/landing/spotlight-card.tsx create mode 100644 components/landing/stats-section.tsx create mode 100644 components/landing/status-section.tsx delete mode 100644 components/landing/tech-logos.tsx create mode 100644 components/landing/tech-marks.tsx diff --git a/app/auth/signin/page.tsx b/app/auth/signin/page.tsx index f189a50..d986d86 100644 --- a/app/auth/signin/page.tsx +++ b/app/auth/signin/page.tsx @@ -1,9 +1,12 @@ "use client"; +import Link from "next/link"; import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; import { Suspense, useState } from "react"; +import { AlertCircle, Terminal } from "lucide-react"; import { LiveboardIcon } from "@/components/logo"; +import { primaryButtonClass, secondaryButtonClass } from "@/components/landing/button-styles"; const hasGoogle = Boolean(process.env.NEXT_PUBLIC_GOOGLE_ENABLED) || @@ -37,102 +40,103 @@ function SignInForm() { } return ( -
+
- {/* Logo */} -
- - - Liveboard - -
- -
-
-

Welcome back

-

Sign in to your Liveboard workspace

+ + + Liveboard + + +
+
+

Sign in to Liveboard

+

New here? Signing in creates your workspace automatically.

{error && ( -
- {ERROR_MESSAGES[error] ?? "Authentication error. Please try again."} +
+ + {ERROR_MESSAGES[error] ?? "Authentication error. Please try again."}
)} - {/* Google button — shown when GOOGLE_CLIENT_ID is configured */} - {hasGoogle && ( - - )} - - {/* Dev login form — shown in development when Google isn't set up */} - {isDev && ( - <> - {hasGoogle && ( -
-
- dev login -
-
- )} -
- {!hasGoogle && ( -
-

- Dev mode.{" "} - Google OAuth not configured — sign in with any email below. - Set GOOGLE_CLIENT_ID to enable Google. -

+
+ {hasGoogle && ( + + )} + + {isDev && ( +
+ {hasGoogle && ( +
+
+ or +
)} -
- setEmail(e.target.value)} - required - className="w-full px-3 py-2 rounded border border-[#2A2A2A] bg-[#161616] text-sm text-[#F5F5F5] placeholder-[#333] outline-none focus:border-blue/50 transition-colors" - /> - setName(e.target.value)} - className="w-full px-3 py-2 rounded border border-[#2A2A2A] bg-[#161616] text-sm text-[#F5F5F5] placeholder-[#333] outline-none focus:border-blue/50 transition-colors" - /> + +
+ + Development sign-in
- - - - )} - {!hasGoogle && !isDev && ( -

- Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. -

- )} +
+
+ + setEmail(e.target.value)} + required + className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors placeholder:text-muted-dark focus:border-blue" + /> +
+
+ + setName(e.target.value)} + className="w-full border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-colors placeholder:text-muted-dark focus:border-blue" + /> +
+ +
+ +

+ No password needed in development. This form is hidden in production once Google OAuth is configured. +

+
+ )} + + {!hasGoogle && !isDev && ( +

+ Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. +

+ )} +
-

- By signing in you agree to our{" "} - Terms of Service - {" "}and{" "} - Privacy Policy. +

+ By signing in you agree to our Terms of Service and Privacy Policy.

- -

Open source API observability

); @@ -140,7 +144,13 @@ function SignInForm() { export default function SignInPage() { return ( - }> + +
+
+ } + >
); diff --git a/app/page.tsx b/app/page.tsx index 2d71cde..5f0f532 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,89 +1,32 @@ import { redirect } from "next/navigation"; import { auth } from "@/auth"; -import { LandingNavbar } from "@/components/landing/navbar"; -import { ScrollProgress } from "@/components/landing/scroll-progress"; +import { ScrollIndicator } from "@/components/landing/scroll-indicator"; +import { Navbar } from "@/components/landing/navbar"; import { Hero } from "@/components/landing/hero"; -import { IntegrationsStrip } from "@/components/landing/integrations-strip"; -import { Showcase } from "@/components/landing/showcase"; +import { MarqueeStrip } from "@/components/landing/marquee-strip"; import { FeatureGrid } from "@/components/landing/feature-grid"; -import { HowItWorks } from "@/components/landing/how-it-works"; +import { IncidentSection } from "@/components/landing/incident-section"; +import { StatusSection } from "@/components/landing/status-section"; +import { StatsSection } from "@/components/landing/stats-section"; import { CtaSection } from "@/components/landing/cta-section"; -import { LandingFooter } from "@/components/landing/footer"; +import { Footer } from "@/components/landing/footer"; export default async function Home() { const session = await auth(); if (session) redirect("/overview"); return ( -
- - +
+ + - - - - - - - - + - - - - + + + - +
); } diff --git a/auth.ts b/auth.ts index 2f30061..05e00d0 100644 --- a/auth.ts +++ b/auth.ts @@ -52,7 +52,14 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ ] : []), - ...(process.env.NODE_ENV !== "production" + // Gated on two independent conditions, not just NODE_ENV: this provider + // authenticates as whatever email is typed in, with zero proof of + // ownership. NODE_ENV=production already excludes it from the real + // Docker image (see Dockerfile.frontend), but that's one setting one + // deploy path away from being wrong. DISABLE_DEV_LOGIN is a second, + // explicit switch for any non-production environment (staging, a demo + // box) that still holds real user data and is reachable over a network. + ...(process.env.NODE_ENV !== "production" && process.env.DISABLE_DEV_LOGIN !== "true" ? [ Credentials({ id: "dev-login", @@ -64,10 +71,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ authorize(credentials) { if (!credentials?.email) return null; const email = credentials.email as string; + // Pass an empty field through as null rather than defaulting it + // to the email prefix here — the backend already falls back to + // the email prefix for brand-new users, and preserves the + // existing name (COALESCE) for returning ones. Resolving a + // default on the client would defeat that preservation on + // every subsequent sign-in. + const typedName = (credentials.name as string)?.trim(); return { id: email, email, - name: (credentials.name as string) || email.split("@")[0], + name: typedName || null, image: null, }; }, diff --git a/components/landing/animated-counter.tsx b/components/landing/animated-counter.tsx new file mode 100644 index 0000000..877f06d --- /dev/null +++ b/components/landing/animated-counter.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { animate, useInView, useReducedMotion } from "framer-motion"; + +interface AnimatedCounterProps { + target: number; + decimals?: number; + prefix?: string; + suffix?: string; +} + +export function AnimatedCounter({ target, decimals = 0, prefix = "", suffix = "" }: AnimatedCounterProps) { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-10% 0px -10% 0px" }); + const reduceMotion = useReducedMotion(); + const [display, setDisplay] = useState(reduceMotion ? target : 0); + + useEffect(() => { + if (!inView || reduceMotion) return; + const controls = animate(0, target, { + duration: 1.2, + ease: [0.19, 1, 0.22, 1], + onUpdate: (v) => setDisplay(v), + }); + return () => controls.stop(); + }, [inView, target, reduceMotion]); + + return ( + + {prefix} + {display.toFixed(decimals)} + {suffix} + + ); +} diff --git a/components/landing/browser-frame.tsx b/components/landing/browser-frame.tsx deleted file mode 100644 index 5bb0be6..0000000 --- a/components/landing/browser-frame.tsx +++ /dev/null @@ -1,46 +0,0 @@ -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 ( -
-
-
- - - -
-
- - - - - {url} -
- {live && ( -
- - Live -
- )} -
- {alt} -
- ); -} diff --git a/components/landing/button-styles.ts b/components/landing/button-styles.ts new file mode 100644 index 0000000..e5f6a37 --- /dev/null +++ b/components/landing/button-styles.ts @@ -0,0 +1,21 @@ +// Shared hover treatment for every button-shaped CTA on the landing page: +// a flat blue fill sweeps in from the left on hover (the same blue used in +// the Liveboard mark), text flips to white for contrast. Kept out of plain +// text links (nav items, footer links) — this is for buttons only. +const BASE = + "group relative isolate inline-flex items-center justify-center gap-1.5 overflow-hidden border font-medium transition-colors duration-300 before:absolute before:inset-0 before:-z-10 before:origin-left before:scale-x-0 before:bg-blue before:transition-transform before:duration-300 before:content-['']"; + +const SIZES = { + sm: "px-4 py-2 text-[13px]", + md: "px-5 py-2.5 text-sm", +} as const; + +type Size = keyof typeof SIZES; + +export function primaryButtonClass(size: Size = "md", extra = "") { + return `${BASE} ${SIZES[size]} border-foreground bg-foreground text-background hover:text-white hover:before:scale-x-100 ${extra}`; +} + +export function secondaryButtonClass(size: Size = "md", extra = "") { + return `${BASE} ${SIZES[size]} border-border text-foreground hover:border-blue hover:text-white hover:before:scale-x-100 ${extra}`; +} diff --git a/components/landing/channel-marks.tsx b/components/landing/channel-marks.tsx new file mode 100644 index 0000000..d39004d --- /dev/null +++ b/components/landing/channel-marks.tsx @@ -0,0 +1,35 @@ +import { Hash, Webhook } from "lucide-react"; + +interface ChannelMark { + key: string; + label: string; + slug?: string; +} + +// Discord and PagerDuty are real brand marks (Simple Icons CDN). Slack's mark +// was removed from Simple Icons, so it falls back to a generic glyph like +// "Webhook" — a delivery method, not a brand. +export const ALERT_CHANNELS: ChannelMark[] = [ + { key: "slack", label: "Slack" }, + { key: "discord", label: "Discord", slug: "discord" }, + { key: "pagerduty", label: "PagerDuty", slug: "pagerduty" }, + { key: "webhook", label: "Webhook" }, +]; + +export function ChannelMarkIcon({ channel, className }: { channel: ChannelMark; className?: string }) { + if (!channel.slug) { + const Icon = channel.key === "slack" ? Hash : Webhook; + return ; + } + return ( + // eslint-disable-next-line @next/next/no-img-element -- external brand mark, next/image needs SVG allow-listing for no benefit here + {channel.label} + ); +} diff --git a/components/landing/code-snippet.tsx b/components/landing/code-snippet.tsx deleted file mode 100644 index daa9ec0..0000000 --- a/components/landing/code-snippet.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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 ( - {children} - ); -} - -// ── Prebuilt, reused snippets ──────────────────────────────────────────────── - -export function InstallSnippet({ prompt = true }: { prompt?: boolean }) { - return ( - - {prompt && $ } - npm - install - liveboard-sdk - - ); -} - -export function MiddlewareSnippet() { - return ( - - app - .use - ( - liveboard - .middleware - ({"{ "} - apiKey - {" }"}) - ) - - ); -} - -export function DashboardUrlSnippet() { - return ( - - → - liveboard.dev/overview - - ); -} diff --git a/components/landing/count-up.tsx b/components/landing/count-up.tsx deleted file mode 100644 index 6515755..0000000 --- a/components/landing/count-up.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { animate, useInView } from "framer-motion"; - -interface CountUpProps { - to: number; - decimals?: number; - prefix?: string; - suffix?: string; - duration?: number; -} - -export function CountUp({ to, decimals = 0, prefix = "", suffix = "", duration = 1.6 }: CountUpProps) { - const ref = useRef(null); - const inView = useInView(ref, { once: true, margin: "-80px" }); - const [value, setValue] = useState(0); - - useEffect(() => { - if (!inView) return; - const controls = animate(0, to, { - duration, - ease: [0.22, 1, 0.36, 1], - onUpdate: (v) => setValue(v), - }); - return () => controls.stop(); - }, [inView, to, duration]); - - return ( - - {prefix} - {value.toFixed(decimals)} - {suffix} - - ); -} diff --git a/components/landing/cta-section.tsx b/components/landing/cta-section.tsx index b44295d..ed67848 100644 --- a/components/landing/cta-section.tsx +++ b/components/landing/cta-section.tsx @@ -1,41 +1,60 @@ 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"; +import { ArrowUpRight, Activity, Container } from "lucide-react"; +import { RevealOnScroll } from "@/components/landing/reveal-on-scroll"; export function CtaSection() { return ( -
- {/* subtle monochrome focus + faint radar echo */} -
-
-
-
-
- -

- Ship with confidence. +
+ +

+ One line of middleware. Zero blind spots.

-

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

+ Self-host the whole stack, or sign in and start right here.

-
- - + + + - + +
); } diff --git a/components/landing/feature-grid.tsx b/components/landing/feature-grid.tsx index 3d07015..65cc6aa 100644 --- a/components/landing/feature-grid.tsx +++ b/components/landing/feature-grid.tsx @@ -1,180 +1,72 @@ -import { Bell, Gauge, Sparkles, Users, Zap } from "lucide-react"; -import { Reveal } from "@/components/landing/reveal"; -import { SpotlightCard } from "@/components/landing/spotlight-card"; -import { InstallSnippet } from "@/components/landing/code-snippet"; - -const UPTIME_DAYS = Array.from({ length: 44 }, (_, i) => (i === 29 ? "yellow" : i === 30 ? "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" }, -]; - -function Eyebrow({ children }: { children: React.ReactNode }) { - return ( - - - {children} - - ); -} - -function cardShell(extra = "") { - return `h-full rounded-2xl hairline hairline-hover p-6 ${extra}`; -} +import { RevealOnScroll } from "@/components/landing/reveal-on-scroll"; +import { ScreenshotFrame } from "@/components/landing/screenshot-frame"; +import { ALERT_CHANNELS, ChannelMarkIcon } from "@/components/landing/channel-marks"; +import { InstallSnippet } from "@/components/landing/install-snippet"; export function FeatureGrid() { return ( -
- - Platform -

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

+ Every request, instrumented.

-

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

+ Distributed traces, per-endpoint health scores, and alerts routed to the tools your team already uses.

- +
-
- {/* Onboarding */} - - -
- -

60-second onboarding

-

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

-
- -
-
-
-
+ + + - {/* Endpoints / health */} - - -
- -

Per-endpoint health

-

- p50 / p95 / p99, error rate and a rolling health score for every route you serve. -

-
- {[ - { r: "/checkout", p: 92, c: "#22C55E" }, - { r: "/orders", p: 68, c: "#F59E0B" }, - { r: "/search", p: 34, c: "#EF4444" }, - ].map((e) => ( -
- {e.r} -
-
-
- {e.p} -
- ))} -
-
- - +
+ + +
+

Distributed tracing

+

+ Flame graphs and service maps for every request, down to the individual span. Trace IDs propagate across all five SDK adapters automatically. +

+
+
- {/* AI incident summaries — featured, tall */} - - -
-
- - - AI - -
-

AI incident summaries

-

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

+ + +
+

Endpoint health scores

+

+ Sort by error rate or p99 latency and catch a regression before it pages anyone.

- -
-
- - Critical - - 2m ago -
-

- p99 latency spike on /api/checkout -

-

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

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

User-centric observability

-

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

+

Alerts that reach your team

+

+ Threshold rules on error rate or latency, routed to the channel your on-call rotation actually watches.

-
- {TOP_USERS.map((u) => ( -
- - {u.id} - {u.reqs} req -
- ))} -
- - - - {/* Alerts + status page combined-ish */} - - -
- -

Alerts + status page

-

- Route anomalies to Slack, PagerDuty or a webhook — and ship a public status page for free. -

-
-
- 99.97% uptime - 44 days -
-
- {UPTIME_DAYS.map((color, i) => ( - - ))} +
+ {ALERT_CHANNELS.map((channel) => ( +
+ + {channel.label}
-
+ ))}
- - + +
); diff --git a/components/landing/footer.tsx b/components/landing/footer.tsx index 6c33896..63e1c5a 100644 --- a/components/landing/footer.tsx +++ b/components/landing/footer.tsx @@ -1,6 +1,5 @@ import Link from "next/link"; import { LiveboardIcon } from "@/components/logo"; -import { GithubIcon } from "@/components/landing/github-icon"; interface FooterLink { label: string; @@ -13,45 +12,38 @@ 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" }, + { label: "Anomaly detection", href: "#detection" }, + { 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" }, + { label: "JS SDK", href: "https://github.com/ryzrr/liveboard/tree/main/packages/sdk-js", external: true }, + { label: "Python SDK", href: "https://github.com/ryzrr/liveboard/tree/main/packages/sdk-python", external: true }, ], }, ]; -export function LandingFooter() { +export function Footer() { return ( -