From 699ebbaa85cef665f4ba577e9b3a5f501bb2fac0 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 15:36:50 -0800 Subject: [PATCH 01/21] docs: add Products milestone to Phase 3 - Move Integrations to top of Phase 3 milestones - Add Products milestone with Vibes and Volt - Vibes: Remote control for Claude Code sessions - Volt: Volatility analysis & trade execution (in progress) --- docs/PROGRESS.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 5c433d4..4f0c283 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -160,10 +160,22 @@ Track the progress of phases, milestones, and tasks for the Vibes website. ## Phase 3: Content & Credibility -**Goal:** Industry pages, case studies, about page, newsletter integration. +**Goal:** Industry pages, case studies, about page, newsletter integration, and product showcases. ### Milestones +#### Integrations +| Task | Status | PR | +|------|--------|-----| +| Newsletter provider setup | ⬜ Not Started | — | +| Newsletter signup form | ⬜ Not Started | — | + +#### Products +| Product | Status | Description | +|---------|--------|-------------| +| [Vibes](https://github.com/run-vibes/vibes) | ⬜ Not Started | Remote control for your Claude Code sessions | +| Volt | 🔄 In Progress | Volatility analysis, simulation & trade execution system | + #### Industry Pages | Page | Status | PR | |------|--------|-----| @@ -187,12 +199,6 @@ Track the progress of phases, milestones, and tasks for the Vibes website. | About page | ⬜ Not Started | — | | Team section | ⬜ Not Started | — | -#### Integrations -| Task | Status | PR | -|------|--------|-----| -| Newsletter provider setup | ⬜ Not Started | — | -| Newsletter signup form | ⬜ Not Started | — | - --- ## Phase 4: Insights & Growth From 7ca09e5aa2b55a4315d14cbd182e52a9c1edecd7 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 15:52:13 -0800 Subject: [PATCH 02/21] docs: add products pages design plan (09) --- docs/plans/09-products/design.md | 249 +++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/plans/09-products/design.md diff --git a/docs/plans/09-products/design.md b/docs/plans/09-products/design.md new file mode 100644 index 0000000..adf266c --- /dev/null +++ b/docs/plans/09-products/design.md @@ -0,0 +1,249 @@ +# Products Pages Design + +## Overview + +Add product showcase pages to the Vibes website, serving dual purposes: +1. **Product marketing** — Treat Vibes and Volt as standalone products +2. **Lead generation** — Demonstrate Vibes studio's product development capabilities + +## Information Architecture + +### URL Structure + +``` +/products → Index page (both products) +/products/vibes → Full Vibes product page +/products/volt → Volt teaser page +``` + +### Navigation + +Add "Products" to main navbar between "Services" and "Contact". + +### Status Model + +```typescript +type ProductStatus = 'available' | 'coming-soon' +``` + +Drives badge styling, CTA text, and whether install commands appear. + +--- + +## Products Index Page (`/products`) + +### Hero Section + +- **Headline:** "What We're Building" +- **Subhead:** "Open source tools and platforms from the Vibes studio." + +### Product Cards + +2-column grid on desktop, stacked on mobile. Each card includes: + +- Screenshot thumbnail (16:9 ratio) +- Status badge ("Available" / "Coming Soon") +- Product name (large heading) +- One-liner tagline +- 3 feature bullets +- "Learn More →" CTA + +#### Vibes Card + +``` +[Screenshot: terminal with vibes claude command] +[Available badge] + +Vibes +Remote control for your Claude Code sessions + +• Control sessions from any device +• Native Rust plugin system +• Real-time session mirroring + +[Learn More →] +``` + +#### Volt Card + +``` +[Screenshot: blurred/cropped prototype dashboard] +[Coming Soon badge] + +Volt +Volatility analysis & trade execution + +• IV surfaces and Greeks analytics +• 11 options strategies built-in +• Backtest with synthetic or real data + +[Learn More →] +``` + +--- + +## Vibes Product Page (`/products/vibes`) + +### Hero Section + +- **Headline:** "Vibes" +- **Tagline:** "Remote control for your Claude Code sessions" +- **Description:** 1-2 sentences expanding on value prop +- **Primary CTA:** Copyable install command + ```bash + curl -sSf https://vibes.run/install | sh + ``` +- **Secondary CTA:** "Star on GitHub" with star count badge +- **Visual:** Terminal screenshot or animated demo + +### Features Grid (2x2) + +| Feature | Description | +|---------|-------------| +| Remote Access | Control sessions from your phone, tablet, or any device via web UI | +| Session Mirroring | Real-time sync between terminal and remote devices | +| Plugin System | Extend with native Rust plugins for custom workflows | +| Cross-Platform | Single binary for Linux, macOS, and Windows | + +### How It Works Section + +3-step visual flow: +1. Install vibes CLI +2. Run `vibes claude "your prompt"` +3. Access from any device at `localhost:7432` + +### Architecture Diagram + +Clean SVG recreation of the ASCII architecture from README. + +### Built by Vibes Callout + +Subtle banner: "Vibes is built by Vibes, the agentic consulting studio. Need custom AI tooling? [Let's talk →]" + +--- + +## Volt Teaser Page (`/products/volt`) + +### Atmospheric Background + +- Blurred, cropped screenshot of prototype dashboard +- Dark overlay gradient for text legibility +- Subtle animated grain or glow effect (matches site aesthetic) + +### Hero Section (centered, minimal) + +- **Status badge:** "Coming Soon" +- **Headline:** "Volt" +- **Tagline:** "Volatility analysis, simulation & trade execution" +- **Description:** "A comprehensive platform for traders who want to analyze options volatility, backtest strategies, and execute with confidence." +- No feature list — maintain mystery + +### Email Capture Form + +``` +[Email input: "Enter your email"] +[Button: "Get Early Access"] +``` + +- Success state: "You're on the list. We'll notify you when Volt launches." +- Stores to D1 `waitlist` table with `product: 'volt'` + +### Screenshot Gallery + +Below the fold, 2-3 static screenshots from HTML prototypes: +- "IV Surface Visualization" +- "Strategy Builder" +- "Backtest Results" + +Styled as floating cards with subtle shadows. + +### Built by Vibes Callout + +Same treatment as Vibes page. + +--- + +## Technical Implementation + +### New Routes + +``` +src/routes/products/index.tsx → Products index +src/routes/products/vibes.tsx → Vibes product page +src/routes/products/volt.tsx → Volt teaser page +``` + +### New Components + +``` +src/components/products/ProductCard.tsx → Card for index page +src/components/products/StatusBadge.tsx → "Available" / "Coming Soon" +src/components/products/FeatureGrid.tsx → 2x2 or 3-col feature display +src/components/products/WaitlistForm.tsx → Email capture for Volt +src/components/products/CodeBlock.tsx → Copyable install command +src/components/products/BuiltByVibes.tsx → Lead-gen callout banner +``` + +### Backend: Waitlist API + +**New endpoint:** `POST /api/waitlist` + +```typescript +// Request +{ email: string, product: string } + +// Response +{ success: true } | { error: string } +``` + +**Validation:** +- Email format validation +- Product must be in allowed list (`volt`, `vibes`) +- Rate limiting (reuse existing session-based approach) + +### Database: Waitlist Table + +```sql +-- 0003_waitlist.sql +CREATE TABLE waitlist ( + id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), + email TEXT NOT NULL, + product TEXT NOT NULL, -- 'volt' | 'vibes' | future products + created_at TEXT NOT NULL DEFAULT (datetime('now')), + + -- Optional context + referrer TEXT, -- Where they came from + user_agent TEXT, -- Browser info + + UNIQUE(email, product) -- Prevent duplicate signups per product +); + +CREATE INDEX idx_waitlist_product ON waitlist(product); +CREATE INDEX idx_waitlist_created ON waitlist(created_at); +``` + +--- + +## Assets Required + +### Vibes + +- Terminal screenshot showing `vibes claude` command +- Architecture diagram (SVG, recreated from ASCII) +- OG image for social sharing + +### Volt + +- 2-3 prototype screenshots (from HTML prototypes) +- Blurred hero background image +- OG image for social sharing + +--- + +## Lead Generation Tie-in + +Both product pages include a "Built by Vibes" callout: + +> "Vibes is built by Vibes, the agentic consulting studio. Need custom AI tooling? Let's talk →" + +Links to `/contact` to drive leads. From da14009bd389b6509417d41f1ce99595ebb3654f Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 15:57:07 -0800 Subject: [PATCH 03/21] docs: add products pages implementation plan --- docs/plans/09-products/implementation.md | 1601 ++++++++++++++++++++++ 1 file changed, 1601 insertions(+) create mode 100644 docs/plans/09-products/implementation.md diff --git a/docs/plans/09-products/implementation.md b/docs/plans/09-products/implementation.md new file mode 100644 index 0000000..ebcd429 --- /dev/null +++ b/docs/plans/09-products/implementation.md @@ -0,0 +1,1601 @@ +# Products Pages Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add product showcase pages for Vibes and Volt with waitlist email capture. + +**Architecture:** Three new routes (`/products`, `/products/vibes`, `/products/volt`) with shared product components. Waitlist API endpoint stores emails in D1. Placeholder assets for screenshots that can be swapped later. + +**Tech Stack:** TanStack Start, Tailwind CSS, class-variance-authority, Cloudflare Workers/D1 + +--- + +## Task 1: Waitlist Database Migration + +**Files:** +- Create: `workers/chat-api/migrations/0003_waitlist.sql` + +**Step 1: Write the migration** + +```sql +-- 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); +``` + +**Step 2: Run migration locally** + +```bash +just worker-migrate local +``` + +Expected: Migration applies successfully + +**Step 3: Commit** + +```bash +git add workers/chat-api/migrations/0003_waitlist.sql +git commit -m "feat: add waitlist table migration" +``` + +--- + +## Task 2: Waitlist API Endpoint + +**Files:** +- Modify: `workers/chat-api/src/index.ts` +- Create: `workers/chat-api/src/waitlist.ts` + +**Step 1: Create waitlist module** + +Create `workers/chat-api/src/waitlist.ts`: + +```typescript +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' } + } +} +``` + +**Step 2: Add route to worker** + +In `workers/chat-api/src/index.ts`, add after the `/chat` route (around line 228): + +```typescript +import { addToWaitlist } from './waitlist' + +// ... existing code ... + + 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) + } + } +``` + +**Step 3: Test locally** + +```bash +just worker-dev +# In another terminal: +curl -X POST http://localhost:8787/waitlist \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","product":"volt"}' +``` + +Expected: `{"success":true}` + +**Step 4: Commit** + +```bash +git add workers/chat-api/src/waitlist.ts workers/chat-api/src/index.ts +git commit -m "feat: add waitlist API endpoint" +``` + +--- + +## Task 3: StatusBadge Component + +**Files:** +- Create: `src/components/products/StatusBadge/StatusBadge.tsx` +- Create: `src/components/products/StatusBadge/StatusBadge.test.tsx` +- Create: `src/components/products/StatusBadge/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/StatusBadge/StatusBadge.test.tsx`: + +```tsx +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') + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/StatusBadge/StatusBadge.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/StatusBadge/StatusBadge.tsx`: + +```tsx +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]} +} +``` + +Create `src/components/products/StatusBadge/index.ts`: + +```typescript +export { StatusBadge, type ProductStatus } from './StatusBadge' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/StatusBadge/StatusBadge.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/StatusBadge/ +git commit -m "feat: add StatusBadge component" +``` + +--- + +## Task 4: CodeBlock Component + +**Files:** +- Create: `src/components/products/CodeBlock/CodeBlock.tsx` +- Create: `src/components/products/CodeBlock/CodeBlock.test.tsx` +- Create: `src/components/products/CodeBlock/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/CodeBlock/CodeBlock.test.tsx`: + +```tsx +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() + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/CodeBlock/CodeBlock.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/CodeBlock/CodeBlock.tsx`: + +```tsx +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 () => { + await navigator.clipboard.writeText(code) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+ {code} + +
+ ) +} +``` + +Create `src/components/products/CodeBlock/index.ts`: + +```typescript +export { CodeBlock } from './CodeBlock' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/CodeBlock/CodeBlock.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/CodeBlock/ +git commit -m "feat: add CodeBlock component with copy functionality" +``` + +--- + +## Task 5: FeatureGrid Component + +**Files:** +- Create: `src/components/products/FeatureGrid/FeatureGrid.tsx` +- Create: `src/components/products/FeatureGrid/FeatureGrid.test.tsx` +- Create: `src/components/products/FeatureGrid/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/FeatureGrid/FeatureGrid.test.tsx`: + +```tsx +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() + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/FeatureGrid/FeatureGrid.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/FeatureGrid/FeatureGrid.tsx`: + +```tsx +import { cn } from '@/lib/cn' +import { Heading, Text } from '@/components/ui' + +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} +
+ ))} +
+ ) +} +``` + +Create `src/components/products/FeatureGrid/index.ts`: + +```typescript +export { FeatureGrid, type Feature } from './FeatureGrid' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/FeatureGrid/FeatureGrid.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/FeatureGrid/ +git commit -m "feat: add FeatureGrid component" +``` + +--- + +## Task 6: BuiltByVibes Component + +**Files:** +- Create: `src/components/products/BuiltByVibes/BuiltByVibes.tsx` +- Create: `src/components/products/BuiltByVibes/BuiltByVibes.test.tsx` +- Create: `src/components/products/BuiltByVibes/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/BuiltByVibes/BuiltByVibes.test.tsx`: + +```tsx +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { createMemoryHistory, createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router' +import { BuiltByVibes } from './BuiltByVibes' + +function renderWithRouter(component: React.ReactNode) { + const rootRoute = createRootRoute({ component: () => component }) + const router = createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ initialEntries: ['/'] }), + }) + return render() +} + +describe('BuiltByVibes', () => { + it('renders the callout text', () => { + renderWithRouter() + expect(screen.getByText(/Built by Vibes/i)).toBeInTheDocument() + }) + + it('renders link to contact page', () => { + renderWithRouter() + const link = screen.getByRole('link', { name: /let's talk/i }) + expect(link).toHaveAttribute('href', '/contact') + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/BuiltByVibes/BuiltByVibes.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/BuiltByVibes/BuiltByVibes.tsx`: + +```tsx +import { cn } from '@/lib/cn' +import { Text } from '@/components/ui' +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 → + + +
+ ) +} +``` + +Create `src/components/products/BuiltByVibes/index.ts`: + +```typescript +export { BuiltByVibes } from './BuiltByVibes' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/BuiltByVibes/BuiltByVibes.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/BuiltByVibes/ +git commit -m "feat: add BuiltByVibes lead-gen callout component" +``` + +--- + +## Task 7: WaitlistForm Component + +**Files:** +- Create: `src/components/products/WaitlistForm/WaitlistForm.tsx` +- Create: `src/components/products/WaitlistForm/WaitlistForm.test.tsx` +- Create: `src/components/products/WaitlistForm/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/WaitlistForm/WaitlistForm.test.tsx`: + +```tsx +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { describe, expect, it, vi, beforeEach } 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() + }) + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/WaitlistForm/WaitlistForm.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/WaitlistForm/WaitlistForm.tsx`: + +```tsx +import { cn } from '@/lib/cn' +import { Button, Input, Text } from '@/components/ui' +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 ( +
+ setEmail(e.target.value)} + required + disabled={status === 'loading'} + className="flex-1" + /> + + {status === 'error' && ( + + {errorMessage} + + )} +
+ ) +} +``` + +Create `src/components/products/WaitlistForm/index.ts`: + +```typescript +export { WaitlistForm } from './WaitlistForm' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/WaitlistForm/WaitlistForm.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/WaitlistForm/ +git commit -m "feat: add WaitlistForm component for email capture" +``` + +--- + +## Task 8: ProductCard Component + +**Files:** +- Create: `src/components/products/ProductCard/ProductCard.tsx` +- Create: `src/components/products/ProductCard/ProductCard.test.tsx` +- Create: `src/components/products/ProductCard/index.ts` + +**Step 1: Write the failing test** + +Create `src/components/products/ProductCard/ProductCard.test.tsx`: + +```tsx +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { createMemoryHistory, createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router' +import { ProductCard } from './ProductCard' + +function renderWithRouter(component: React.ReactNode) { + const rootRoute = createRootRoute({ component: () => component }) + const router = createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ initialEntries: ['/'] }), + }) + return render() +} + +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', () => { + renderWithRouter() + expect(screen.getByText('Vibes')).toBeInTheDocument() + expect(screen.getByText('Remote control for Claude Code')).toBeInTheDocument() + }) + + it('renders status badge', () => { + renderWithRouter() + expect(screen.getByText('Available')).toBeInTheDocument() + }) + + it('renders feature list', () => { + renderWithRouter() + expect(screen.getByText('Feature 1')).toBeInTheDocument() + expect(screen.getByText('Feature 2')).toBeInTheDocument() + }) + + it('renders link to product page', () => { + renderWithRouter() + expect(screen.getByRole('link', { name: /learn more/i })).toHaveAttribute( + 'href', + '/products/vibes', + ) + }) +}) +``` + +**Step 2: Run test to verify it fails** + +```bash +pnpm test src/components/products/ProductCard/ProductCard.test.tsx +``` + +Expected: FAIL - module not found + +**Step 3: Write minimal implementation** + +Create `src/components/products/ProductCard/ProductCard.tsx`: + +```tsx +import { cn } from '@/lib/cn' +import { Card, CardContent, Heading, Text } from '@/components/ui' +import { Link } from '@tanstack/react-router' +import { StatusBadge, type ProductStatus } 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} +
+ )} + +
+ +
+
+ + {name} + + {tagline} +
+
    + {features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + Learn More → + +
+
+ ) +} +``` + +Create `src/components/products/ProductCard/index.ts`: + +```typescript +export { ProductCard } from './ProductCard' +``` + +**Step 4: Run test to verify it passes** + +```bash +pnpm test src/components/products/ProductCard/ProductCard.test.tsx +``` + +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/components/products/ProductCard/ +git commit -m "feat: add ProductCard component for products index" +``` + +--- + +## Task 9: Products Index Export + +**Files:** +- Create: `src/components/products/index.ts` + +**Step 1: Create barrel export** + +Create `src/components/products/index.ts`: + +```typescript +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' +``` + +**Step 2: Commit** + +```bash +git add src/components/products/index.ts +git commit -m "feat: add products components barrel export" +``` + +--- + +## Task 10: Placeholder Assets + +**Files:** +- Create: `public/images/products/vibes-terminal.svg` +- Create: `public/images/products/volt-dashboard.svg` +- Create: `public/images/products/volt-bg.svg` + +**Step 1: Create placeholder directory** + +```bash +mkdir -p public/images/products +``` + +**Step 2: Create Vibes terminal placeholder** + +Create `public/images/products/vibes-terminal.svg`: + +```svg + + + + + + + $ vibes claude "refactor the auth module" + ✓ Session started + → Web UI available at http://localhost:7432 + [Placeholder - replace with real screenshot] + +``` + +**Step 3: Create Volt dashboard placeholder** + +Create `public/images/products/volt-dashboard.svg`: + +```svg + + + + + + + IV Surface + Greeks Chart + P&L Attribution + Risk Limits + [Placeholder - replace with real screenshot] + +``` + +**Step 4: Create Volt background placeholder** + +Create `public/images/products/volt-bg.svg`: + +```svg + + + + + + + + + + + +``` + +**Step 5: Commit** + +```bash +git add public/images/products/ +git commit -m "feat: add placeholder product images" +``` + +--- + +## Task 11: Products Index Route + +**Files:** +- Create: `src/routes/products/index.tsx` + +**Step 1: Create products index route** + +Create `src/routes/products/index.tsx`: + +```tsx +import { Container } from '@/components/ui/Container' +import { Section } from '@/components/ui/Section' +import { Heading, Text } from '@/components/ui/Typography' +import { ProductCard } from '@/components/products' +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) => ( + + ))} +
+
+
+ + ) +} +``` + +**Step 2: Test dev server** + +```bash +pnpm dev +# Navigate to http://localhost:3000/products +``` + +Expected: Products index page renders with both product cards + +**Step 3: Commit** + +```bash +git add src/routes/products/index.tsx +git commit -m "feat: add products index page" +``` + +--- + +## Task 12: Vibes Product Route + +**Files:** +- Create: `src/routes/products/vibes.tsx` + +**Step 1: Create Vibes product route** + +Create `src/routes/products/vibes.tsx`: + +```tsx +import { Button, Container, Heading, Section, Text } from '@/components/ui' +import { BuiltByVibes, CodeBlock, FeatureGrid } from '@/components/products' +import { createFileRoute, Link } 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 */} +
+ +
+ Vibes terminal interface +
+
+
+ + {/* Features */} +
+ + + Features + + + +
+ + {/* How It Works */} +
+ + + How It Works + +
+ {steps.map((item) => ( +
+
+ {item.step} +
+ + {item.title} + + {item.description} +
+ ))} +
+
+
+ + {/* CTA */} +
+ + + +
+ + ) +} +``` + +**Step 2: Test dev server** + +```bash +pnpm dev +# Navigate to http://localhost:3000/products/vibes +``` + +Expected: Vibes product page renders with hero, features, how it works, and callout + +**Step 3: Commit** + +```bash +git add src/routes/products/vibes.tsx +git commit -m "feat: add Vibes product page" +``` + +--- + +## Task 13: Volt Teaser Route + +**Files:** +- Create: `src/routes/products/volt.tsx` + +**Step 1: Create Volt teaser route** + +Create `src/routes/products/volt.tsx`: + +```tsx +import { Container, Heading, Section, Text } from '@/components/ui' +import { BuiltByVibes, StatusBadge, WaitlistForm } from '@/components/products' +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} +
+ + {screenshot.caption} + +
+ ))} +
+
+
+ + {/* CTA */} +
+ + + +
+ + ) +} +``` + +**Step 2: Test dev server** + +```bash +pnpm dev +# Navigate to http://localhost:3000/products/volt +``` + +Expected: Volt teaser page renders with atmospheric hero, waitlist form, and screenshots + +**Step 3: Commit** + +```bash +git add src/routes/products/volt.tsx +git commit -m "feat: add Volt teaser page with waitlist" +``` + +--- + +## Task 14: Update Navbar + +**Files:** +- Modify: `src/components/navigation/Navbar.tsx` + +**Step 1: Add Products link to navbar** + +In `src/components/navigation/Navbar.tsx`, add Products link after the logo and before Services: + +```tsx +
+ + Products + + + Services + + + Let's Talk + +
+``` + +**Step 2: Test navigation** + +```bash +pnpm dev +# Click Products in navbar, verify navigation works +``` + +Expected: Products link appears and navigates to /products + +**Step 3: Commit** + +```bash +git add src/components/navigation/Navbar.tsx +git commit -m "feat: add Products link to navbar" +``` + +--- + +## Task 15: E2E Tests + +**Files:** +- Create: `e2e/products.spec.ts` + +**Step 1: Write E2E tests** + +Create `e2e/products.spec.ts`: + +```typescript +import { expect, test } from '@playwright/test' + +test.describe('Products Pages', () => { + test('products index displays both products', async ({ page }) => { + await page.goto('/products') + + await expect(page.getByRole('heading', { name: "What We're Building" })).toBeVisible() + await expect(page.getByText('Vibes')).toBeVisible() + await expect(page.getByText('Volt')).toBeVisible() + await expect(page.getByText('Available')).toBeVisible() + await expect(page.getByText('Coming Soon')).toBeVisible() + }) + + test('can navigate to Vibes product page', async ({ page }) => { + await page.goto('/products') + + await page.getByRole('link', { name: /learn more/i }).first().click() + await expect(page).toHaveURL('/products/vibes') + await expect(page.getByRole('heading', { name: 'Vibes', level: 1 })).toBeVisible() + }) + + test('Vibes page shows install command', async ({ page }) => { + await page.goto('/products/vibes') + + await expect(page.getByText('curl -sSf https://vibes.run/install | sh')).toBeVisible() + await expect(page.getByRole('link', { name: /star on github/i })).toBeVisible() + }) + + test('can navigate to Volt teaser page', async ({ page }) => { + await page.goto('/products') + + await page.getByRole('link', { name: /learn more/i }).last().click() + await expect(page).toHaveURL('/products/volt') + await expect(page.getByRole('heading', { name: 'Volt', level: 1 })).toBeVisible() + }) + + test('Volt page shows waitlist form', async ({ page }) => { + await page.goto('/products/volt') + + await expect(page.getByText('Coming Soon')).toBeVisible() + await expect(page.getByPlaceholder(/email/i)).toBeVisible() + await expect(page.getByRole('button', { name: /get early access/i })).toBeVisible() + }) + + test('navbar has Products link', async ({ page }) => { + await page.goto('/') + + const productsLink = page.getByRole('link', { name: 'Products' }) + await expect(productsLink).toBeVisible() + await productsLink.click() + await expect(page).toHaveURL('/products') + }) +}) +``` + +**Step 2: Run E2E tests** + +```bash +just e2e +``` + +Expected: All products tests pass + +**Step 3: Commit** + +```bash +git add e2e/products.spec.ts +git commit -m "test: add E2E tests for products pages" +``` + +--- + +## Task 16: Run All Checks + +**Step 1: Run full check suite** + +```bash +just check +``` + +Expected: All checks pass (typecheck, lint, test, e2e) + +**Step 2: Verify dev server** + +```bash +pnpm dev +# Navigate through all pages, verify everything works +``` + +**Step 3: Final commit if any fixes needed** + +--- + +## Task 17: Update Progress Doc + +**Files:** +- Modify: `docs/PROGRESS.md` + +**Step 1: Update Products milestone status** + +Update the Products section in Phase 3: + +```markdown +#### Products +| Product | Status | Description | +|---------|--------|-------------| +| [Vibes](https://github.com/run-vibes/vibes) | ✅ Done | Remote control for your Claude Code sessions | +| Volt | ✅ Done | Volatility analysis, simulation & trade execution system | +``` + +**Step 2: Add Recent Updates entry** + +Add to Recent Updates: + +```markdown +### 2025-12-26 (Products Pages) +- Added products index page with product cards ([#XX](https://github.com/run-vibes/website/pull/XX)) +- Added Vibes product page with features, how it works, install command +- Added Volt teaser page with atmospheric background and waitlist form +- Added waitlist API endpoint for email capture +- Added Products link to navigation +``` + +**Step 3: Commit** + +```bash +git add docs/PROGRESS.md +git commit -m "docs: update progress with products milestone" +``` From 0c4f048d152f511cd9532be9e3038aed45b6b004 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:00:43 -0800 Subject: [PATCH 04/21] feat: add waitlist table migration --- justfile | 6 +++--- workers/chat-api/migrations/0003_waitlist.sql | 13 +++++++++++++ workers/chat-api/wrangler.toml | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 workers/chat-api/migrations/0003_waitlist.sql diff --git a/justfile b/justfile index c6a6af6..a1965dc 100644 --- a/justfile +++ b/justfile @@ -110,11 +110,11 @@ worker-db-create-staging: worker-migrate env: #!/usr/bin/env bash if [ "{{env}}" = "production" ]; then - cd workers/chat-api && wrangler d1 migrations apply vibes-chat --remote + cd workers/chat-api && wrangler d1 migrations apply vibes-chat --remote --config wrangler.toml elif [ "{{env}}" = "staging" ]; then - cd workers/chat-api && wrangler d1 migrations apply vibes-chat-staging --remote + cd workers/chat-api && wrangler d1 migrations apply vibes-chat-staging --remote --config wrangler.toml elif [ "{{env}}" = "local" ]; then - cd workers/chat-api && wrangler d1 migrations apply vibes-chat --local + cd workers/chat-api && wrangler d1 migrations apply vibes-chat --local --config wrangler.toml else echo "Error: env must be 'staging', 'production', or 'local'" exit 1 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/wrangler.toml b/workers/chat-api/wrangler.toml index d9680e4..25d8a1b 100644 --- a/workers/chat-api/wrangler.toml +++ b/workers/chat-api/wrangler.toml @@ -1,6 +1,7 @@ name = "vibes-chat-api" main = "src/index.ts" compatibility_date = "2024-01-01" +migrations_dir = "migrations" # Production (default) [vars] From e85624043ae4dcf07aed205c2790b5f1a8d76f86 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:02:39 -0800 Subject: [PATCH 05/21] feat: add waitlist API endpoint --- justfile | 2 +- workers/chat-api/src/index.ts | 25 +++++++++++++++++ workers/chat-api/src/waitlist.ts | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 workers/chat-api/src/waitlist.ts diff --git a/justfile b/justfile index a1965dc..4b0a70c 100644 --- a/justfile +++ b/justfile @@ -96,7 +96,7 @@ worker-deploy env: # Run chat worker locally worker-dev: - cd workers/chat-api && wrangler dev + cd workers/chat-api && wrangler dev --config wrangler.toml # Create D1 database for chat worker worker-db-create: diff --git a/workers/chat-api/src/index.ts b/workers/chat-api/src/index.ts index 8c6886a..b051c4f 100644 --- a/workers/chat-api/src/index.ts +++ b/workers/chat-api/src/index.ts @@ -1,4 +1,5 @@ import { callClaude, cleanResponse, isLeadComplete } from './claude' +import { addToWaitlist } from './waitlist' import { notifyTeam } from './email' import { extractLeadFromConversation, generatePRDDraft, saveLead } from './leads' import { calculateLeadScore, getLeadTier } from './scoring' @@ -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..faa27f0 --- /dev/null +++ b/workers/chat-api/src/waitlist.ts @@ -0,0 +1,47 @@ +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' } + } +} From 1b6f8d6b6d35805504dca3e1374d6c3ca2145ffa Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:03:41 -0800 Subject: [PATCH 06/21] feat: add StatusBadge component --- .../products/StatusBadge/StatusBadge.test.tsx | 19 +++++++++++ .../products/StatusBadge/StatusBadge.tsx | 33 +++++++++++++++++++ src/components/products/StatusBadge/index.ts | 1 + 3 files changed, 53 insertions(+) create mode 100644 src/components/products/StatusBadge/StatusBadge.test.tsx create mode 100644 src/components/products/StatusBadge/StatusBadge.tsx create mode 100644 src/components/products/StatusBadge/index.ts 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' From 33d82f7dfcfbd7ba97655023ad4b8810ee3422a4 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:05:00 -0800 Subject: [PATCH 07/21] feat: add CodeBlock component with copy functionality --- .../products/CodeBlock/CodeBlock.test.tsx | 36 +++++++++++++++++++ .../products/CodeBlock/CodeBlock.tsx | 36 +++++++++++++++++++ src/components/products/CodeBlock/index.ts | 1 + 3 files changed, 73 insertions(+) create mode 100644 src/components/products/CodeBlock/CodeBlock.test.tsx create mode 100644 src/components/products/CodeBlock/CodeBlock.tsx create mode 100644 src/components/products/CodeBlock/index.ts 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..1672c6c --- /dev/null +++ b/src/components/products/CodeBlock/CodeBlock.tsx @@ -0,0 +1,36 @@ +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 () => { + await navigator.clipboard.writeText(code) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + 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' From 660d0da0cedf40fffef1395a177433fc5c763e4a Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:05:41 -0800 Subject: [PATCH 08/21] feat: add FeatureGrid component --- .../products/FeatureGrid/FeatureGrid.test.tsx | 22 ++++++++++++ .../products/FeatureGrid/FeatureGrid.tsx | 34 +++++++++++++++++++ src/components/products/FeatureGrid/index.ts | 1 + 3 files changed, 57 insertions(+) create mode 100644 src/components/products/FeatureGrid/FeatureGrid.test.tsx create mode 100644 src/components/products/FeatureGrid/FeatureGrid.tsx create mode 100644 src/components/products/FeatureGrid/index.ts 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..851bfe0 --- /dev/null +++ b/src/components/products/FeatureGrid/FeatureGrid.tsx @@ -0,0 +1,34 @@ +import { cn } from '@/lib/cn' +import { Heading, Text } from '@/components/ui' + +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' From 1ae7af3ece0369b5caa23ea3fb98e3196e81606a Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:07:22 -0800 Subject: [PATCH 09/21] feat: add BuiltByVibes lead-gen callout component --- .../BuiltByVibes/BuiltByVibes.test.tsx | 23 ++++++++++++++ .../products/BuiltByVibes/BuiltByVibes.tsx | 30 +++++++++++++++++++ src/components/products/BuiltByVibes/index.ts | 1 + 3 files changed, 54 insertions(+) create mode 100644 src/components/products/BuiltByVibes/BuiltByVibes.test.tsx create mode 100644 src/components/products/BuiltByVibes/BuiltByVibes.tsx create mode 100644 src/components/products/BuiltByVibes/index.ts diff --git a/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx b/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx new file mode 100644 index 0000000..49cd699 --- /dev/null +++ b/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { BuiltByVibes } from './BuiltByVibes' + +// Mock the Link component from TanStack Router +vi.mock('@tanstack/react-router', () => ({ + 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..fc9eff0 --- /dev/null +++ b/src/components/products/BuiltByVibes/BuiltByVibes.tsx @@ -0,0 +1,30 @@ +import { cn } from '@/lib/cn' +import { Text } from '@/components/ui' +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' From cda10ea422d7b44760da1594266e2c6f5c60fa49 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:08:27 -0800 Subject: [PATCH 10/21] feat: add WaitlistForm component for email capture --- .../WaitlistForm/WaitlistForm.test.tsx | 61 ++++++++++++++++ .../products/WaitlistForm/WaitlistForm.tsx | 72 +++++++++++++++++++ src/components/products/WaitlistForm/index.ts | 1 + 3 files changed, 134 insertions(+) create mode 100644 src/components/products/WaitlistForm/WaitlistForm.test.tsx create mode 100644 src/components/products/WaitlistForm/WaitlistForm.tsx create mode 100644 src/components/products/WaitlistForm/index.ts diff --git a/src/components/products/WaitlistForm/WaitlistForm.test.tsx b/src/components/products/WaitlistForm/WaitlistForm.test.tsx new file mode 100644 index 0000000..8384daa --- /dev/null +++ b/src/components/products/WaitlistForm/WaitlistForm.test.tsx @@ -0,0 +1,61 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { describe, expect, it, vi, beforeEach } 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..fe93e43 --- /dev/null +++ b/src/components/products/WaitlistForm/WaitlistForm.tsx @@ -0,0 +1,72 @@ +import { cn } from '@/lib/cn' +import { Button, Input, Text } from '@/components/ui' +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 ( +
+ setEmail(e.target.value)} + required + disabled={status === 'loading'} + className="flex-1" + /> + + {status === 'error' && ( + + {errorMessage} + + )} +
+ ) +} 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' From 9a6771906fff3e46314aa7811fdd2ec7a07a72f2 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:09:16 -0800 Subject: [PATCH 11/21] feat: add ProductCard component for products index --- .../products/ProductCard/ProductCard.test.tsx | 45 ++++++++++++++ .../products/ProductCard/ProductCard.tsx | 59 +++++++++++++++++++ src/components/products/ProductCard/index.ts | 1 + 3 files changed, 105 insertions(+) create mode 100644 src/components/products/ProductCard/ProductCard.test.tsx create mode 100644 src/components/products/ProductCard/ProductCard.tsx create mode 100644 src/components/products/ProductCard/index.ts diff --git a/src/components/products/ProductCard/ProductCard.test.tsx b/src/components/products/ProductCard/ProductCard.test.tsx new file mode 100644 index 0000000..e8ec029 --- /dev/null +++ b/src/components/products/ProductCard/ProductCard.test.tsx @@ -0,0 +1,45 @@ +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..5031ee3 --- /dev/null +++ b/src/components/products/ProductCard/ProductCard.tsx @@ -0,0 +1,59 @@ +import { cn } from '@/lib/cn' +import { Card, CardContent, Heading, Text } from '@/components/ui' +import { Link } from '@tanstack/react-router' +import { StatusBadge, type ProductStatus } 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} +
+ )} + +
+ +
+
+ + {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' From 508a967b9a9570394b4bc704d5f5007beaaf1953 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:09:30 -0800 Subject: [PATCH 12/21] feat: add products components barrel export --- src/components/products/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/components/products/index.ts 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' From 97f8c5880007d25afe71c42d5ec8d35d94cc1647 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:10:27 -0800 Subject: [PATCH 13/21] feat: add placeholder product images --- public/images/products/vibes-terminal.svg | 11 +++++++++++ public/images/products/volt-bg.svg | 11 +++++++++++ public/images/products/volt-dashboard.svg | 12 ++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 public/images/products/vibes-terminal.svg create mode 100644 public/images/products/volt-bg.svg create mode 100644 public/images/products/volt-dashboard.svg diff --git a/public/images/products/vibes-terminal.svg b/public/images/products/vibes-terminal.svg new file mode 100644 index 0000000..cb1a1b1 --- /dev/null +++ b/public/images/products/vibes-terminal.svg @@ -0,0 +1,11 @@ + + + + + + + $ vibes claude "refactor the auth module" + ✓ Session started + → Web UI available at http://localhost:7432 + [Placeholder - replace with real screenshot] + diff --git a/public/images/products/volt-bg.svg b/public/images/products/volt-bg.svg new file mode 100644 index 0000000..c1fadd2 --- /dev/null +++ b/public/images/products/volt-bg.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/images/products/volt-dashboard.svg b/public/images/products/volt-dashboard.svg new file mode 100644 index 0000000..9f03f45 --- /dev/null +++ b/public/images/products/volt-dashboard.svg @@ -0,0 +1,12 @@ + + + + + + + IV Surface + Greeks Chart + P&L Attribution + Risk Limits + [Placeholder - replace with real screenshot] + From b1fecbf241dc9d1d489d454f81a0ce92a395174b Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:10:53 -0800 Subject: [PATCH 14/21] feat: add products index page --- src/routes/products/index.tsx | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/routes/products/index.tsx diff --git a/src/routes/products/index.tsx b/src/routes/products/index.tsx new file mode 100644 index 0000000..0b5d298 --- /dev/null +++ b/src/routes/products/index.tsx @@ -0,0 +1,63 @@ +import { Container } from '@/components/ui/Container' +import { Section } from '@/components/ui/Section' +import { Heading, Text } from '@/components/ui/Typography' +import { ProductCard } from '@/components/products' +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) => ( + + ))} +
+
+
+ + ) +} From 1e3c69553ef08755f940eb6b23174c0601b23d50 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:11:25 -0800 Subject: [PATCH 15/21] feat: add Vibes product page --- src/routes/products/vibes.tsx | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/routes/products/vibes.tsx diff --git a/src/routes/products/vibes.tsx b/src/routes/products/vibes.tsx new file mode 100644 index 0000000..a3cdd3f --- /dev/null +++ b/src/routes/products/vibes.tsx @@ -0,0 +1,123 @@ +import { Button, Container, Heading, Section, Text } from '@/components/ui' +import { BuiltByVibes, CodeBlock, FeatureGrid } from '@/components/products' +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 */} +
+ +
+ Vibes terminal interface +
+
+
+ + {/* Features */} +
+ + + Features + + + +
+ + {/* How It Works */} +
+ + + How It Works + +
+ {steps.map((item) => ( +
+
+ {item.step} +
+ + {item.title} + + {item.description} +
+ ))} +
+
+
+ + {/* CTA */} +
+ + + +
+ + ) +} From 6c57debea9c497af4086644c2a809cacdfee43c2 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:12:01 -0800 Subject: [PATCH 16/21] feat: add Volt teaser page with waitlist --- src/routes/products/volt.tsx | 77 ++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/routes/products/volt.tsx diff --git a/src/routes/products/volt.tsx b/src/routes/products/volt.tsx new file mode 100644 index 0000000..10f387e --- /dev/null +++ b/src/routes/products/volt.tsx @@ -0,0 +1,77 @@ +import { Container, Heading, Section, Text } from '@/components/ui' +import { BuiltByVibes, StatusBadge, WaitlistForm } from '@/components/products' +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} +
+ + {screenshot.caption} + +
+ ))} +
+
+
+ + {/* CTA */} +
+ + + +
+ + ) +} From 2446f80e8bfff4f4f89be1789b8db5909113f0ab Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:12:22 -0800 Subject: [PATCH 17/21] feat: add Products link to navbar --- src/components/navigation/Navbar.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/navigation/Navbar.tsx b/src/components/navigation/Navbar.tsx index f5da945..c92e9cd 100644 --- a/src/components/navigation/Navbar.tsx +++ b/src/components/navigation/Navbar.tsx @@ -20,6 +20,12 @@ export function Navbar({ className }: NavbarProps) {
+ + Products + Date: Fri, 26 Dec 2025 16:12:44 -0800 Subject: [PATCH 18/21] test: add E2E tests for products pages --- e2e/products.spec.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 e2e/products.spec.ts diff --git a/e2e/products.spec.ts b/e2e/products.spec.ts new file mode 100644 index 0000000..0dbdf76 --- /dev/null +++ b/e2e/products.spec.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test' + +test.describe('Products Pages', () => { + test('products index displays both products', async ({ page }) => { + await page.goto('/products') + + await expect(page.getByRole('heading', { name: "What We're Building" })).toBeVisible() + await expect(page.getByText('Vibes')).toBeVisible() + await expect(page.getByText('Volt')).toBeVisible() + await expect(page.getByText('Available')).toBeVisible() + await expect(page.getByText('Coming Soon')).toBeVisible() + }) + + test('can navigate to Vibes product page', async ({ page }) => { + await page.goto('/products') + + await page.getByRole('link', { name: /learn more/i }).first().click() + await expect(page).toHaveURL('/products/vibes') + await expect(page.getByRole('heading', { name: 'Vibes', level: 1 })).toBeVisible() + }) + + test('Vibes page shows install command', async ({ page }) => { + await page.goto('/products/vibes') + + await expect(page.getByText('curl -sSf https://vibes.run/install | sh')).toBeVisible() + await expect(page.getByRole('link', { name: /star on github/i })).toBeVisible() + }) + + test('can navigate to Volt teaser page', async ({ page }) => { + await page.goto('/products') + + await page.getByRole('link', { name: /learn more/i }).last().click() + await expect(page).toHaveURL('/products/volt') + await expect(page.getByRole('heading', { name: 'Volt', level: 1 })).toBeVisible() + }) + + test('Volt page shows waitlist form', async ({ page }) => { + await page.goto('/products/volt') + + await expect(page.getByText('Coming Soon')).toBeVisible() + await expect(page.getByPlaceholder(/email/i)).toBeVisible() + await expect(page.getByRole('button', { name: /get early access/i })).toBeVisible() + }) + + test('navbar has Products link', async ({ page }) => { + await page.goto('/') + + const productsLink = page.getByRole('link', { name: 'Products' }) + await expect(productsLink).toBeVisible() + await productsLink.click() + await expect(page).toHaveURL('/products') + }) +}) From 8ddc3b633abc06c0755912be1ce267f2a4abd60d Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:17:03 -0800 Subject: [PATCH 19/21] fix: use precise selectors in E2E tests for Products pages - Use getByRole('heading', { level: 3 }) for product headings instead of getByText - Add exact: true to Products navbar link to avoid matching 'Product Development' --- e2e/products.spec.ts | 16 ++-- .../BuiltByVibes/BuiltByVibes.test.tsx | 4 +- .../products/BuiltByVibes/BuiltByVibes.tsx | 14 +--- .../products/FeatureGrid/FeatureGrid.tsx | 8 +- .../products/ProductCard/ProductCard.test.tsx | 4 +- .../products/ProductCard/ProductCard.tsx | 4 +- .../WaitlistForm/WaitlistForm.test.tsx | 2 +- .../products/WaitlistForm/WaitlistForm.tsx | 5 +- src/routeTree.gen.ts | 82 ++++++++++++++++++- src/routes/products/index.tsx | 2 +- src/routes/products/vibes.tsx | 14 +++- src/routes/products/volt.tsx | 12 +-- workers/chat-api/src/index.ts | 2 +- workers/chat-api/src/waitlist.ts | 7 +- 14 files changed, 131 insertions(+), 45 deletions(-) diff --git a/e2e/products.spec.ts b/e2e/products.spec.ts index 0dbdf76..1d921e9 100644 --- a/e2e/products.spec.ts +++ b/e2e/products.spec.ts @@ -5,8 +5,8 @@ test.describe('Products Pages', () => { await page.goto('/products') await expect(page.getByRole('heading', { name: "What We're Building" })).toBeVisible() - await expect(page.getByText('Vibes')).toBeVisible() - await expect(page.getByText('Volt')).toBeVisible() + await expect(page.getByRole('heading', { name: 'Vibes', level: 3 })).toBeVisible() + await expect(page.getByRole('heading', { name: 'Volt', level: 3 })).toBeVisible() await expect(page.getByText('Available')).toBeVisible() await expect(page.getByText('Coming Soon')).toBeVisible() }) @@ -14,7 +14,10 @@ test.describe('Products Pages', () => { test('can navigate to Vibes product page', async ({ page }) => { await page.goto('/products') - await page.getByRole('link', { name: /learn more/i }).first().click() + await page + .getByRole('link', { name: /learn more/i }) + .first() + .click() await expect(page).toHaveURL('/products/vibes') await expect(page.getByRole('heading', { name: 'Vibes', level: 1 })).toBeVisible() }) @@ -29,7 +32,10 @@ test.describe('Products Pages', () => { test('can navigate to Volt teaser page', async ({ page }) => { await page.goto('/products') - await page.getByRole('link', { name: /learn more/i }).last().click() + await page + .getByRole('link', { name: /learn more/i }) + .last() + .click() await expect(page).toHaveURL('/products/volt') await expect(page.getByRole('heading', { name: 'Volt', level: 1 })).toBeVisible() }) @@ -45,7 +51,7 @@ test.describe('Products Pages', () => { test('navbar has Products link', async ({ page }) => { await page.goto('/') - const productsLink = page.getByRole('link', { name: 'Products' }) + const productsLink = page.getByRole('link', { name: 'Products', exact: true }) await expect(productsLink).toBeVisible() await productsLink.click() await expect(page).toHaveURL('/products') diff --git a/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx b/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx index 49cd699..5482cb8 100644 --- a/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx +++ b/src/components/products/BuiltByVibes/BuiltByVibes.test.tsx @@ -5,7 +5,9 @@ import { BuiltByVibes } from './BuiltByVibes' // Mock the Link component from TanStack Router vi.mock('@tanstack/react-router', () => ({ Link: ({ to, children, ...props }: { to: string; children: React.ReactNode }) => ( - {children} + + {children} + ), })) diff --git a/src/components/products/BuiltByVibes/BuiltByVibes.tsx b/src/components/products/BuiltByVibes/BuiltByVibes.tsx index fc9eff0..35523f9 100644 --- a/src/components/products/BuiltByVibes/BuiltByVibes.tsx +++ b/src/components/products/BuiltByVibes/BuiltByVibes.tsx @@ -1,5 +1,5 @@ -import { cn } from '@/lib/cn' import { Text } from '@/components/ui' +import { cn } from '@/lib/cn' import { Link } from '@tanstack/react-router' interface BuiltByVibesProps { @@ -8,16 +8,10 @@ interface BuiltByVibesProps { export function BuiltByVibes({ className }: BuiltByVibesProps) { return ( -
+
- Built by{' '} - Vibes, the - agentic consulting studio.{' '} + Built by Vibes, the agentic + consulting studio.{' '} {features.map((feature) => (
diff --git a/src/components/products/ProductCard/ProductCard.test.tsx b/src/components/products/ProductCard/ProductCard.test.tsx index e8ec029..5741c8b 100644 --- a/src/components/products/ProductCard/ProductCard.test.tsx +++ b/src/components/products/ProductCard/ProductCard.test.tsx @@ -5,7 +5,9 @@ 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} + + {children} + ), })) diff --git a/src/components/products/ProductCard/ProductCard.tsx b/src/components/products/ProductCard/ProductCard.tsx index 5031ee3..bb6aaca 100644 --- a/src/components/products/ProductCard/ProductCard.tsx +++ b/src/components/products/ProductCard/ProductCard.tsx @@ -1,7 +1,7 @@ -import { cn } from '@/lib/cn' import { Card, CardContent, Heading, Text } from '@/components/ui' +import { cn } from '@/lib/cn' import { Link } from '@tanstack/react-router' -import { StatusBadge, type ProductStatus } from '../StatusBadge' +import { type ProductStatus, StatusBadge } from '../StatusBadge' interface ProductCardProps { name: string diff --git a/src/components/products/WaitlistForm/WaitlistForm.test.tsx b/src/components/products/WaitlistForm/WaitlistForm.test.tsx index 8384daa..79e6ad1 100644 --- a/src/components/products/WaitlistForm/WaitlistForm.test.tsx +++ b/src/components/products/WaitlistForm/WaitlistForm.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { describe, expect, it, vi, beforeEach } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { WaitlistForm } from './WaitlistForm' const mockFetch = vi.fn() diff --git a/src/components/products/WaitlistForm/WaitlistForm.tsx b/src/components/products/WaitlistForm/WaitlistForm.tsx index fe93e43..e29b07d 100644 --- a/src/components/products/WaitlistForm/WaitlistForm.tsx +++ b/src/components/products/WaitlistForm/WaitlistForm.tsx @@ -1,5 +1,5 @@ -import { cn } from '@/lib/cn' import { Button, Input, Text } from '@/components/ui' +import { cn } from '@/lib/cn' import { useState } from 'react' interface WaitlistFormProps { @@ -42,7 +42,8 @@ export function WaitlistForm({ product, className }: WaitlistFormProps) { return (
- You're on the list! We'll notify you when {product === 'volt' ? 'Volt' : 'Vibes'} launches. + You're on the list! We'll notify you when {product === 'volt' ? 'Volt' : 'Vibes'}{' '} + launches.
) 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 index 0b5d298..616ad9e 100644 --- a/src/routes/products/index.tsx +++ b/src/routes/products/index.tsx @@ -1,7 +1,7 @@ +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 { ProductCard } from '@/components/products' import { createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/products/')({ diff --git a/src/routes/products/vibes.tsx b/src/routes/products/vibes.tsx index a3cdd3f..9e7572d 100644 --- a/src/routes/products/vibes.tsx +++ b/src/routes/products/vibes.tsx @@ -1,5 +1,5 @@ -import { Button, Container, Heading, Section, Text } from '@/components/ui' 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')({ @@ -54,12 +54,20 @@ function VibesPage() {
diff --git a/src/routes/products/volt.tsx b/src/routes/products/volt.tsx index 10f387e..50c484d 100644 --- a/src/routes/products/volt.tsx +++ b/src/routes/products/volt.tsx @@ -1,5 +1,5 @@ -import { Container, Heading, Section, Text } from '@/components/ui' 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')({ @@ -33,8 +33,8 @@ function VoltPage() { Volatility analysis, simulation & trade execution - A comprehensive platform for traders who want to analyze options volatility, - backtest strategies, and execute with confidence. + A comprehensive platform for traders who want to analyze options volatility, backtest + strategies, and execute with confidence. @@ -51,11 +51,7 @@ function VoltPage() { {screenshots.map((screenshot) => (
- {screenshot.caption} + {screenshot.caption}
{screenshot.caption} diff --git a/workers/chat-api/src/index.ts b/workers/chat-api/src/index.ts index b051c4f..62be755 100644 --- a/workers/chat-api/src/index.ts +++ b/workers/chat-api/src/index.ts @@ -1,5 +1,4 @@ import { callClaude, cleanResponse, isLeadComplete } from './claude' -import { addToWaitlist } from './waitlist' import { notifyTeam } from './email' import { extractLeadFromConversation, generatePRDDraft, saveLead } from './leads' import { calculateLeadScore, getLeadTier } from './scoring' @@ -12,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. diff --git a/workers/chat-api/src/waitlist.ts b/workers/chat-api/src/waitlist.ts index faa27f0..abafc89 100644 --- a/workers/chat-api/src/waitlist.ts +++ b/workers/chat-api/src/waitlist.ts @@ -36,7 +36,12 @@ export async function addToWaitlist( VALUES (?, ?, ?, ?) ON CONFLICT (email, product) DO NOTHING`, ) - .bind(entry.email.toLowerCase(), entry.product, entry.referrer ?? null, entry.userAgent ?? null) + .bind( + entry.email.toLowerCase(), + entry.product, + entry.referrer ?? null, + entry.userAgent ?? null, + ) .run() return { success: true } From 3a1566b60ba582f00c8e0f37d63b4af9c8ec3bde Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:18:26 -0800 Subject: [PATCH 20/21] docs: update progress for products pages implementation - Add product page tasks to Phase 3 Products milestone - Update Phase 3 status to In Progress (20%) - Add 2025-12-26 entry to Recent Updates section --- docs/PROGRESS.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 4f0c283..c475848 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,7 +2,7 @@ Track the progress of phases, milestones, and tasks for the Vibes website. -**Last Updated:** 2025-12-24 +**Last Updated:** 2025-12-26 --- @@ -12,7 +12,7 @@ Track the progress of phases, milestones, and tasks for the Vibes website. |-------|------|--------|----------| | 1 | Foundation (MVP) | Complete | 100% | | 2 | Brand Identity | Complete | 100% | -| 3 | Content & Credibility | Not Started | 0% | +| 3 | Content & Credibility | In Progress | 20% | | 4 | Insights & Growth | Not Started | 0% | | 5 | Polish & Expand | Not Started | 0% | @@ -171,10 +171,18 @@ Track the progress of phases, milestones, and tasks for the Vibes website. | Newsletter signup form | ⬜ Not Started | — | #### Products -| Product | Status | Description | -|---------|--------|-------------| -| [Vibes](https://github.com/run-vibes/vibes) | ⬜ Not Started | Remote control for your Claude Code sessions | -| Volt | 🔄 In Progress | Volatility analysis, simulation & trade execution system | +| Product | Page Status | Product Status | Description | +|---------|-------------|----------------|-------------| +| [Vibes](https://github.com/run-vibes/vibes) | ✅ Done | 🔄 In Progress | Remote control for your Claude Code sessions | +| Volt | ✅ Done | 🔄 In Progress | Volatility analysis, simulation & trade execution system | + +| Task | Status | PR | +|------|--------|-----| +| Products index route (/products) | ✅ Done | [#27](https://github.com/run-vibes/website/pull/27) | +| Vibes product page (/products/vibes) | ✅ Done | [#27](https://github.com/run-vibes/website/pull/27) | +| Volt teaser page (/products/volt) | ✅ Done | [#27](https://github.com/run-vibes/website/pull/27) | +| Waitlist API endpoint | ✅ Done | [#27](https://github.com/run-vibes/website/pull/27) | +| Product components (StatusBadge, CodeBlock, WaitlistForm, etc.) | ✅ Done | [#27](https://github.com/run-vibes/website/pull/27) | #### Industry Pages | Page | Status | PR | @@ -245,6 +253,16 @@ Track the progress of phases, milestones, and tasks for the Vibes website. ## Recent Updates +### 2025-12-26 (Products Pages) +- Added product showcase pages for Vibes and Volt ([#27](https://github.com/run-vibes/website/pull/27)) + - Products index page (/products) with product cards + - Vibes product page (/products/vibes) with install command, features, and "How It Works" + - Volt teaser page (/products/volt) with atmospheric design and waitlist form +- Added waitlist API endpoint for email capture +- Created 6 new product components: StatusBadge, CodeBlock, FeatureGrid, BuiltByVibes, WaitlistForm, ProductCard +- Added Products link to navbar +- Updated Phase 3 to In Progress + ### 2025-12-24 (CI/CD Automation) - Simplified CI workflow - Cloudflare's GitHub integration handles Pages deployment (#19) - Added staging environment for chat worker From 14164d1d074ad7d811de54797847d7fff30d898d Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Fri, 26 Dec 2025 16:27:18 -0800 Subject: [PATCH 21/21] fix: address PR review comments - Add comment explaining migrations_dir in wrangler.toml - Add try-catch error handling to CodeBlock clipboard copy - Convert volt.tsx inline styles to Tailwind arbitrary values --- src/components/products/CodeBlock/CodeBlock.tsx | 11 ++++++++--- src/routes/products/volt.tsx | 7 +------ workers/chat-api/wrangler.toml | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/products/CodeBlock/CodeBlock.tsx b/src/components/products/CodeBlock/CodeBlock.tsx index 1672c6c..8071698 100644 --- a/src/components/products/CodeBlock/CodeBlock.tsx +++ b/src/components/products/CodeBlock/CodeBlock.tsx @@ -10,9 +10,14 @@ export function CodeBlock({ code, className }: CodeBlockProps) { const [copied, setCopied] = useState(false) const handleCopy = async () => { - await navigator.clipboard.writeText(code) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + 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 ( diff --git a/src/routes/products/volt.tsx b/src/routes/products/volt.tsx index 50c484d..20b6203 100644 --- a/src/routes/products/volt.tsx +++ b/src/routes/products/volt.tsx @@ -16,12 +16,7 @@ function VoltPage() { {/* Hero with atmospheric background */}
diff --git a/workers/chat-api/wrangler.toml b/workers/chat-api/wrangler.toml index 25d8a1b..cff9d56 100644 --- a/workers/chat-api/wrangler.toml +++ b/workers/chat-api/wrangler.toml @@ -1,6 +1,7 @@ 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)