mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
supabase product proxy and rendering
This commit is contained in:
37
web/src/app/api/products/route.ts
Normal file
37
web/src/app/api/products/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const type = searchParams.get('type');
|
||||
|
||||
if (!type || !['hotel', 'airline'].includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'type parameter must be "hotel" or "airline"' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const backendUrl = process.env.BACKEND_URL || 'http://localhost:5000';
|
||||
const url = new URL(`${backendUrl}/api/products/${type}`);
|
||||
|
||||
// forward date index offset to backend
|
||||
const dateIndex = searchParams.get('dateIndex');
|
||||
if (dateIndex) url.searchParams.set('dateIndex', dateIndex);
|
||||
|
||||
const res = await fetch(url.toString());
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Backend returned ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
console.error('[PRODUCTS_PROXY_ERROR]', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch products' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Navigation } from '@/components/ui';
|
||||
import HotelCard from '@/components/feats/hotel/HotelCard';
|
||||
import { transformProduct, type Hotel, type HotelProduct } from '@/lib/hotel-utils';
|
||||
|
||||
interface Hotel {
|
||||
id: string;
|
||||
name: string;
|
||||
roomType: string;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
amenities: string[];
|
||||
refundable: boolean;
|
||||
pricePerNight: number;
|
||||
nights: number;
|
||||
function RoomsList() {
|
||||
const searchParams = useSearchParams();
|
||||
const [rooms, setRooms] = useState<Hotel[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRooms = async () => {
|
||||
try {
|
||||
const url = new URL('/api/products', window.location.origin);
|
||||
url.searchParams.set('type', 'hotel');
|
||||
|
||||
const dateIndex = searchParams.get('dateIndex');
|
||||
if (dateIndex) url.searchParams.set('dateIndex', dateIndex);
|
||||
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
|
||||
const json = await res.json();
|
||||
const transformed = json.data.map((p: HotelProduct) => transformProduct(p));
|
||||
setRooms(transformed);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load products');
|
||||
console.error('[FETCH_ERROR]', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchRooms();
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-bold mb-6">Available Rooms</h1>
|
||||
{loading && <div className="text-center py-8">Loading...</div>}
|
||||
{error && <div className="text-red-500 text-center py-8">{error}</div>}
|
||||
{!loading && !error && (
|
||||
<div className="space-y-4">
|
||||
{rooms.map((r) => (
|
||||
<HotelCard key={r.id} hotel={r} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const genRandomHotels = (): Hotel[] => {
|
||||
const names = [
|
||||
'Grand Plaza Hotel',
|
||||
'Seaside Resort',
|
||||
'Downtown Suites',
|
||||
'Mountain View Lodge',
|
||||
'City Center Inn',
|
||||
'Luxury Beach Resort',
|
||||
'Urban Boutique Hotel',
|
||||
'Garden View Hotel',
|
||||
];
|
||||
const roomTypes = ['Standard Room', 'Deluxe Room', 'Suite', 'Executive Suite', 'Premium Room'];
|
||||
const amenities = ['wifi', 'pool', 'gym', 'parking', 'breakfast', 'spa'];
|
||||
|
||||
return Array.from({ length: 10 }, (_, i) => {
|
||||
const nights = Math.floor(Math.random() * 5) + 1;
|
||||
const basePrice = Math.floor(80 + Math.random() * 220);
|
||||
const selectedAmenities = amenities
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, Math.floor(Math.random() * 3) + 2);
|
||||
|
||||
const today = new Date();
|
||||
const checkInDate = new Date(today);
|
||||
checkInDate.setDate(today.getDate() + Math.floor(Math.random() * 10));
|
||||
const checkOutDate = new Date(checkInDate);
|
||||
checkOutDate.setDate(checkInDate.getDate() + nights);
|
||||
|
||||
return {
|
||||
id: `htl-${i}`,
|
||||
name: names[i % names.length],
|
||||
roomType: roomTypes[Math.floor(Math.random() * roomTypes.length)],
|
||||
checkIn: checkInDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
checkOut: checkOutDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
amenities: selectedAmenities,
|
||||
refundable: Math.random() > 0.5,
|
||||
pricePerNight: basePrice,
|
||||
nights,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default function HotelProducts() {
|
||||
const hotels = genRandomHotels();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Available Hotels</h1>
|
||||
<div className="space-y-4">
|
||||
{hotels.map((h) => (
|
||||
<HotelCard key={h.id} hotel={h} />
|
||||
))}
|
||||
</div>
|
||||
<Suspense fallback={<div className="text-center py-8">Loading...</div>}>
|
||||
<RoomsList />
|
||||
</Suspense>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button, Label, Input, DateInput, Dropdown, DropdownCounter } from '@/components/ui';
|
||||
import { dateToDaysFromToday } from '@/lib/hotel-utils';
|
||||
|
||||
const LocationIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -11,14 +13,25 @@ const LocationIcon = () => (
|
||||
);
|
||||
|
||||
export default function HotelHero() {
|
||||
const router = useRouter();
|
||||
const [destination, setDestination] = useState('');
|
||||
const [checkIn, setCheckIn] = useState('');
|
||||
const [checkOut, setCheckOut] = useState('');
|
||||
const [guests, setGuests] = useState({ adults: 2, rooms: 1 });
|
||||
|
||||
const handleSearch = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log({ destination, checkIn, checkOut, guests });
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (checkIn) {
|
||||
const daysOffset = dateToDaysFromToday(checkIn);
|
||||
params.set('dateIndex', daysOffset.toString());
|
||||
}
|
||||
|
||||
if (destination) params.set('destination', destination);
|
||||
params.set('adults', guests.adults.toString());
|
||||
params.set('rooms', guests.rooms.toString());
|
||||
|
||||
router.push(`/hotel/products?${params.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -26,16 +39,16 @@ export default function HotelHero() {
|
||||
<div className="w-full max-w-4xl px-4">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
Find your perfect stay
|
||||
Find your perfect room
|
||||
</h1>
|
||||
<p className="text-lg">
|
||||
Search hotels, compare prices, and book with confidence
|
||||
Search rooms, compare prices, and book with confidence
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="search-form">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="sm:col-span-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="destination">Where to?</Label>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -49,7 +62,7 @@ export default function HotelHero() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="checkIn">Check-in</Label>
|
||||
<Label htmlFor="checkIn">Date (1 night stay)</Label>
|
||||
<DateInput
|
||||
id="checkIn"
|
||||
value={checkIn}
|
||||
@@ -59,43 +72,27 @@ export default function HotelHero() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="checkOut">Check-out</Label>
|
||||
<DateInput
|
||||
id="checkOut"
|
||||
value={checkOut}
|
||||
onChange={(e) => setCheckOut(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 lg:col-span-4">
|
||||
<Label htmlFor="guests">Guests & Rooms</Label>
|
||||
<Dropdown label={`${guests.adults} ${guests.adults === 1 ? 'adult' : 'adults'}, ${guests.rooms} ${guests.rooms === 1 ? 'room' : 'rooms'}`}>
|
||||
<Label htmlFor="guests">Guests</Label>
|
||||
<Dropdown label={`${guests.adults} ${guests.adults === 1 ? 'adult' : 'adults'}`}>
|
||||
<DropdownCounter
|
||||
label="Adults"
|
||||
value={guests.adults}
|
||||
min={1}
|
||||
onChange={(v) => setGuests({ ...guests, adults: v })}
|
||||
/>
|
||||
<DropdownCounter
|
||||
label="Rooms"
|
||||
value={guests.rooms}
|
||||
min={1}
|
||||
onChange={(v) => setGuests({ ...guests, rooms: v })}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 lg:col-span-4">
|
||||
<div className="sm:col-span-2 lg:col-span-3">
|
||||
<Button type="submit" fullWidth>
|
||||
Search Hotels
|
||||
Search Rooms
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm">
|
||||
<p>Over 2 million hotels worldwide · Best price guarantee · Free cancellation on most bookings</p>
|
||||
<p>Over 2 million rooms worldwide · Best price guarantee · Free cancellation on most bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
69
web/src/lib/hotel-utils.ts
Normal file
69
web/src/lib/hotel-utils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface HotelProduct {
|
||||
id: string;
|
||||
room_type: string;
|
||||
date_index: number;
|
||||
metadata: {
|
||||
amenities?: string[];
|
||||
total?: number;
|
||||
image_url?: string;
|
||||
base_price?: number;
|
||||
name?: string;
|
||||
refundable?: boolean;
|
||||
};
|
||||
availability: number;
|
||||
}
|
||||
|
||||
export interface Hotel {
|
||||
id: string;
|
||||
name: string;
|
||||
roomType: string;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
amenities: string[];
|
||||
refundable: boolean;
|
||||
pricePerNight: number;
|
||||
nights: number;
|
||||
}
|
||||
|
||||
const EPOCH = new Date(0);
|
||||
|
||||
export const transformProduct = (p: HotelProduct): Hotel => {
|
||||
const { id, room_type, date_index, metadata } = p;
|
||||
const checkIn = new Date(EPOCH.getTime() + date_index * 86400000);
|
||||
const nights = 1;
|
||||
const checkOut = new Date(checkIn.getTime() + nights * 86400000);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: metadata?.name || room_type,
|
||||
roomType: room_type,
|
||||
checkIn: checkIn.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
checkOut: checkOut.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
amenities: metadata?.amenities || [],
|
||||
refundable: metadata?.refundable || false,
|
||||
pricePerNight: metadata?.base_price || 100,
|
||||
nights,
|
||||
};
|
||||
};
|
||||
|
||||
// convert date string to days from today
|
||||
export const dateToDaysFromToday = (dateStr: string): number => {
|
||||
const target = new Date(dateStr);
|
||||
target.setHours(0, 0, 0, 0);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return Math.floor((target.getTime() - today.getTime()) / 86400000);
|
||||
};
|
||||
|
||||
// convert date string to date_index (days since epoch)
|
||||
export const dateToIndex = (dateStr: string): number => {
|
||||
const d = new Date(dateStr);
|
||||
return Math.floor((d.getTime() - EPOCH.getTime()) / 86400000);
|
||||
};
|
||||
|
||||
// get current date_index
|
||||
export const todayIndex = (): number => {
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
return Math.floor((now.getTime() - EPOCH.getTime()) / 86400000);
|
||||
};
|
||||
Reference in New Issue
Block a user