commit 1c37a7b88a273b354c409f2f9771f92c9bb9cb6b Author: Rami Bitar Date: Sun Oct 4 01:37:09 2026 -0400 Initial TanStack Start port of nextjs-startup Co-Authored-By: Claude Opus 5.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3afb834 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules/* +dist +.output +.nitro +.vercel +.yarn/install-state.gz +.env*.local +src/routeTree.gen.ts +*.tsbuildinfo + +.tanstack diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..dee2af2 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Vite AI Startup + +An AI startup landing page built with Vite, TanStack Start, Tailwind CSS v4 and shadcn/ui. +Ported from `nextjs-templates/nextjs-startup`. + +Runs in the nodesandbox runtime: `npm install` → `npm run dev` → port 3000. + +## Getting started + +```sh +corepack enable # uses the Yarn 4 pinned in package.json +yarn install +yarn dev # http://localhost:3000 +``` + +| Script | What it does | +| ---------------- | --------------------------------------------------------- | +| `yarn dev` | Vite dev server with SSR | +| `yarn build` | Production build; Nitro packages the server for the host | +| `yarn start` | Run the Node build from `.output/` | +| `yarn typecheck` | `tsc` | + +## Deploying to Vercel + +Push and deploy — no dashboard settings. `vercel.json` selects the TanStack +Start preset and installs with Yarn 4 via Corepack; Nitro writes +`.vercel/output/` during `vite build`. + +## nodesandbox compatibility + +The sandbox runs Vite's native tools as WebAssembly (`NAPI_RS_FORCE_WASI`), and +its installer resolves from `package.json` ranges rather than `yarn.lock`. The +toolchain versions match `vite-shopify-storefront`: + +- `vite` is pinned to **8.3.2** (rolldown 1.2.x WASI binding). +- WASI bindings (`@rolldown/binding-wasm32-wasi`, `@tailwindcss/oxide-wasm32-wasi`) + are **not** listed: the runtime installs each package's `*-wasm32-wasi` + optional dependency itself, and npm on other hosts rejects them. +- `@vitejs/plugin-react` (6.0.0) and Tailwind (4.3.1) match the harness samples. +- Nitro (the hosting adapter) only loads for `vite build`, never in `vite dev`. + +## Layout + +``` +src/ + router.tsx Router setup + routes/ File routes (__root.tsx is the document shell, index.tsx the page) + components/ Sections, layout and shadcn/ui components + lib/site.ts Site name, copy and links + styles.css Tailwind v4 theme +public/ Static assets served from / +``` diff --git a/components.json b/components.json new file mode 100644 index 0000000..09dc5ef --- /dev/null +++ b/components.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "remixicon", + "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": { + "@magicui": "https://magicui.design/r/{name}" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ffaeb45 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "vite-startup", + "type": "module", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite dev --host 0.0.0.0 --port 3000", + "build": "vite build", + "start": "node .output/server/index.mjs", + "typecheck": "tsc --noEmit" + }, + "packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e", + "dependencies": { + "@base-ui/react": "^1.6.0", + "@fontsource-variable/geist": "^5.3.0", + "@fontsource-variable/geist-mono": "^5.3.0", + "@remixicon/react": "^4.9.0", + "@shadcn/react": "^0.1.0", + "@tanstack/react-router": "^1.170.41", + "@tanstack/react-start": "^1.168.60", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "cobe": "^0.6.4", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "motion": "^13.3.0", + "next-themes": "^0.4.6", + "nitro": "3.0.260903-beta", + "radix-ui": "^1.6.0", + "react": "^19.3.0", + "react-day-picker": "^10.0.1", + "react-dom": "^19.3.0", + "react-is": "^19.3.0", + "react-resizable-panels": "^4.12.0", + "recharts": "3.8.0", + "shadcn": "^4.12.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + "vaul": "^1.1.2" + }, + "devDependencies": { + "@tailwindcss/vite": "4.3.1", + "@types/node": "^26.6.4", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/react-is": "^19", + "@vitejs/plugin-react": "6.0.0", + "tailwindcss": "4.3.1", + "typescript": "^5.7.3", + "vite": "8.3.2" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..c1daae0 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/og.png b/public/og.png new file mode 100644 index 0000000..66afe63 Binary files /dev/null and b/public/og.png differ diff --git a/src/components/layout/mobile-navigation.tsx b/src/components/layout/mobile-navigation.tsx new file mode 100644 index 0000000..ac1921e --- /dev/null +++ b/src/components/layout/mobile-navigation.tsx @@ -0,0 +1,115 @@ +import { useEffect, useRef, useState } from "react" +import { RiCloseLine, RiMenuLine } from "@remixicon/react" + +import { Brand } from "@/components/shared/brand" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" + +type NavigationItem = { + label: string + href: string + description: string +} + +export function MobileNavigation({ items }: { items: NavigationItem[] }) { + const [open, setOpen] = useState(false) + const navigating = useRef(false) + + useEffect(() => { + const desktop = window.matchMedia("(min-width: 768px)") + const closeOnDesktop = () => { + if (desktop.matches) setOpen(false) + } + + desktop.addEventListener("change", closeOnDesktop) + return () => desktop.removeEventListener("change", closeOnDesktop) + }, []) + + return ( + + + + + + { + // Let the anchor move focus to its section without the trigger + // interrupting the page's smooth scroll when the sheet closes. + if (navigating.current) event.preventDefault() + navigating.current = false + }} + > +
+ + + + +
+ +
+ + Explore Vertex + + + Explore our products, models, features, pricing, and developer documentation. + + +
+ +
+

One API. Every AI model.

+ + { navigating.current = true }} + className="flex h-12 items-center justify-center rounded-lg bg-blue-600 px-4 text-sm font-medium text-white shadow-[0_6px_24px_rgba(23,61,166,.2)] transition-colors hover:bg-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" + > + Get started + + +
+
+
+ ) +} diff --git a/src/components/layout/site-footer.tsx b/src/components/layout/site-footer.tsx new file mode 100644 index 0000000..03205b0 --- /dev/null +++ b/src/components/layout/site-footer.tsx @@ -0,0 +1,41 @@ +import { RiGithubFill, RiTwitterXFill } from "@remixicon/react" + +import { Brand } from "@/components/shared/brand" +import { Button } from "@/components/ui/button" + +export function SiteFooter() { + return ( + + ) +} diff --git a/src/components/layout/site-header.tsx b/src/components/layout/site-header.tsx new file mode 100644 index 0000000..11f9578 --- /dev/null +++ b/src/components/layout/site-header.tsx @@ -0,0 +1,49 @@ +import { RiArrowDownSLine, RiArrowRightSLine } from "@remixicon/react" + +import { MobileNavigation } from "@/components/layout/mobile-navigation" +import { ThemeToggle } from "@/components/layout/theme-toggle" +import { Brand } from "@/components/shared/brand" +import { FrameCrosses } from "@/components/shared/frame-crosses" + +const navigationItems = [ + { label: "Products", href: "#features", description: "One gateway for your AI stack" }, + { label: "Models", href: "#models", description: "Explore the models you can build with" }, + { label: "Features", href: "#features", description: "Routing, insights, and reliability" }, + { label: "Pricing", href: "#pricing", description: "Start free. Scale at your pace." }, + { label: "Docs", href: "#developers", description: "Your first request starts here" }, +] + +export function SiteHeader() { + return ( +
+
+ + + + + + +
+
+ ) +} diff --git a/src/components/layout/theme-provider.tsx b/src/components/layout/theme-provider.tsx new file mode 100644 index 0000000..376d207 --- /dev/null +++ b/src/components/layout/theme-provider.tsx @@ -0,0 +1,9 @@ +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return {children} +} diff --git a/src/components/layout/theme-toggle.tsx b/src/components/layout/theme-toggle.tsx new file mode 100644 index 0000000..392fbc4 --- /dev/null +++ b/src/components/layout/theme-toggle.tsx @@ -0,0 +1,31 @@ +import { RiMoonLine, RiSunLine } from "@remixicon/react" +import { useTheme } from "next-themes" +import { useSyncExternalStore } from "react" + +import { Button } from "@/components/ui/button" + +const emptySubscribe = () => () => {} + +export function ThemeToggle() { + const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false) + const { resolvedTheme, setTheme } = useTheme() + + const isDark = mounted && resolvedTheme === "dark" + + return ( + + ) +} diff --git a/src/components/route-states.tsx b/src/components/route-states.tsx new file mode 100644 index 0000000..f10e016 --- /dev/null +++ b/src/components/route-states.tsx @@ -0,0 +1,34 @@ +import { Link } from '@tanstack/react-router'; + +// Stand-ins for Next's built-in 404 and error pages. + +export function NotFound() { + return ( +
+
+
404
+
+
+ This page could not be found.{' '} + + Go home + +
+
+
+ ); +} + +export function ErrorMessage({ error }: { error: unknown }) { + return ( +
+
+
Something went wrong
+
+
+ {error instanceof Error ? error.message : String(error)} +
+
+
+ ); +} diff --git a/src/components/sections/cta-section.tsx b/src/components/sections/cta-section.tsx new file mode 100644 index 0000000..3d188e8 --- /dev/null +++ b/src/components/sections/cta-section.tsx @@ -0,0 +1,31 @@ +import { VertexMark } from "@/components/shared/brand" +import { FrameCrosses } from "@/components/shared/frame-crosses" +import { FlickeringGrid } from "@/components/ui/flickering-grid" + +export function CtaSection() { + return ( +
+ +
+ +
+ +

+ Build on every model. + Ship through one API. +

+

+ Your models will change. Your infrastructure doesn’t have to. +

+
+
+
+ ) +} diff --git a/src/components/sections/developers-section.tsx b/src/components/sections/developers-section.tsx new file mode 100644 index 0000000..6d38a80 --- /dev/null +++ b/src/components/sections/developers-section.tsx @@ -0,0 +1,41 @@ +import { RiCodeSSlashLine, RiSpeedUpLine, RiFlowChart } from "@remixicon/react" + +import { SectionHeading } from "@/components/shared/section-heading" +import { cn } from "@/lib/utils" + +export function DevelopersSection() { + return ( +
+ +
+ {[ + { + icon: RiCodeSSlashLine, + title: "Change one base URL", + text: "Keep your existing OpenAI-compatible SDK, payloads, and streaming logic.", + }, + { + icon: RiFlowChart, + title: "Set a routing policy", + text: "Choose a model directly or let Vertex optimize for cost, quality, or latency.", + }, + { + icon: RiSpeedUpLine, + title: "Observe every request", + text: "Trace performance and spend across models, applications, and environments.", + }, + ].map((step, index) => ( +
+ +

{step.title}

+

{step.text}

+
+ ))} +
+
+ ) +} diff --git a/src/components/sections/features-section.tsx b/src/components/sections/features-section.tsx new file mode 100644 index 0000000..d29244e --- /dev/null +++ b/src/components/sections/features-section.tsx @@ -0,0 +1,63 @@ +import { ApiTerminal } from "@/components/sections/features/api-terminal" +import { GlobalGlobe } from "@/components/sections/features/global-globe" +import { IntegrationOrbit } from "@/components/sections/features/integration-orbit" +import { RoutingActivity } from "@/components/sections/features/routing-activity" +import { UsageChart } from "@/components/sections/features/usage-chart" +import { FeatureCopy } from "@/components/shared/feature-copy" +import { SectionHeading } from "@/components/shared/section-heading" + +export function FeaturesSection() { + return ( +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ +
+ +
+ + +
+
+
+ ) +} diff --git a/src/components/sections/features/api-terminal.tsx b/src/components/sections/features/api-terminal.tsx new file mode 100644 index 0000000..6e85b56 --- /dev/null +++ b/src/components/sections/features/api-terminal.tsx @@ -0,0 +1,32 @@ +import { + AnimatedSpan, + Terminal, + TypingAnimation, +} from "@/components/ui/terminal" + +export function ApiTerminal() { + return ( +
+ + + {"> curl https://api.vertex.ai/v1/chat/completions \\"} + + + {` -d '{ "model": "vertex/auto", "messages": [...] }'`} + + + → evaluating 14 available models + + + → route selected: anthropic/claude-sonnet + + + ✓ response streamed in 342ms + + + One endpoint. Any model. No rewrites. + + +
+ ) +} diff --git a/src/components/sections/features/global-globe.tsx b/src/components/sections/features/global-globe.tsx new file mode 100644 index 0000000..4fb0f81 --- /dev/null +++ b/src/components/sections/features/global-globe.tsx @@ -0,0 +1,53 @@ +import { type COBEOptions } from "cobe" +import { useTheme } from "next-themes" + +import { Globe } from "@/components/ui/globe" + +const darkGlobeConfig: COBEOptions = { + width: 800, + height: 800, + onRender: () => {}, + devicePixelRatio: 2, + phi: 0, + theta: 0.28, + dark: 1, + diffuse: 1.1, + mapSamples: 12000, + mapBrightness: 4.5, + baseColor: [0.05, 0.12, 0.32] as [number, number, number], + markerColor: [0.15, 0.46, 1] as [number, number, number], + glowColor: [0.08, 0.18, 0.48] as [number, number, number], + markers: [ + { location: [37.7749, -122.4194] as [number, number], size: 0.08 }, + { location: [40.7128, -74.006] as [number, number], size: 0.06 }, + { location: [51.5072, -0.1276] as [number, number], size: 0.07 }, + { location: [48.8566, 2.3522] as [number, number], size: 0.05 }, + { location: [1.3521, 103.8198] as [number, number], size: 0.08 }, + { location: [35.6762, 139.6503] as [number, number], size: 0.06 }, + { location: [-33.8688, 151.2093] as [number, number], size: 0.05 }, + { location: [-23.5505, -46.6333] as [number, number], size: 0.06 }, + ], +} + +const lightGlobeConfig: COBEOptions = { + ...darkGlobeConfig, + dark: 0, + diffuse: 0.4, + mapBrightness: 1.2, + baseColor: [0.9, 0.94, 1], + markerColor: [0.09, 0.24, 0.65], + glowColor: [0.85, 0.9, 1], +} + +export function GlobalGlobe() { + const { resolvedTheme } = useTheme() + + return ( +
+ +
+ ) +} diff --git a/src/components/sections/features/integration-orbit.tsx b/src/components/sections/features/integration-orbit.tsx new file mode 100644 index 0000000..7644483 --- /dev/null +++ b/src/components/sections/features/integration-orbit.tsx @@ -0,0 +1,76 @@ +import { VertexMark } from "@/components/shared/brand" +import { OrbitingCircles } from "@/components/ui/orbiting-circles" + +const integrations = [ + { + name: "Supabase", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/supabase.svg", + }, + { + name: "Notion", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/notion.svg", + }, + { + name: "Figma", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/figma.svg", + }, + { + name: "Dropbox", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/dropbox.svg", + }, + { + name: "Google", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/google.svg", + }, + { + name: "OpenAI", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/openai.svg", + }, + { + name: "Stripe", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/stripe.svg", + }, + { + name: "GitHub", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/github.svg", + }, +] + +function IntegrationMark({ src, name }: { src: string; name: string }) { + return ( + + {`${name} + + ) +} + +export function IntegrationOrbit() { + return ( +
+ + + {integrations.slice(0, 4).map((integration) => ( + + ))} + + + {integrations.slice(4).map((integration) => ( + + ))} + + + ) +} diff --git a/src/components/sections/features/routing-activity.tsx b/src/components/sections/features/routing-activity.tsx new file mode 100644 index 0000000..c24be50 --- /dev/null +++ b/src/components/sections/features/routing-activity.tsx @@ -0,0 +1,58 @@ +import { RiPulseLine, RiCheckboxCircleLine, RiRouteLine, RiShieldCheckLine, RiFlashlightLine } from "@remixicon/react" + +import { AnimatedList } from "@/components/ui/animated-list" +import { cn } from "@/lib/utils" + +const routingEvents = [ + { + icon: , + title: "Routed to Claude Sonnet", + meta: "342ms · 8.2k tokens", + accent: "bg-blue-600/15 text-blue-600 dark:text-blue-400", + }, + { + icon: , + title: "Prompt cache hit", + meta: "Saved $0.018 · 42ms", + accent: "bg-amber-500/15 text-amber-500", + }, + { + icon: , + title: "Latency policy applied", + meta: "Gemini Flash · 118ms", + accent: "bg-blue-500/15 text-blue-500", + }, + { + icon: , + title: "Fallback completed", + meta: "OpenAI → Mistral · 621ms", + accent: "bg-emerald-500/15 text-emerald-500", + }, + { + icon: , + title: "Guardrail passed", + meta: "No sensitive data detected", + accent: "bg-cyan-500/15 text-cyan-500", + }, +] + +export function RoutingActivity() { + return ( +
+ + {routingEvents.map((event) => ( +
+ + {event.icon} + + + {event.title} + {event.meta} + +
+ ))} +
+
+
+ ) +} diff --git a/src/components/sections/features/usage-chart.tsx b/src/components/sections/features/usage-chart.tsx new file mode 100644 index 0000000..9bc315d --- /dev/null +++ b/src/components/sections/features/usage-chart.tsx @@ -0,0 +1,94 @@ +import { useId } from "react" +import { Area, AreaChart, XAxis, YAxis, usePlotArea, type DotProps } from "recharts" + +import { ChartContainer, ChartTooltip, type ChartConfig } from "@/components/ui/chart" + +const usageData = [ + { time: "08:00", requests: 2350 }, + { time: "10:00", requests: 2910 }, + { time: "12:00", requests: 2560 }, + { time: "14:00", requests: 3833 }, + { time: "16:00", requests: 3540 }, + { time: "18:00", requests: 4370 }, + { time: "20:00", requests: 5680 }, +] + +const chartConfig = { + requests: { label: "Requests", color: "var(--brand)" }, +} satisfies ChartConfig + +function UsageHighlight({ cx, cy, value }: DotProps & { value?: number | number[] }) { + const plot = usePlotArea() + const guideId = useId() + const amount = Array.isArray(value) ? value[value.length - 1] : value + + if (cx == null || cy == null || !plot || amount == null) return null + + const labelX = Math.max(plot.x + 8, Math.min(cx - 82, plot.x + plot.width - 84)) + const labelY = Math.max(8, cy - 46) + const formattedValue = amount.toLocaleString("en-US") + + return ( + + {formattedValue} requests + + + + + + + + + + + + + {formattedValue} + + + ) +} + +export function UsageChart() { + const fillId = useId() + + return ( + + + + + + + + + + + + null} /> + } + isAnimationActive={false} + /> + + + ) +} diff --git a/src/components/sections/hero-section.tsx b/src/components/sections/hero-section.tsx new file mode 100644 index 0000000..d1e433b --- /dev/null +++ b/src/components/sections/hero-section.tsx @@ -0,0 +1,41 @@ +import { RiArrowRightLine } from "@remixicon/react" + +import { FrameCrosses } from "@/components/shared/frame-crosses" + +export function HeroSection() { + return ( +
+ +
+ + The unified AI gateway + +

+ One API. + Every AI model. +

+

+ Route, optimize, and observe every model request through one OpenAI-compatible endpoint built for production. +

+ +
+ +
+ ) +} diff --git a/src/components/sections/logos-section.tsx b/src/components/sections/logos-section.tsx new file mode 100644 index 0000000..0033d59 --- /dev/null +++ b/src/components/sections/logos-section.tsx @@ -0,0 +1,90 @@ +import { FlickeringGrid } from "@/components/ui/flickering-grid" + +const customerLogos = [ + { + name: "Next.js", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/nextjs-logo.svg", + width: 394, + height: 80, + }, + { + name: "OpenAI", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/openai-logo.svg", + width: 512, + height: 126, + }, + { + name: "Stripe", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/stripe-logo.svg", + width: 73, + height: 31, + }, + { + name: "Vercel", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/vercel-logo.svg", + width: 2048, + height: 407, + }, + { + name: "Supabase", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/supabase-logo.svg", + width: 581, + height: 113, + }, + { + name: "Anthropic", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/anthropic-logo.svg", + width: 579, + height: 65, + }, + { + name: "Tailwind CSS", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/tailwindcss-logo.svg", + width: 665, + height: 82, + }, + { + name: "Linear", + src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/standard/linear-logo.svg", + width: 122, + height: 31, + }, +] + +export function LogosSection() { + return ( +
+
+ +
+

+ Built for + your AI stack. +

+
+
+ {customerLogos.map((logo) => ( +
+ {`${logo.name} +
+ ))} +
+
+ ) +} diff --git a/src/components/sections/pricing-section.tsx b/src/components/sections/pricing-section.tsx new file mode 100644 index 0000000..49755ac --- /dev/null +++ b/src/components/sections/pricing-section.tsx @@ -0,0 +1,106 @@ +import { useState } from "react" +import { RiCheckLine } from "@remixicon/react" + +import { SectionHeading } from "@/components/shared/section-heading" +import { cn } from "@/lib/utils" + +export function PricingSection() { + const [annual, setAnnual] = useState(false) + + return ( +
+ +
+
+ + +
+
+
+ {[ + { + name: "Developer", + price: "$0", + cadence: "/ month", + description: "For prototypes, side projects, and individual builders.", + action: "Start building", + popular: false, + features: ["Pay-as-you-go model usage", "All model providers", "Basic usage analytics", "Community support"], + }, + { + name: "Team", + price: annual ? "$39" : "$49", + cadence: "/ month", + description: "For teams running reliable AI products in production.", + action: "Start 14-day trial", + popular: true, + features: ["Everything in Developer", "Routing and fallback policies", "Team budgets and alerts", "90-day request analytics", "Priority support"], + }, + { + name: "Enterprise", + price: "Custom", + cadence: "", + description: "For organizations that need security and scale.", + action: "Talk to sales", + popular: false, + features: ["Everything in Team", "SAML SSO and audit logs", "Custom retention and regions", "Dedicated capacity", "Enterprise SLA"], + }, + ].map((plan) => ( +
+
+
+

{plan.name}

+ {plan.popular && ( + + Most popular + + )} +
+
+ {plan.price} + {plan.cadence && {plan.cadence}} +
+

{plan.description}

+ + {plan.action} + +
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ ))} +
+
+ ) +} diff --git a/src/components/sections/reliability-section.tsx b/src/components/sections/reliability-section.tsx new file mode 100644 index 0000000..19d8294 --- /dev/null +++ b/src/components/sections/reliability-section.tsx @@ -0,0 +1,35 @@ +import { RiLock2Line } from "@remixicon/react" + +import { Ripple } from "@/components/ui/ripple" +import { cn } from "@/lib/utils" + +export function ReliabilitySection() { + return ( +
+ +
+
+ +
+

+ Reliability built into every request. +

+

+ Production-grade controls keep your workloads available, observable, and protected as they scale. +

+
+ {[ + ["99.99%", "Gateway uptime"], + ["8", "Global regions"], + ["< 20ms", "Routing overhead"], + ].map(([value, label], index) => ( +
+ {value} + {label} +
+ ))} +
+
+
+ ) +} diff --git a/src/components/sections/testimonials-section.tsx b/src/components/sections/testimonials-section.tsx new file mode 100644 index 0000000..922f64a --- /dev/null +++ b/src/components/sections/testimonials-section.tsx @@ -0,0 +1,71 @@ +import { ReviewCard, type Testimonial } from "@/components/shared/review-card" +import { Marquee } from "@/components/ui/marquee" + +const reviews: Testimonial[] = [ + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/female-1.jpeg", + name: "Maya Kim", + role: "AI Platform Lead", + quote: + "Vertex gave us one reliable interface for every model experiment. We stopped rebuilding provider integrations and started shipping.", + }, + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/male-1.jpeg", + name: "Jon Bell", + role: "Founding Engineer", + quote: + "Fallback routing turned model outages into a non-event. Our application stays fast even when a provider is under pressure.", + }, + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/male-2.jpeg", + name: "Ari Rao", + role: "VP of Engineering", + quote: + "The unified cost view finally made inference spend understandable across teams, providers, and environments.", + }, + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/female-2.jpeg", + name: "Sofia Chen", + role: "Product Engineer", + quote: + "We changed one base URL and had access to the models we needed. The developer experience is exactly that simple.", + }, + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/male-3.jpeg", + name: "Owen Taylor", + role: "Infrastructure Director", + quote: + "Vertex lets our developers move quickly while the platform team keeps control of budgets, policies, and reliability.", + }, + { + avatar: "https://sb.frontend.co/storage/v1/object/public/assets/avatars/unsplash/female-3.jpeg", + name: "Nina Brooks", + role: "Applied AI Engineer", + quote: + "Model comparisons that used to take days now take minutes. The same payload works everywhere we want to test.", + }, +] + +export function TestimonialsSection() { + const firstReviews = reviews.slice(0, 3) + const secondReviews = reviews.slice(3) + + return ( +
+
+

Early access feedback

+

Built for teams shipping AI

+
+
+ + {firstReviews.map((review) => )} + + + {secondReviews.map((review) => )} + +
+
+
+
+ ) +} diff --git a/src/components/shared/brand.tsx b/src/components/shared/brand.tsx new file mode 100644 index 0000000..1b6f2d1 --- /dev/null +++ b/src/components/shared/brand.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils" + +export function VertexMark({ className }: { className?: string }) { + return ( + + ) +} + +export function Brand({ compact = false }: { compact?: boolean }) { + return ( + + + {!compact && ( + Vertex + )} + + ) +} diff --git a/src/components/shared/feature-copy.tsx b/src/components/shared/feature-copy.tsx new file mode 100644 index 0000000..5161998 --- /dev/null +++ b/src/components/shared/feature-copy.tsx @@ -0,0 +1,14 @@ +export function FeatureCopy({ + title, + description, +}: { + title: string + description: string +}) { + return ( +
+

{title}

+

{description}

+
+ ) +} diff --git a/src/components/shared/frame-crosses.tsx b/src/components/shared/frame-crosses.tsx new file mode 100644 index 0000000..d4bfe3d --- /dev/null +++ b/src/components/shared/frame-crosses.tsx @@ -0,0 +1,22 @@ +export function FrameCrosses({ + edges = "all", +}: { + edges?: "all" | "top" | "bottom" +}) { + return ( + <> + {edges !== "bottom" && ( + <> +