+
+ Products
+
({
+ Link: ({ to, children, ...props }: { to: string; children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+}))
+
+describe('BuiltByVibes', () => {
+ it('renders the callout text', () => {
+ render(
)
+ expect(screen.getByText(/Built by/i)).toBeInTheDocument()
+ })
+
+ it('renders link to contact page', () => {
+ render(
)
+ const link = screen.getByRole('link', { name: /let's talk/i })
+ expect(link).toHaveAttribute('href', '/contact')
+ })
+})
diff --git a/src/components/products/BuiltByVibes/BuiltByVibes.tsx b/src/components/products/BuiltByVibes/BuiltByVibes.tsx
new file mode 100644
index 0000000..35523f9
--- /dev/null
+++ b/src/components/products/BuiltByVibes/BuiltByVibes.tsx
@@ -0,0 +1,24 @@
+import { Text } from '@/components/ui'
+import { cn } from '@/lib/cn'
+import { Link } from '@tanstack/react-router'
+
+interface BuiltByVibesProps {
+ className?: string
+}
+
+export function BuiltByVibes({ className }: BuiltByVibesProps) {
+ return (
+
+
+ Built by Vibes, the agentic
+ consulting studio.{' '}
+
+ Let's talk →
+
+
+
+ )
+}
diff --git a/src/components/products/BuiltByVibes/index.ts b/src/components/products/BuiltByVibes/index.ts
new file mode 100644
index 0000000..5a57c38
--- /dev/null
+++ b/src/components/products/BuiltByVibes/index.ts
@@ -0,0 +1 @@
+export { BuiltByVibes } from './BuiltByVibes'
diff --git a/src/components/products/CodeBlock/CodeBlock.test.tsx b/src/components/products/CodeBlock/CodeBlock.test.tsx
new file mode 100644
index 0000000..ffc59d0
--- /dev/null
+++ b/src/components/products/CodeBlock/CodeBlock.test.tsx
@@ -0,0 +1,36 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import { CodeBlock } from './CodeBlock'
+
+// Mock clipboard API
+const mockWriteText = vi.fn()
+Object.assign(navigator, {
+ clipboard: { writeText: mockWriteText },
+})
+
+describe('CodeBlock', () => {
+ it('renders the code content', () => {
+ render(
)
+ expect(screen.getByText('npm install vibes')).toBeInTheDocument()
+ })
+
+ it('copies code to clipboard when copy button is clicked', async () => {
+ mockWriteText.mockResolvedValueOnce(undefined)
+ render(
)
+
+ const copyButton = screen.getByRole('button', { name: /copy/i })
+ fireEvent.click(copyButton)
+
+ expect(mockWriteText).toHaveBeenCalledWith('curl example.com')
+ })
+
+ it('shows copied feedback after clicking', async () => {
+ mockWriteText.mockResolvedValueOnce(undefined)
+ render(
)
+
+ const copyButton = screen.getByRole('button', { name: /copy/i })
+ fireEvent.click(copyButton)
+
+ expect(await screen.findByText(/copied/i)).toBeInTheDocument()
+ })
+})
diff --git a/src/components/products/CodeBlock/CodeBlock.tsx b/src/components/products/CodeBlock/CodeBlock.tsx
new file mode 100644
index 0000000..8071698
--- /dev/null
+++ b/src/components/products/CodeBlock/CodeBlock.tsx
@@ -0,0 +1,41 @@
+import { cn } from '@/lib/cn'
+import { useState } from 'react'
+
+interface CodeBlockProps {
+ code: string
+ className?: string
+}
+
+export function CodeBlock({ code, className }: CodeBlockProps) {
+ const [copied, setCopied] = useState(false)
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(code)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch {
+ // Clipboard API may fail in non-HTTPS contexts or without permissions
+ console.error('Failed to copy to clipboard')
+ }
+ }
+
+ return (
+
+ {code}
+
+
+ )
+}
diff --git a/src/components/products/CodeBlock/index.ts b/src/components/products/CodeBlock/index.ts
new file mode 100644
index 0000000..9f4055e
--- /dev/null
+++ b/src/components/products/CodeBlock/index.ts
@@ -0,0 +1 @@
+export { CodeBlock } from './CodeBlock'
diff --git a/src/components/products/FeatureGrid/FeatureGrid.test.tsx b/src/components/products/FeatureGrid/FeatureGrid.test.tsx
new file mode 100644
index 0000000..0d02c12
--- /dev/null
+++ b/src/components/products/FeatureGrid/FeatureGrid.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { FeatureGrid } from './FeatureGrid'
+
+const features = [
+ { title: 'Remote Access', description: 'Control from anywhere' },
+ { title: 'Plugin System', description: 'Extend with plugins' },
+]
+
+describe('FeatureGrid', () => {
+ it('renders all feature titles', () => {
+ render(
)
+ expect(screen.getByText('Remote Access')).toBeInTheDocument()
+ expect(screen.getByText('Plugin System')).toBeInTheDocument()
+ })
+
+ it('renders all feature descriptions', () => {
+ render(
)
+ expect(screen.getByText('Control from anywhere')).toBeInTheDocument()
+ expect(screen.getByText('Extend with plugins')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/products/FeatureGrid/FeatureGrid.tsx b/src/components/products/FeatureGrid/FeatureGrid.tsx
new file mode 100644
index 0000000..89fb05b
--- /dev/null
+++ b/src/components/products/FeatureGrid/FeatureGrid.tsx
@@ -0,0 +1,30 @@
+import { Heading, Text } from '@/components/ui'
+import { cn } from '@/lib/cn'
+
+export interface Feature {
+ title: string
+ description: string
+}
+
+interface FeatureGridProps {
+ features: Feature[]
+ columns?: 2 | 3
+ className?: string
+}
+
+export function FeatureGrid({ features, columns = 2, className }: FeatureGridProps) {
+ return (
+
+ {features.map((feature) => (
+
+
+ {feature.title}
+
+ {feature.description}
+
+ ))}
+
+ )
+}
diff --git a/src/components/products/FeatureGrid/index.ts b/src/components/products/FeatureGrid/index.ts
new file mode 100644
index 0000000..23ca2af
--- /dev/null
+++ b/src/components/products/FeatureGrid/index.ts
@@ -0,0 +1 @@
+export { FeatureGrid, type Feature } from './FeatureGrid'
diff --git a/src/components/products/ProductCard/ProductCard.test.tsx b/src/components/products/ProductCard/ProductCard.test.tsx
new file mode 100644
index 0000000..5741c8b
--- /dev/null
+++ b/src/components/products/ProductCard/ProductCard.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import { ProductCard } from './ProductCard'
+
+// Mock the Link component from TanStack Router
+vi.mock('@tanstack/react-router', () => ({
+ Link: ({ to, children, ...props }: { to: string; children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+}))
+
+describe('ProductCard', () => {
+ const props = {
+ name: 'Vibes',
+ tagline: 'Remote control for Claude Code',
+ status: 'available' as const,
+ features: ['Feature 1', 'Feature 2'],
+ href: '/products/vibes',
+ }
+
+ it('renders product name and tagline', () => {
+ render(
)
+ expect(screen.getByText('Vibes')).toBeInTheDocument()
+ expect(screen.getByText('Remote control for Claude Code')).toBeInTheDocument()
+ })
+
+ it('renders status badge', () => {
+ render(
)
+ expect(screen.getByText('Available')).toBeInTheDocument()
+ })
+
+ it('renders feature list', () => {
+ render(
)
+ expect(screen.getByText('Feature 1')).toBeInTheDocument()
+ expect(screen.getByText('Feature 2')).toBeInTheDocument()
+ })
+
+ it('renders link to product page', () => {
+ render(
)
+ expect(screen.getByRole('link', { name: /learn more/i })).toHaveAttribute(
+ 'href',
+ '/products/vibes',
+ )
+ })
+})
diff --git a/src/components/products/ProductCard/ProductCard.tsx b/src/components/products/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000..bb6aaca
--- /dev/null
+++ b/src/components/products/ProductCard/ProductCard.tsx
@@ -0,0 +1,59 @@
+import { Card, CardContent, Heading, Text } from '@/components/ui'
+import { cn } from '@/lib/cn'
+import { Link } from '@tanstack/react-router'
+import { type ProductStatus, StatusBadge } from '../StatusBadge'
+
+interface ProductCardProps {
+ name: string
+ tagline: string
+ status: ProductStatus
+ features: string[]
+ href: string
+ image?: string
+ className?: string
+}
+
+export function ProductCard({
+ name,
+ tagline,
+ status,
+ features,
+ href,
+ image,
+ className,
+}: ProductCardProps) {
+ return (
+
+ {image && (
+
+

+
+ )}
+
+
+
+
+
+
+ {name}
+
+ {tagline}
+
+
+ {features.map((feature) => (
+ -
+ •
+ {feature}
+
+ ))}
+
+
+ Learn More →
+
+
+
+ )
+}
diff --git a/src/components/products/ProductCard/index.ts b/src/components/products/ProductCard/index.ts
new file mode 100644
index 0000000..d76f29d
--- /dev/null
+++ b/src/components/products/ProductCard/index.ts
@@ -0,0 +1 @@
+export { ProductCard } from './ProductCard'
diff --git a/src/components/products/StatusBadge/StatusBadge.test.tsx b/src/components/products/StatusBadge/StatusBadge.test.tsx
new file mode 100644
index 0000000..c1d1ef6
--- /dev/null
+++ b/src/components/products/StatusBadge/StatusBadge.test.tsx
@@ -0,0 +1,19 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { StatusBadge } from './StatusBadge'
+
+describe('StatusBadge', () => {
+ it('renders available status with green styling', () => {
+ render(
)
+ const badge = screen.getByText('Available')
+ expect(badge).toBeInTheDocument()
+ expect(badge).toHaveClass('bg-green-500/10')
+ })
+
+ it('renders coming-soon status with amber styling', () => {
+ render(
)
+ const badge = screen.getByText('Coming Soon')
+ expect(badge).toBeInTheDocument()
+ expect(badge).toHaveClass('bg-amber-500/10')
+ })
+})
diff --git a/src/components/products/StatusBadge/StatusBadge.tsx b/src/components/products/StatusBadge/StatusBadge.tsx
new file mode 100644
index 0000000..960004b
--- /dev/null
+++ b/src/components/products/StatusBadge/StatusBadge.tsx
@@ -0,0 +1,33 @@
+import { cn } from '@/lib/cn'
+import { type VariantProps, cva } from 'class-variance-authority'
+
+const badgeVariants = cva(
+ 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium font-heading uppercase tracking-wide',
+ {
+ variants: {
+ status: {
+ available: 'bg-green-500/10 text-green-400 border border-green-500/20',
+ 'coming-soon': 'bg-amber-500/10 text-amber-400 border border-amber-500/20',
+ },
+ },
+ defaultVariants: {
+ status: 'available',
+ },
+ },
+)
+
+export type ProductStatus = 'available' | 'coming-soon'
+
+interface StatusBadgeProps extends VariantProps
{
+ status: ProductStatus
+ className?: string
+}
+
+const statusLabels: Record = {
+ available: 'Available',
+ 'coming-soon': 'Coming Soon',
+}
+
+export function StatusBadge({ status, className }: StatusBadgeProps) {
+ return {statusLabels[status]}
+}
diff --git a/src/components/products/StatusBadge/index.ts b/src/components/products/StatusBadge/index.ts
new file mode 100644
index 0000000..53e8335
--- /dev/null
+++ b/src/components/products/StatusBadge/index.ts
@@ -0,0 +1 @@
+export { StatusBadge, type ProductStatus } from './StatusBadge'
diff --git a/src/components/products/WaitlistForm/WaitlistForm.test.tsx b/src/components/products/WaitlistForm/WaitlistForm.test.tsx
new file mode 100644
index 0000000..79e6ad1
--- /dev/null
+++ b/src/components/products/WaitlistForm/WaitlistForm.test.tsx
@@ -0,0 +1,61 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { WaitlistForm } from './WaitlistForm'
+
+const mockFetch = vi.fn()
+global.fetch = mockFetch
+
+describe('WaitlistForm', () => {
+ beforeEach(() => {
+ mockFetch.mockReset()
+ })
+
+ it('renders email input and submit button', () => {
+ render()
+ expect(screen.getByPlaceholderText(/email/i)).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /get early access/i })).toBeInTheDocument()
+ })
+
+ it('submits email to waitlist API', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ success: true }),
+ })
+
+ render()
+
+ const input = screen.getByPlaceholderText(/email/i)
+ const button = screen.getByRole('button', { name: /get early access/i })
+
+ fireEvent.change(input, { target: { value: 'test@example.com' } })
+ fireEvent.click(button)
+
+ await waitFor(() => {
+ expect(mockFetch).toHaveBeenCalledWith(
+ expect.stringContaining('/waitlist'),
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify({ email: 'test@example.com', product: 'volt' }),
+ }),
+ )
+ })
+ })
+
+ it('shows success message after submission', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ success: true }),
+ })
+
+ render()
+
+ fireEvent.change(screen.getByPlaceholderText(/email/i), {
+ target: { value: 'test@example.com' },
+ })
+ fireEvent.click(screen.getByRole('button'))
+
+ await waitFor(() => {
+ expect(screen.getByText(/you're on the list/i)).toBeInTheDocument()
+ })
+ })
+})
diff --git a/src/components/products/WaitlistForm/WaitlistForm.tsx b/src/components/products/WaitlistForm/WaitlistForm.tsx
new file mode 100644
index 0000000..e29b07d
--- /dev/null
+++ b/src/components/products/WaitlistForm/WaitlistForm.tsx
@@ -0,0 +1,73 @@
+import { Button, Input, Text } from '@/components/ui'
+import { cn } from '@/lib/cn'
+import { useState } from 'react'
+
+interface WaitlistFormProps {
+ product: string
+ className?: string
+}
+
+export function WaitlistForm({ product, className }: WaitlistFormProps) {
+ const [email, setEmail] = useState('')
+ const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
+ const [errorMessage, setErrorMessage] = useState('')
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setStatus('loading')
+ setErrorMessage('')
+
+ try {
+ const apiUrl = import.meta.env.VITE_CHAT_API_URL || ''
+ const response = await fetch(`${apiUrl}/waitlist`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, product }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok || !data.success) {
+ throw new Error(data.error || 'Failed to join waitlist')
+ }
+
+ setStatus('success')
+ } catch (err) {
+ setStatus('error')
+ setErrorMessage(err instanceof Error ? err.message : 'Something went wrong')
+ }
+ }
+
+ if (status === 'success') {
+ return (
+
+
+ You're on the list! We'll notify you when {product === 'volt' ? 'Volt' : 'Vibes'}{' '}
+ launches.
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/products/WaitlistForm/index.ts b/src/components/products/WaitlistForm/index.ts
new file mode 100644
index 0000000..ec86616
--- /dev/null
+++ b/src/components/products/WaitlistForm/index.ts
@@ -0,0 +1 @@
+export { WaitlistForm } from './WaitlistForm'
diff --git a/src/components/products/index.ts b/src/components/products/index.ts
new file mode 100644
index 0000000..1556655
--- /dev/null
+++ b/src/components/products/index.ts
@@ -0,0 +1,6 @@
+export { StatusBadge, type ProductStatus } from './StatusBadge'
+export { CodeBlock } from './CodeBlock'
+export { FeatureGrid, type Feature } from './FeatureGrid'
+export { BuiltByVibes } from './BuiltByVibes'
+export { WaitlistForm } from './WaitlistForm'
+export { ProductCard } from './ProductCard'
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index d3f314f..df85b20 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -13,6 +13,9 @@ import { Route as ServicesRouteImport } from './routes/services'
import { Route as ContactRouteImport } from './routes/contact'
import { Route as BrandRouteImport } from './routes/brand'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as ProductsIndexRouteImport } from './routes/products/index'
+import { Route as ProductsVoltRouteImport } from './routes/products/volt'
+import { Route as ProductsVibesRouteImport } from './routes/products/vibes'
const ServicesRoute = ServicesRouteImport.update({
id: '/services',
@@ -34,18 +37,39 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const ProductsIndexRoute = ProductsIndexRouteImport.update({
+ id: '/products/',
+ path: '/products/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ProductsVoltRoute = ProductsVoltRouteImport.update({
+ id: '/products/volt',
+ path: '/products/volt',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const ProductsVibesRoute = ProductsVibesRouteImport.update({
+ id: '/products/vibes',
+ path: '/products/vibes',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/brand': typeof BrandRoute
'/contact': typeof ContactRoute
'/services': typeof ServicesRoute
+ '/products/vibes': typeof ProductsVibesRoute
+ '/products/volt': typeof ProductsVoltRoute
+ '/products': typeof ProductsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/brand': typeof BrandRoute
'/contact': typeof ContactRoute
'/services': typeof ServicesRoute
+ '/products/vibes': typeof ProductsVibesRoute
+ '/products/volt': typeof ProductsVoltRoute
+ '/products': typeof ProductsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
@@ -53,13 +77,38 @@ export interface FileRoutesById {
'/brand': typeof BrandRoute
'/contact': typeof ContactRoute
'/services': typeof ServicesRoute
+ '/products/vibes': typeof ProductsVibesRoute
+ '/products/volt': typeof ProductsVoltRoute
+ '/products/': typeof ProductsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/brand' | '/contact' | '/services'
+ fullPaths:
+ | '/'
+ | '/brand'
+ | '/contact'
+ | '/services'
+ | '/products/vibes'
+ | '/products/volt'
+ | '/products'
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/brand' | '/contact' | '/services'
- id: '__root__' | '/' | '/brand' | '/contact' | '/services'
+ to:
+ | '/'
+ | '/brand'
+ | '/contact'
+ | '/services'
+ | '/products/vibes'
+ | '/products/volt'
+ | '/products'
+ id:
+ | '__root__'
+ | '/'
+ | '/brand'
+ | '/contact'
+ | '/services'
+ | '/products/vibes'
+ | '/products/volt'
+ | '/products/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -67,6 +116,9 @@ export interface RootRouteChildren {
BrandRoute: typeof BrandRoute
ContactRoute: typeof ContactRoute
ServicesRoute: typeof ServicesRoute
+ ProductsVibesRoute: typeof ProductsVibesRoute
+ ProductsVoltRoute: typeof ProductsVoltRoute
+ ProductsIndexRoute: typeof ProductsIndexRoute
}
declare module '@tanstack/react-router' {
@@ -99,6 +151,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/products/': {
+ id: '/products/'
+ path: '/products'
+ fullPath: '/products'
+ preLoaderRoute: typeof ProductsIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/products/volt': {
+ id: '/products/volt'
+ path: '/products/volt'
+ fullPath: '/products/volt'
+ preLoaderRoute: typeof ProductsVoltRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/products/vibes': {
+ id: '/products/vibes'
+ path: '/products/vibes'
+ fullPath: '/products/vibes'
+ preLoaderRoute: typeof ProductsVibesRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
@@ -107,6 +180,9 @@ const rootRouteChildren: RootRouteChildren = {
BrandRoute: BrandRoute,
ContactRoute: ContactRoute,
ServicesRoute: ServicesRoute,
+ ProductsVibesRoute: ProductsVibesRoute,
+ ProductsVoltRoute: ProductsVoltRoute,
+ ProductsIndexRoute: ProductsIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/routes/products/index.tsx b/src/routes/products/index.tsx
new file mode 100644
index 0000000..616ad9e
--- /dev/null
+++ b/src/routes/products/index.tsx
@@ -0,0 +1,63 @@
+import { ProductCard } from '@/components/products'
+import { Container } from '@/components/ui/Container'
+import { Section } from '@/components/ui/Section'
+import { Heading, Text } from '@/components/ui/Typography'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/products/')({
+ component: ProductsPage,
+})
+
+const products = [
+ {
+ name: 'Vibes',
+ tagline: 'Remote control for your Claude Code sessions',
+ status: 'available' as const,
+ features: [
+ 'Control sessions from any device',
+ 'Native Rust plugin system',
+ 'Real-time session mirroring',
+ ],
+ href: '/products/vibes',
+ image: '/images/products/vibes-terminal.svg',
+ },
+ {
+ name: 'Volt',
+ tagline: 'Volatility analysis & trade execution',
+ status: 'coming-soon' as const,
+ features: [
+ 'IV surfaces and Greeks analytics',
+ '11 options strategies built-in',
+ 'Backtest with synthetic or real data',
+ ],
+ href: '/products/volt',
+ image: '/images/products/volt-dashboard.svg',
+ },
+]
+
+function ProductsPage() {
+ return (
+ <>
+
+
+
+ What We're Building
+
+
+ Open source tools and platforms from the Vibes studio.
+
+
+
+
+
+
+
+ {products.map((product) => (
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/src/routes/products/vibes.tsx b/src/routes/products/vibes.tsx
new file mode 100644
index 0000000..9e7572d
--- /dev/null
+++ b/src/routes/products/vibes.tsx
@@ -0,0 +1,131 @@
+import { BuiltByVibes, CodeBlock, FeatureGrid } from '@/components/products'
+import { Button, Container, Heading, Section, Text } from '@/components/ui'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/products/vibes')({
+ component: VibesPage,
+})
+
+const features = [
+ {
+ title: 'Remote Access',
+ description: 'Control Claude Code sessions from your phone, tablet, or any device via web UI.',
+ },
+ {
+ title: 'Session Mirroring',
+ description: 'Real-time sync between your terminal and remote devices.',
+ },
+ {
+ title: 'Plugin System',
+ description: 'Extend vibes with native Rust plugins for custom commands and workflows.',
+ },
+ {
+ title: 'Cross-Platform',
+ description: 'Single binary for Linux, macOS, and Windows.',
+ },
+]
+
+const steps = [
+ { step: '1', title: 'Install', description: 'Run the install command' },
+ { step: '2', title: 'Start', description: 'Run vibes claude with your prompt' },
+ { step: '3', title: 'Access', description: 'Open the web UI from any device' },
+]
+
+function VibesPage() {
+ return (
+ <>
+ {/* Hero */}
+
+
+
+ Vibes
+
+
+ Remote control for your Claude Code sessions
+
+
+ Wrap Claude Code with remote access, session management, and a plugin ecosystem —
+ control your AI coding sessions from anywhere.
+
+
+
+
+
+
+
+
+
+
+ {/* Screenshot */}
+
+
+
+

+
+
+
+
+ {/* Features */}
+
+
+ {/* How It Works */}
+
+
+
+ How It Works
+
+
+ {steps.map((item) => (
+
+
+ {item.step}
+
+
+ {item.title}
+
+
{item.description}
+
+ ))}
+
+
+
+
+ {/* CTA */}
+
+ >
+ )
+}
diff --git a/src/routes/products/volt.tsx b/src/routes/products/volt.tsx
new file mode 100644
index 0000000..20b6203
--- /dev/null
+++ b/src/routes/products/volt.tsx
@@ -0,0 +1,68 @@
+import { BuiltByVibes, StatusBadge, WaitlistForm } from '@/components/products'
+import { Container, Heading, Section, Text } from '@/components/ui'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/products/volt')({
+ component: VoltPage,
+})
+
+const screenshots = [
+ { src: '/images/products/volt-dashboard.svg', caption: 'IV Surface Visualization' },
+]
+
+function VoltPage() {
+ return (
+ <>
+ {/* Hero with atmospheric background */}
+
+
+
+
+
+ Volt
+
+
+ Volatility analysis, simulation & trade execution
+
+
+ A comprehensive platform for traders who want to analyze options volatility, backtest
+ strategies, and execute with confidence.
+
+
+
+
+
+
+ {/* Screenshot Gallery */}
+
+
+
+ Preview
+
+
+ {screenshots.map((screenshot) => (
+
+
+

+
+
+ {screenshot.caption}
+
+
+ ))}
+
+
+
+
+ {/* CTA */}
+
+ >
+ )
+}
diff --git a/workers/chat-api/migrations/0003_waitlist.sql b/workers/chat-api/migrations/0003_waitlist.sql
new file mode 100644
index 0000000..89a251b
--- /dev/null
+++ b/workers/chat-api/migrations/0003_waitlist.sql
@@ -0,0 +1,13 @@
+-- 0003_waitlist.sql
+CREATE TABLE waitlist (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ email TEXT NOT NULL,
+ product TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ referrer TEXT,
+ user_agent TEXT,
+ UNIQUE(email, product)
+);
+
+CREATE INDEX idx_waitlist_product ON waitlist(product);
+CREATE INDEX idx_waitlist_created ON waitlist(created_at);
diff --git a/workers/chat-api/src/index.ts b/workers/chat-api/src/index.ts
index 8c6886a..62be755 100644
--- a/workers/chat-api/src/index.ts
+++ b/workers/chat-api/src/index.ts
@@ -11,6 +11,7 @@ import {
saveMessage,
} from './session'
import type { ChatRequest, ChatResponse, Env, InterviewAnswers, LeadTierValue } from './types'
+import { addToWaitlist } from './waitlist'
// In-memory store for interview answers per session.
// Limited to prevent unbounded growth in long-running isolates.
@@ -227,6 +228,30 @@ export default {
}
}
+ if (url.pathname === '/waitlist' && request.method === 'POST') {
+ try {
+ const body = (await request.json()) as { email: string; product: string }
+ const referrer = request.headers.get('Referer') ?? undefined
+ const userAgent = request.headers.get('User-Agent') ?? undefined
+
+ const result = await addToWaitlist(env.DB, {
+ email: body.email,
+ product: body.product,
+ referrer,
+ userAgent,
+ })
+
+ if (!result.success) {
+ return jsonResponse({ error: result.error }, 400, origin)
+ }
+
+ return jsonResponse({ success: true }, 200, origin)
+ } catch (err) {
+ console.error('Waitlist error:', err)
+ return jsonResponse({ error: 'Invalid request' }, 400, origin)
+ }
+ }
+
return new Response('Not found', { status: 404 })
},
}
diff --git a/workers/chat-api/src/waitlist.ts b/workers/chat-api/src/waitlist.ts
new file mode 100644
index 0000000..abafc89
--- /dev/null
+++ b/workers/chat-api/src/waitlist.ts
@@ -0,0 +1,52 @@
+import type { D1Database } from '@cloudflare/workers-types'
+
+export interface WaitlistEntry {
+ email: string
+ product: string
+ referrer?: string
+ userAgent?: string
+}
+
+const VALID_PRODUCTS = new Set(['volt', 'vibes'])
+
+export function isValidEmail(email: string): boolean {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
+}
+
+export function isValidProduct(product: string): boolean {
+ return VALID_PRODUCTS.has(product)
+}
+
+export async function addToWaitlist(
+ db: D1Database,
+ entry: WaitlistEntry,
+): Promise<{ success: boolean; error?: string }> {
+ if (!isValidEmail(entry.email)) {
+ return { success: false, error: 'Invalid email format' }
+ }
+
+ if (!isValidProduct(entry.product)) {
+ return { success: false, error: 'Invalid product' }
+ }
+
+ try {
+ await db
+ .prepare(
+ `INSERT INTO waitlist (email, product, referrer, user_agent)
+ VALUES (?, ?, ?, ?)
+ ON CONFLICT (email, product) DO NOTHING`,
+ )
+ .bind(
+ entry.email.toLowerCase(),
+ entry.product,
+ entry.referrer ?? null,
+ entry.userAgent ?? null,
+ )
+ .run()
+
+ return { success: true }
+ } catch (err) {
+ console.error('Waitlist insert error:', err)
+ return { success: false, error: 'Failed to join waitlist' }
+ }
+}
diff --git a/workers/chat-api/wrangler.toml b/workers/chat-api/wrangler.toml
index d9680e4..cff9d56 100644
--- a/workers/chat-api/wrangler.toml
+++ b/workers/chat-api/wrangler.toml
@@ -1,6 +1,8 @@
name = "vibes-chat-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
+# Directory where Wrangler looks for D1 database migration files
+migrations_dir = "migrations"
# Production (default)
[vars]