'use client'; import { useState } from 'react'; import { EXPERIENCE_MODES, type ExperienceMode } from '@/lib/modes'; interface FormData { destination: string; startDate: string; numDays: number; budget: number; currency: string; experienceMode: ExperienceMode; preferences: string[]; } const PREFERENCE_OPTIONS = [ 'Culture & History', 'Gastronomy', 'Bars & Nightlife', 'Concerts & Music', 'Theater & Shows', 'Parks & Nature', 'Art & Museums', ]; export default function OttoPlanner() { const [form, setForm] = useState({ destination: '', startDate: '', numDays: 3, budget: 200, currency: 'EUR', experienceMode: 'balanced', preferences: [], }); const [isSubmitting, setIsSubmitting] = useState(false); // eslint-disable-next-line @typescript-eslint/no-explicit-any const [result, setResult] = useState(null); // TODO: proper Trip type after Phase 3 (stub) const togglePreference = (pref: string) => { setForm((prev) => ({ ...prev, preferences: prev.preferences.includes(pref) ? prev.preferences.filter((p) => p !== pref) : [...prev.preferences, pref], })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!form.destination || !form.startDate) { alert('Please provide destination and start date'); return; } setIsSubmitting(true); try { // TODO Phase 3: call real /api/generate const res = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }); if (!res.ok) throw new Error('Generation failed'); const data = await res.json(); setResult(data); } catch { // Stub response for early development (will be replaced) setResult({ tripId: 'stub-' + Date.now(), destination: form.destination, numDays: form.numDays, experienceMode: form.experienceMode, summary: `Stub itinerary for ${form.destination} (${form.numDays} days, ${form.experienceMode} mode). Budget: ${form.budget} ${form.currency}.`, days: Array.from({ length: form.numDays }, (_, i) => ({ day: i + 1, activities: [ { time: '09:30', title: 'Top cultural sight', type: 'attraction', cost: 12 }, { time: '13:00', title: 'Local lunch', type: 'restaurant', cost: 25 }, ], })), estimatedCost: Math.round(form.budget * 0.85), }); } finally { setIsSubmitting(false); } }; return (

Otto

Real itineraries powered by live APIs. Pick a mode and we’ll fit everything into your days and budget.

{/* Destination + Dates + Days */}
setForm({ ...form, destination: e.target.value })} placeholder="Barcelona, London, Mexico City..." className="w-full rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3" required />
setForm({ ...form, startDate: e.target.value })} className="w-full rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3" required />
setForm({ ...form, numDays: parseInt(e.target.value) || 1 })} className="w-full rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3" />
{/* Budget */}
setForm({ ...form, budget: parseInt(e.target.value) || 0 })} className="flex-1 rounded-xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-3" />
{/* Experience Mode */}
{EXPERIENCE_MODES.map((mode) => ( ))}
{/* Preferences */}
{PREFERENCE_OPTIONS.map((pref) => { const active = form.preferences.includes(pref); return ( ); })}
{/* Stub / Result area */} {result && (
Itinerary for
{result.destination}
{result.numDays} days • {result.experienceMode} mode
~{result.estimatedCost} {form.currency}
{result.days?.map((day: any, idx: number) => ( // eslint-disable-line @typescript-eslint/no-explicit-any
Day {day.day}
    {day.activities.map((a: any, i: number) => ( // eslint-disable-line @typescript-eslint/no-explicit-any
  • {a.time} — {a.title} {a.cost} {form.currency}
  • ))}
))}

This is a development stub. Real API + LLM generation coming in Phase 3.

)}
); }