Files
holiday-travel-app/app/page.tsx

181 lines
8.2 KiB
TypeScript

'use client';
import { useState } from "react";
import Section from "@/components/Section";
import DealCard from "@/components/DealCard";
type Result = {
results: any[]
};
export default function Page() {
const [origin, setOrigin] = useState("YOW");
const [dest, setDest] = useState("CUN,PUJ,MBJ");
const [startDate, setStartDate] = useState(new Date().toISOString().slice(0,10));
const [endDate, setEndDate] = useState(new Date(Date.now() + 1000*60*60*24*60).toISOString().slice(0,10));
const [minN, setMinN] = useState(5);
const [maxN, setMaxN] = useState(9);
const [budget, setBudget] = useState<number | ''>('');
const [nonStop, setNonStop] = useState(false);
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<any[]>([]);
const [useDeals, setUseDeals] = useState(true);
const [useSky, setUseSky] = useState(true);
const [useG, setUseG] = useState(true);
const [useAC, setUseAC] = useState(true);
const [useAT, setUseAT] = useState(true);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setResults([]);
const payload = {
origin: origin.trim().toUpperCase(),
destinations: dest.split(',').map(s => s.trim().toUpperCase()).filter(Boolean),
startDate, endDate,
tripLengthMin: Number(minN), tripLengthMax: Number(maxN),
budget: budget === '' ? null : Number(budget),
currency: "CAD",
nonStopOnly: nonStop,
sources: [
useDeals ? "Deals" : "",
useSky ? "Skyscanner" : "",
useG ? "GoogleFlights" : "",
useAC ? "AirCanada" : "",
useAT ? "AirTransat" : ""
].filter(Boolean)
};
const res = await fetch("/api/search", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
const data: Result = await res.json();
// Sort by destination, then by source/provider
const sorted = (data.results || []).sort((a: any, b: any) => {
// First sort by destination
const destCompare = (a.destination || '').localeCompare(b.destination || '');
if (destCompare !== 0) return destCompare;
// Then by source/provider
return (a.source || '').localeCompare(b.source || '');
});
setResults(sorted);
setLoading(false);
}
return (
<main className="space-y-6">
<Section title="Your trip idea">
<form className="grid md:grid-cols-4 gap-4" onSubmit={onSubmit}>
<div>
<label className="label">From (IATA)</label>
<input className="input" value={origin} onChange={e=>setOrigin(e.target.value.toUpperCase())} maxLength={4} placeholder="YOW" />
</div>
<div className="md:col-span-3">
<label className="label">To (IATA, comma-separated)</label>
<input className="input" value={dest} onChange={e=>setDest(e.target.value)} placeholder="CUN,PUJ,MBJ" />
</div>
<div>
<label className="label">Start date</label>
<input type="date" className="input" value={startDate} onChange={e=>setStartDate(e.target.value)} />
</div>
<div>
<label className="label">End date</label>
<input type="date" className="input" value={endDate} onChange={e=>setEndDate(e.target.value)} />
</div>
<div>
<label className="label">Trip length (min)</label>
<input type="number" className="input" value={minN} onChange={e=>setMinN(Number(e.target.value))} min={1} />
</div>
<div>
<label className="label">Trip length (max)</label>
<input type="number" className="input" value={maxN} onChange={e=>setMaxN(Number(e.target.value))} min={minN} />
</div>
<div>
<label className="label">Budget (CAD)</label>
<input type="number" className="input" value={budget} onChange={e=>setBudget(e.target.value === '' ? '' : Number(e.target.value))} />
</div>
<div className="flex items-end gap-3">
<label className="inline-flex items-center gap-2">
<input type="checkbox" checked={nonStop} onChange={e=>setNonStop(e.target.checked)} />
Non-stop only
</label>
</div>
<div className="md:col-span-4">
<label className="label">Sources</label>
<div className="flex flex-wrap gap-3">
<label className="inline-flex items-center gap-2"><input type="checkbox" checked={useDeals} onChange={e=>setUseDeals(e.target.checked)} /> Deal sites (YOW/YYZ/YUL/etc.)</label>
<label className="inline-flex items-center gap-2"><input type="checkbox" checked={useSky} onChange={e=>setUseSky(e.target.checked)} /> Skyscanner links</label>
<label className="inline-flex items-center gap-2"><input type="checkbox" checked={useG} onChange={e=>setUseG(e.target.checked)} /> Google Flights links</label>
<label className="inline-flex items-center gap-2"><input type="checkbox" checked={useAC} onChange={e=>setUseAC(e.target.checked)} /> Air Canada links</label>
<label className="inline-flex items-center gap-2"><input type="checkbox" checked={useAT} onChange={e=>setUseAT(e.target.checked)} /> Air Transat links</label>
</div>
</div>
<div className="md:col-span-4 flex gap-3">
<button className="btn" type="submit" disabled={loading}>{loading ? "Searching..." : "Search deals & dates"}</button>
<button className="btn" type="button" onClick={()=>{
localStorage.setItem("lastSearch", JSON.stringify({origin,dest,startDate,endDate,minN,maxN,budget,nonStop,useDeals,useSky,useG,useAC,useAT}));
alert("Saved to this browser.");
}}>Save search</button>
<button className="btn" type="button" onClick={()=>{
const raw = localStorage.getItem("lastSearch");
if (raw) {
const s = JSON.parse(raw);
setOrigin(s.origin); setDest(s.dest);
setStartDate(s.startDate); setEndDate(s.endDate);
setMinN(s.minN); setMaxN(s.maxN);
setBudget(s.budget); setNonStop(s.nonStop);
setUseDeals(s.useDeals); setUseSky(s.useSky); setUseG(s.useG); setUseAC(s.useAC); setUseAT(s.useAT || true);
} else alert("No saved search found.");
}}>Load last</button>
</div>
</form>
</Section>
<Section title="Results">
{!loading && results.length === 0 && (
<div className="opacity-70">No results yet. Fill the form and hit search.</div>
)}
{results.length > 0 && (() => {
// Group by destination
const grouped: Record<string, any[]> = {};
results.forEach((r: any) => {
const dest = r.destination || 'Unknown';
if (!grouped[dest]) grouped[dest] = [];
grouped[dest].push(r);
});
return Object.keys(grouped).sort().map(destination => (
<div key={destination} className="mb-8">
<h3 className="text-xl font-bold mb-4 border-b border-slate-300 dark:border-slate-700 pb-2">
{destination}
</h3>
{(() => {
// Group by source/provider within this destination
const bySource: Record<string, any[]> = {};
grouped[destination].forEach((r: any) => {
const src = r.source || 'Other';
if (!bySource[src]) bySource[src] = [];
bySource[src].push(r);
});
return Object.keys(bySource).sort().map(source => (
<div key={source} className="mb-6">
<h4 className="text-sm font-semibold mb-3 text-slate-600 dark:text-slate-400 uppercase tracking-wide">
{source}
</h4>
<div className="grid md:grid-cols-2 gap-4">
{bySource[source].map((r: any) => <DealCard key={r.id} deal={r} />)}
</div>
</div>
));
})()}
</div>
));
})()}
</Section>
</main>
);
}