Add resort comparison tab to web UI with preference sliders and budget filtering

This commit is contained in:
2025-10-30 08:23:24 -04:00
parent 74f8e268c3
commit 1540bb2609
2 changed files with 261 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,8 @@ type Result = {
}; };
export default function Page() { export default function Page() {
const [activeTab, setActiveTab] = useState<'flights' | 'resorts'>('flights');
const [origin, setOrigin] = useState("YOW"); const [origin, setOrigin] = useState("YOW");
const [dest, setDest] = useState("CUN,PUJ,MBJ"); const [dest, setDest] = useState("CUN,PUJ,MBJ");
const [startDate, setStartDate] = useState(new Date().toISOString().slice(0,10)); const [startDate, setStartDate] = useState(new Date().toISOString().slice(0,10));
@@ -25,6 +27,19 @@ export default function Page() {
const [useAC, setUseAC] = useState(true); const [useAC, setUseAC] = useState(true);
const [useAT, setUseAT] = useState(true); const [useAT, setUseAT] = useState(true);
// Resort comparison state
const [resortLoading, setResortLoading] = useState(false);
const [resortResults, setResortResults] = useState<any[]>([]);
const [selectedResorts, setSelectedResorts] = useState<string[]>([]);
const [resortOrigin, setResortOrigin] = useState("YOW");
const [resortDate, setResortDate] = useState(new Date().toISOString().slice(0,10));
const [resortNights, setResortNights] = useState(7);
const [resortBudget, setResortBudget] = useState<number | ''>(5500);
const [prefs, setPrefs] = useState({
beach: 8, pool: 8, golf: 5, spa: 7, food: 9,
nightlife: 6, shopping: 4, culture: 5, outdoor: 6, family: 5
});
async function onSubmit(e: React.FormEvent) { async function onSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -64,9 +79,80 @@ export default function Page() {
setLoading(false); setLoading(false);
} }
const availableResorts = [
"Dreams Onyx Resort & Spa", "Excellence Riviera Cancun", "Secrets Maroma Beach",
"Hyatt Ziva Cancun", "Moon Palace Cancun", "Barcelo Maya Riviera",
"Grand Sirenis Riviera Maya", "Iberostar Selection Paraiso Maya", "Hotel Xcaret Mexico",
"Dreams Punta Cana", "Excellence Punta Cana", "Secrets Cap Cana",
"Hyatt Ziva Cap Cana", "Iberostar Grand Bavaro"
];
async function onResortCompare(e: React.FormEvent) {
e.preventDefault();
if (selectedResorts.length === 0) {
alert("Please select at least one resort");
return;
}
setResortLoading(true);
setResortResults([]);
const payload = {
resorts: selectedResorts,
departureDate: resortDate,
origin: resortOrigin,
tripLength: resortNights,
budget: resortBudget === '' ? null : Number(resortBudget),
contingencyPercent: 15,
preferences: prefs
};
const res = await fetch("/api/resort-compare", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
setResortResults(data.comparisons || data.allComparisons || []);
setResortLoading(false);
}
function toggleResort(resort: string) {
setSelectedResorts(prev =>
prev.includes(resort) ? prev.filter(r => r !== resort) : [...prev, resort]
);
}
return ( return (
<main className="space-y-6"> <main className="space-y-6">
<Section title="Your trip idea"> {/* Tab Switcher */}
<div className="flex gap-2 border-b border-slate-300 dark:border-slate-700">
<button
onClick={() => setActiveTab('flights')}
className={`px-6 py-3 font-semibold transition-colors ${
activeTab === 'flights'
? 'border-b-2 border-blue-600 text-blue-600 dark:text-blue-400'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
}`}
>
Flight Search
</button>
<button
onClick={() => setActiveTab('resorts')}
className={`px-6 py-3 font-semibold transition-colors ${
activeTab === 'resorts'
? 'border-b-2 border-blue-600 text-blue-600 dark:text-blue-400'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
}`}
>
🏖 Resort Comparison
</button>
</div>
{/* Flight Search Tab */}
{activeTab === 'flights' && (
<>
<Section title="Your trip idea">
<form className="grid md:grid-cols-4 gap-4" onSubmit={onSubmit}> <form className="grid md:grid-cols-4 gap-4" onSubmit={onSubmit}>
<div> <div>
<label className="label">From (IATA)</label> <label className="label">From (IATA)</label>
@@ -175,6 +261,144 @@ export default function Page() {
)); ));
})()} })()}
</Section> </Section>
</>
)}
{/* Resort Comparison Tab */}
{activeTab === 'resorts' && (
<>
<Section title="Compare Resorts">
<form onSubmit={onResortCompare} className="space-y-6">
{/* Basic Settings */}
<div className="grid md:grid-cols-4 gap-4">
<div>
<label className="label">From (IATA)</label>
<input className="input" value={resortOrigin} onChange={e=>setResortOrigin(e.target.value.toUpperCase())} maxLength={4} placeholder="YOW" />
</div>
<div>
<label className="label">Departure Date</label>
<input type="date" className="input" value={resortDate} onChange={e=>setResortDate(e.target.value)} />
</div>
<div>
<label className="label">Trip Length (nights)</label>
<input type="number" className="input" value={resortNights} onChange={e=>setResortNights(Number(e.target.value))} min={1} />
</div>
<div>
<label className="label">Budget (CAD)</label>
<input type="number" className="input" value={resortBudget} onChange={e=>setResortBudget(e.target.value === '' ? '' : Number(e.target.value))} placeholder="5500" />
</div>
</div>
{/* Preference Sliders */}
<div>
<label className="label mb-3">Your Preferences (0-10)</label>
<div className="grid md:grid-cols-2 lg:grid-cols-5 gap-4">
{Object.keys(prefs).map(key => (
<div key={key}>
<label className="text-sm capitalize mb-1 block">{key}</label>
<input
type="range"
min="0"
max="10"
value={prefs[key as keyof typeof prefs]}
onChange={e=>setPrefs({...prefs, [key]: Number(e.target.value)})}
className="w-full"
/>
<div className="text-xs text-center mt-1">{prefs[key as keyof typeof prefs]}</div>
</div>
))}
</div>
</div>
{/* Resort Selection */}
<div>
<label className="label mb-3">Select Resorts ({selectedResorts.length} selected)</label>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-2">
{availableResorts.map(resort => (
<label key={resort} className="inline-flex items-center gap-2 p-2 border rounded hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer">
<input
type="checkbox"
checked={selectedResorts.includes(resort)}
onChange={() => toggleResort(resort)}
/>
<span className="text-sm">{resort}</span>
</label>
))}
</div>
</div>
<button className="btn" type="submit" disabled={resortLoading}>
{resortLoading ? "Comparing..." : "Compare Resorts"}
</button>
</form>
</Section>
<Section title="Results">
{!resortLoading && resortResults.length === 0 && (
<div className="opacity-70">No results yet. Select resorts and hit compare.</div>
)}
{resortResults.length > 0 && (
<div className="space-y-4">
{resortResults.map((comp: any) => (
<div key={comp.resort.name} className="card p-4">
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-bold text-lg">{comp.resort.name}</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
{comp.resort.destination}, {comp.resort.country} ({comp.resort.airportCode})
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{comp.matchScore}
</div>
<div className="text-xs text-slate-600 dark:text-slate-400">Match Score</div>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4 mb-3">
<div>
<div className="text-xs text-slate-600 dark:text-slate-400">TripAdvisor</div>
<div className="font-semibold">{comp.resort.tripAdvisorRating}/5.0 ({comp.resort.tripAdvisorReviews?.toLocaleString()} reviews)</div>
</div>
<div>
<div className="text-xs text-slate-600 dark:text-slate-400">Price Tier</div>
<div className="font-semibold">{comp.resort.priceRange}</div>
</div>
<div>
<div className="text-xs text-slate-600 dark:text-slate-400">Estimated Cost (2 people)</div>
<div className="font-semibold">${comp.estimatedTotalMin?.toLocaleString()}-${comp.estimatedTotalMax?.toLocaleString()}</div>
</div>
</div>
{comp.budgetStatus && (
<div className={`text-sm mb-3 ${comp.withinBudget ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}>
{comp.budgetStatus}
</div>
)}
<div className="text-xs text-slate-600 dark:text-slate-400 mb-2">Resort Features:</div>
<div className="flex flex-wrap gap-2 text-xs">
<span className="badge">Beach: {comp.resort.features.beach}</span>
<span className="badge">Pool: {comp.resort.features.pool}</span>
<span className="badge">Food: {comp.resort.features.food}</span>
<span className="badge">Spa: {comp.resort.features.spa}</span>
<span className="badge">Nightlife: {comp.resort.features.nightlife}</span>
<span className="badge">Golf: {comp.resort.features.golf}</span>
</div>
<div className="mt-3 flex gap-2">
<a href={comp.flightLinks.skyscanner} target="_blank" rel="noopener" className="link text-xs">Skyscanner</a>
<a href={comp.flightLinks.googleFlights} target="_blank" rel="noopener" className="link text-xs">Google Flights</a>
<a href={comp.flightLinks.airCanada} target="_blank" rel="noopener" className="link text-xs">Air Canada</a>
</div>
</div>
))}
</div>
)}
</Section>
</>
)}
</main> </main>
); );
} }