feat: huge push of product changes and item review with cart

This commit is contained in:
2025-11-24 23:43:05 +01:00
parent b72d2610ed
commit bdc2ea86a2
17 changed files with 754 additions and 13 deletions

View File

@@ -197,16 +197,54 @@ def dump_logs(
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/products/{product_type}")
@app.get("/api/products/{product_id}")
async def get_product_by_id(product_id: str):
"""fetch single product by id from either hotel_products or airline_products"""
try:
supabase = get_supabase()
# try hotel_products first
response = supabase.table('hotel_products').select('*').eq('id', product_id).execute()
if response.data and len(response.data) > 0:
return {"success": True, "data": response.data[0]}
# try airline_products
response = supabase.table('airline_products').select('*').eq('id', product_id).execute()
if response.data and len(response.data) > 0:
return {"success": True, "data": response.data[0]}
raise HTTPException(status_code=404, detail="Product not found")
except HTTPException:
raise
except Exception as e:
import traceback
print(f"[PRODUCT_BY_ID_ERROR] {e}")
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/products/type/{product_type}")
async def get_products(
product_type: str,
dateIndex: Optional[int] = None
dateIndex: Optional[int] = None,
origin: Optional[str] = None,
destination: Optional[str] = None,
tripType: Optional[str] = None,
adults: Optional[int] = None,
children: Optional[int] = None,
infants: Optional[int] = None,
rooms: Optional[int] = None
):
"""fetch products from supabase based on type (hotel or airline)
params:
product_type: either 'hotel' or 'airline'
dateIndex: optional days offset from today (e.g., 0=today, 1=tomorrow, -1=yesterday)
origin: (airline) departure airport code
destination: (airline/hotel) arrival airport or hotel location
tripType: (airline) roundtrip, oneway, multicity
adults, children, infants: passenger counts
rooms: (hotel) number of rooms
"""
if product_type not in ['hotel', 'airline']:
raise HTTPException(status_code=400, detail="product_type must be 'hotel' or 'airline'")
@@ -222,8 +260,60 @@ async def get_products(
query = query.eq('date_index', dateIndex)
response = query.execute()
results = response.data
return {"success": True, "count": len(response.data), "data": response.data}
# apply in-memory filters based on metadata for airline products
if product_type == 'airline' and results:
filtered = []
for product in results:
metadata = product.get('metadata', {})
# filter by origin airport
if origin:
dep = metadata.get('departure', {})
if dep.get('airport') != origin:
continue
# filter by destination airport
if destination:
arr = metadata.get('arrival', {})
if arr.get('airport') != destination:
continue
# passenger count validation (ensure total capacity)
if adults is not None or children is not None or infants is not None:
total_pax = (adults or 0) + (children or 0) + (infants or 0)
avail = product.get('availability', 0)
if avail < total_pax:
continue
filtered.append(product)
results = filtered
# apply in-memory filters for hotel products
elif product_type == 'hotel' and results:
filtered = []
for product in results:
metadata = product.get('metadata', {})
# filter by occupancy capacity
if adults is not None:
max_occ = metadata.get('max_occupancy', 2)
if max_occ < adults:
continue
# filter by room availability
if rooms is not None:
avail = product.get('availability', 0)
if avail < rooms:
continue
filtered.append(product)
results = filtered
return {"success": True, "count": len(results), "data": results}
except Exception as e:
import traceback

View File

@@ -0,0 +1,106 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Navigation } from '@/components/ui';
import { useCart } from '@/contexts/CartContext';
import AirlineDetails from '@/components/feats/airline/AirlineDetails';
import { transformProduct, type Flight, type AirlineProduct } from '@/lib/airline-utils';
import type { EventName } from '@/lib/events';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
};
export default function AirlineProductPage() {
const params = useParams();
const router = useRouter();
const { addItem } = useCart();
const [product, setProduct] = useState<Flight | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [added, setAdded] = useState(false);
const productId = params.id as string;
useEffect(() => {
const fetchProduct = async () => {
try {
const res = await fetch(`/api/products/${productId}`);
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
const json = await res.json();
const transformed = transformProduct(json.data as AirlineProduct);
setProduct(transformed);
// fire learn_more_about_item event when product loads
dispatchInteraction('learn_more_about_item', productId, {
type: 'airline',
dateIndex: transformed.dateIndex,
flightType: transformed.flightType,
});
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load product');
console.error('[FETCH_FLIGHT_ERROR]', e);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
const handleAddToCart = () => {
if (!product) return;
addItem({
id: productId,
type: 'airline',
name: product.flightType,
price: product.basePrice,
metadata: {
departure: product.departure,
arrival: product.arrival,
duration: product.duration,
cabinClass: product.cabinClass,
},
dateIndex: product.dateIndex,
});
dispatchInteraction('add_item_to_cart', productId, {
type: 'airline',
price: product.basePrice,
});
setAdded(true);
setTimeout(() => setAdded(false), 2000);
};
return (
<>
<Navigation />
<main className="max-w-4xl mx-auto px-4 py-8">
{loading && <div className="text-center py-8">Loading flight details...</div>}
{error && <div className="text-red-500 text-center py-8">{error}</div>}
{!loading && !error && product && (
<>
<AirlineDetails
product={product}
onAddToCart={handleAddToCart}
addedToCart={added}
/>
<button
onClick={() => router.back()}
className="mt-6 text-blue-600 hover:underline"
>
Back to flights
</button>
</>
)}
</main>
</>
);
}

View File

@@ -18,8 +18,12 @@ function FlightsList() {
const url = new URL('/api/products', window.location.origin);
url.searchParams.set('type', 'airline');
const dateIndex = searchParams.get('dateIndex');
if (dateIndex) url.searchParams.set('dateIndex', dateIndex);
// forward all relevant search params to the API
const params = ['dateIndex', 'origin', 'destination', 'tripType', 'adults', 'children', 'infants'];
params.forEach(param => {
const val = searchParams.get(param);
if (val) url.searchParams.set(param, val);
});
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);

View File

@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
if (!id) {
return NextResponse.json(
{ error: 'product id is required' },
{ status: 400 }
);
}
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:5000';
const url = new URL(`${backendUrl}/api/products/${id}`);
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('[PRODUCT_DETAIL_ERROR]', error);
return NextResponse.json(
{ error: 'Failed to fetch product details' },
{ status: 500 }
);
}
}

View File

@@ -13,11 +13,14 @@ export async function GET(req: NextRequest) {
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:5000';
const url = new URL(`${backendUrl}/api/products/${type}`);
const url = new URL(`${backendUrl}/api/products/type/${type}`);
// forward date index offset to backend
const dateIndex = searchParams.get('dateIndex');
if (dateIndex) url.searchParams.set('dateIndex', dateIndex);
// forward all query params to backend (excluding 'type')
searchParams.forEach((value, key) => {
if (key !== 'type') {
url.searchParams.set(key, value);
}
});
const res = await fetch(url.toString());

110
web/src/app/cart/page.tsx Normal file
View File

@@ -0,0 +1,110 @@
'use client';
import { Navigation } from '@/components/ui';
import { useCart } from '@/contexts/CartContext';
import type { EventName } from '@/lib/events';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
};
export default function CartPage() {
const { items, removeItem, clearCart, itemCount } = useCart();
const handleRemove = (id: string, type: string) => {
removeItem(id);
dispatchInteraction('remove_item', id, { type });
};
let itemTypes = Array.from(new Set(items.map(item => item.type)))[0] || 'items';
const total = items.reduce((sum, item) => sum + item.price, 0);
return (
<>
<Navigation />
<main className="max-w-4xl mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Shopping Cart</h1>
{itemCount > 0 && (
<button
onClick={clearCart}
className="text-sm text-red-600 hover:underline"
>
Clear cart
</button>
)}
</div>
{itemCount === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 mb-4">Your cart is empty</p>
<a href="/" className="text-blue-600 hover:underline">Browse our selection</a>
</div>
) : (
<>
<div className="space-y-4 mb-8">
{items.map(item => (
<div
key={item.id}
className="flex justify-between items-start p-4 border rounded-lg hover:bg-gray-50"
>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="px-2 py-0.5 text-xs font-medium rounded bg-blue-100 text-blue-800">
{item.type}
</span>
<h3 className="font-semibold">{item.name}</h3>
</div>
{item.type === 'hotel' && (
<div className="text-sm text-gray-600">
<p>{item.metadata.roomType as string}</p>
<p>{item.metadata.checkIn as string} - {item.metadata.checkOut as string}</p>
<p>{item.metadata.nights} night{(item.metadata.nights as number) > 1 ? 's' : ''}</p>
</div>
)}
{item.type === 'airline' && (
<div className="text-sm text-gray-600">
<p>{item.metadata.cabinClass as string} Class</p>
<p>{(item.metadata.departure as any).airport} {(item.metadata.arrival as any).airport}</p>
<p>Duration: {item.metadata.duration as string}</p>
</div>
)}
</div>
<div className="text-right ml-4">
<p className="text-xl font-bold mb-2">${item.price}</p>
<button
onClick={() => handleRemove(item.id, item.type)}
className="text-sm text-red-600 hover:underline"
>
Remove
</button>
</div>
</div>
))}
</div>
<div className="border-t pt-4">
<div className="flex justify-between items-center mb-4">
<span className="text-xl font-semibold">Total</span>
<span className="text-3xl font-bold">${total.toFixed(2)}</span>
</div>
<button
onClick={() => dispatchInteraction('checkout_start', undefined, { total, itemCount })}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"
>
Proceed to Checkout
</button>
</div>
</>
)}
</main>
</>
);
}

View File

@@ -0,0 +1,106 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { Navigation } from '@/components/ui';
import { useCart } from '@/contexts/CartContext';
import HotelDetails from '@/components/feats/hotel/HotelDetails';
import { transformProduct, type Hotel, type HotelProduct } from '@/lib/hotel-utils';
import type { EventName } from '@/lib/events';
const dispatchInteraction = (eventName: EventName, productId?: string, metadata?: Record<string, unknown>) => {
const e = new CustomEvent('definedInteraction', {
detail: { eventName, productId, metadata },
});
document.dispatchEvent(e);
};
export default function HotelProductPage() {
const params = useParams();
const router = useRouter();
const { addItem } = useCart();
const [product, setProduct] = useState<Hotel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [added, setAdded] = useState(false);
const productId = params.id as string;
useEffect(() => {
const fetchProduct = async () => {
try {
const res = await fetch(`/api/products/${productId}`);
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
const json = await res.json();
const transformed = transformProduct(json.data as HotelProduct);
setProduct(transformed);
// fire learn_more_about_item event when product loads
dispatchInteraction('learn_more_about_item', productId, {
type: 'hotel',
dateIndex: transformed.dateIndex,
roomType: transformed.roomType,
});
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load product');
console.error('[FETCH_HOTEL_ERROR]', e);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
const handleAddToCart = () => {
if (!product) return;
addItem({
id: productId,
type: 'hotel',
name: product.name,
price: product.pricePerNight,
metadata: {
roomType: product.roomType,
nights: product.nights,
checkIn: product.checkIn,
checkOut: product.checkOut,
},
dateIndex: product.dateIndex,
});
dispatchInteraction('add_item_to_cart', productId, {
type: 'hotel',
price: product.pricePerNight,
});
setAdded(true);
setTimeout(() => setAdded(false), 2000);
};
return (
<>
<Navigation />
<main className="max-w-4xl mx-auto px-4 py-8">
{loading && <div className="text-center py-8">Loading hotel details...</div>}
{error && <div className="text-red-500 text-center py-8">{error}</div>}
{!loading && !error && product && (
<>
<HotelDetails
product={product}
onAddToCart={handleAddToCart}
addedToCart={added}
/>
<button
onClick={() => router.back()}
className="mt-6 text-blue-600 hover:underline"
>
Back to rooms
</button>
</>
)}
</main>
</>
);
}

View File

@@ -18,8 +18,12 @@ function RoomsList() {
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);
// forward all relevant search params to the API
const params = ['dateIndex', 'destination', 'adults', 'rooms'];
params.forEach(param => {
const val = searchParams.get(param);
if (val) url.searchParams.set(param, val);
});
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { TrackingProvider } from "@/components/TrackingProvider";
import { CartProvider } from "@/contexts/CartContext";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -28,7 +29,9 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<CartProvider>
<TrackingProvider>{children}</TrackingProvider>
</CartProvider>
</body>
</html>
);

View File

@@ -32,6 +32,7 @@ export default function AirlineCard({ flight }: { flight: Flight }) {
price: flight.basePrice,
dateIndex: flight.dateIndex,
});
window.location.href = `/airline/products/${flight.id}`;
};
return (

View File

@@ -0,0 +1,83 @@
'use client';
import type { Flight } from '@/lib/airline-utils';
interface AirlineDetailsProps {
product: Flight;
onAddToCart: () => void;
addedToCart: boolean;
}
export default function AirlineDetails({ product, onAddToCart, addedToCart }: AirlineDetailsProps) {
return (
<div className="space-y-6">
<div className="bg-gray-100 h-64 flex items-center justify-center rounded-lg">
<span className="text-gray-400">Flight Image</span>
</div>
<div>
<h1 className="text-3xl font-bold mb-2">{product.flightType}</h1>
<p className="text-lg text-gray-600">{product.cabinClass} Class</p>
</div>
<div className="grid grid-cols-2 gap-6">
<div>
<h3 className="font-semibold mb-2">Departure</h3>
<p className="text-xl font-bold">{product.departure.time}</p>
<p className="text-gray-600">{product.departure.airport}</p>
</div>
<div>
<h3 className="font-semibold mb-2">Arrival</h3>
<p className="text-xl font-bold">{product.arrival.time}</p>
<p className="text-gray-600">{product.arrival.airport}</p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 py-4 border-y">
<div>
<p className="text-sm text-gray-600">Duration</p>
<p className="font-semibold">{product.duration}</p>
</div>
<div>
<p className="text-sm text-gray-600">Stops</p>
<p className="font-semibold">
{product.stops === 0 ? 'Nonstop' : `${product.stops} stop${product.stops > 1 ? 's' : ''}`}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Availability</p>
<p className="font-semibold">{product.availability} seats</p>
</div>
</div>
<div>
<h3 className="font-semibold mb-2">Fare Details</h3>
<p className="text-sm text-gray-600 mb-1">{product.fareRule}</p>
{product.refundable && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mt-2">
<p className="text-green-800 text-sm font-medium">Refundable fare</p>
</div>
)}
</div>
<div className="border-t pt-4">
<div className="flex justify-between items-center">
<div>
<p className="text-sm text-gray-600">Total price</p>
<p className="text-3xl font-bold">${product.basePrice}</p>
</div>
</div>
</div>
<div className="flex gap-4">
<button
onClick={onAddToCart}
disabled={addedToCart}
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-green-600 text-white rounded-lg font-medium transition-colors"
>
{addedToCart ? 'Added to Cart!' : 'Add to Cart'}
</button>
</div>
</div>
);
}

View File

@@ -1,7 +1,9 @@
'use client';
import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
import { Button, Label, Input, DateInput, RadioGroup, Dropdown, DropdownCounter } from '@/components/ui';
import { dateToDaysFromToday } from '@/lib/airline-utils';
type TripType = 'roundtrip' | 'oneway' | 'multicity';
@@ -19,6 +21,7 @@ const LocationIcon = () => (
);
export default function AirlineHero() {
const router = useRouter();
const [tripType, setTripType] = useState<TripType>('roundtrip');
const [origin, setOrigin] = useState('');
const [destination, setDestination] = useState('');
@@ -28,7 +31,23 @@ export default function AirlineHero() {
const handleSearch = (e: FormEvent) => {
e.preventDefault();
console.log({ tripType, origin, destination, departDate, returnDate, passengers });
const params = new URLSearchParams();
if (departDate) {
const daysOffset = dateToDaysFromToday(departDate);
params.set('dateIndex', daysOffset.toString());
}
if (origin) params.set('origin', origin);
if (destination) params.set('destination', destination);
if (tripType !== 'roundtrip') params.set('tripType', tripType);
if (returnDate && tripType === 'roundtrip') params.set('returnDate', returnDate);
params.set('adults', passengers.adults.toString());
params.set('children', passengers.children.toString());
params.set('infants', passengers.infants.toString());
router.push(`/airline/products?${params.toString()}`);
};
const totalPax = passengers.adults + passengers.children + passengers.infants;

View File

@@ -44,6 +44,7 @@ export default function HotelCard({ hotel }: { hotel: Hotel }) {
nights: hotel.nights,
dateIndex: hotel.dateIndex,
});
window.location.href = `/hotel/products/${hotel.id}`;
};
return (

View File

@@ -0,0 +1,74 @@
'use client';
import type { Hotel } from '@/lib/hotel-utils';
interface HotelDetailsProps {
product: Hotel;
onAddToCart: () => void;
addedToCart: boolean;
}
export default function HotelDetails({ product, onAddToCart, addedToCart }: HotelDetailsProps) {
return (
<div className="space-y-6">
<div className="bg-gray-100 h-64 flex items-center justify-center rounded-lg">
<span className="text-gray-400">Hotel Image</span>
</div>
<div>
<h1 className="text-3xl font-bold mb-2">{product.name}</h1>
<p className="text-lg text-gray-600">{product.roomType}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="font-semibold mb-1">Check-in</h3>
<p>{product.checkIn}</p>
</div>
<div>
<h3 className="font-semibold mb-1">Check-out</h3>
<p>{product.checkOut}</p>
</div>
</div>
<div>
<h3 className="font-semibold mb-2">Amenities</h3>
<div className="flex flex-wrap gap-2">
{product.amenities.map(a => (
<span key={a} className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">
{a}
</span>
))}
</div>
</div>
{product.refundable && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-green-800 font-medium">Free cancellation available</p>
</div>
)}
<div className="border-t pt-4">
<div className="flex justify-between items-center">
<div>
<p className="text-sm text-gray-600">Price per night</p>
<p className="text-3xl font-bold">${product.pricePerNight}</p>
<p className="text-sm text-gray-500">
Total for {product.nights} night{product.nights > 1 ? 's' : ''}: ${product.pricePerNight * product.nights}
</p>
</div>
</div>
</div>
<div className="flex gap-4">
<button
onClick={onAddToCart}
disabled={addedToCart}
className="w-full px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-green-600 text-white rounded-lg font-medium transition-colors"
>
{addedToCart ? 'Added to Cart!' : 'Add to Cart'}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
export interface CartItem {
id: string;
type: 'hotel' | 'airline';
name: string;
price: number;
metadata: Record<string, unknown>;
dateIndex: number;
}
interface CartContextType {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
clearCart: () => void;
itemCount: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
const CART_KEY = 'phantom_cart';
export const CartProvider = ({ children }: { children: ReactNode }) => {
const [items, setItems] = useState<CartItem[]>([]);
const [loaded, setLoaded] = useState(false);
// load cart from sessionStorage on mount
useEffect(() => {
const stored = sessionStorage.getItem(CART_KEY);
if (stored) {
try {
setItems(JSON.parse(stored));
} catch (e) {
console.error('[CART_LOAD]', e);
}
}
setLoaded(true);
}, []);
// persist to sessionStorage whenever cart changes
useEffect(() => {
if (!loaded) return;
sessionStorage.setItem(CART_KEY, JSON.stringify(items));
}, [items, loaded]);
const addItem = (item: CartItem) => {
setItems(prev => {
// prevent duplicates
if (prev.find(i => i.id === item.id)) return prev;
return [...prev, item];
});
};
const removeItem = (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
};
const clearCart = () => {
setItems([]);
};
return (
<CartContext.Provider value={{ items, addItem, removeItem, clearCart, itemCount: items.length }}>
{children}
</CartContext.Provider>
);
};
export const useCart = () => {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
};

View File

@@ -0,0 +1,25 @@
import { HotelProduct, Hotel, transformProduct as transformHotel } from './hotel-utils';
import { AirlineProduct, Flight, transformProduct as transformFlight } from './airline-utils';
export type Product = Hotel | Flight;
export type ProductRaw = HotelProduct | AirlineProduct;
export const isHotelProduct = (p: ProductRaw): p is HotelProduct => {
return 'room_type' in p;
};
export const isAirlineProduct = (p: ProductRaw): p is AirlineProduct => {
return 'flight_type' in p;
};
export const transformProduct = (p: ProductRaw): Product => {
if (isHotelProduct(p)) {
return transformHotel(p);
}
return transformFlight(p);
};
export const getProductType = (p: Product): 'hotel' | 'airline' => {
if ('roomType' in p) return 'hotel';
return 'airline';
};

View File

@@ -11,6 +11,7 @@ export function proxy(req: NextRequest) {
pathname.startsWith('/_next') ||
pathname.startsWith('/static') ||
pathname.startsWith('/start-task') ||
pathname.startsWith('/cart') ||
pathname.includes('.')
// TODO: add robots.txt and sitemap.xml if needed here
) {