Files
edr-platform/apps/edr-passenger-web/portal/src/components/SearchWidget.tsx

280 lines
14 KiB
TypeScript

'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api-client';
import { useBookingStore } from '@/lib/booking-store';
import { Station } from '@/types';
import { MapPin, Users, Search, Plus, Minus, ChevronDown } from 'lucide-react';
import { useState } from 'react';
import ModernDatePicker from '@/components/ModernDatePicker';
const searchSchema = z.object({
originStationId: z.string().min(1),
destinationStationId: z.string().min(1),
departureDate: z.string().min(1),
adultCount: z.number().min(1).max(9),
childCount: z.number().min(0).max(9),
nationality: z.enum(['ETHIOPIAN', 'DJIBOUTIAN', 'OTHER']),
}).refine((data) => data.originStationId !== data.destinationStationId, {
message: 'Origin and destination must be different',
path: ['destinationStationId'],
});
type SearchForm = z.infer<typeof searchSchema>;
interface SearchWidgetProps {
fullWidth?: boolean;
onSearch?: () => void;
}
export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps) {
const router = useRouter();
const setSearchCriteria = useBookingStore((s) => s.setSearchCriteria);
const [isPassengerOpen, setIsPassengerOpen] = useState(false);
const { data: stations, isLoading } = useQuery<Station[]>({
queryKey: ['stations'],
queryFn: async () => await apiClient.get('/stations') as Station[],
});
const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm<SearchForm>({
resolver: zodResolver(searchSchema),
defaultValues: {
adultCount: 1,
childCount: 0,
nationality: 'ETHIOPIAN',
departureDate: new Date().toISOString().split('T')[0],
},
});
const originId = watch('originStationId');
const adultCount = watch('adultCount');
const childCount = watch('childCount');
const onSubmit = (data: SearchForm) => {
setSearchCriteria({
...data,
adultCount: data.adultCount,
childCount: data.childCount,
nationality: data.nationality,
});
const params = new URLSearchParams({
origin: data.originStationId,
destination: data.destinationStationId,
date: data.departureDate,
adults: data.adultCount.toString(),
children: data.childCount.toString(),
nationality: data.nationality,
});
if (onSearch) onSearch();
router.push(`/booking/results?${params}`);
};
return (
<div className={fullWidth ? 'w-full' : 'w-full max-w-6xl mx-auto'}>
<form onSubmit={handleSubmit(onSubmit)} className="bg-white/95 dark:bg-gray-800/95 rounded-2xl shadow-lg border border-gray-200/20 dark:border-gray-700/20 overflow-visible backdrop-blur-sm">
<div className="p-6 md:p-8 overflow-visible">
{/* Row 1: From, To, Date */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end mb-4">
{/* From */}
<div className="space-y-2 md:col-span-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">From</label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-primary" />
<select
{...register('originStationId')}
className="w-full pl-11 pr-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
disabled={isLoading}
>
<option value="">Select departure</option>
{stations?.map((s) => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</div>
{errors.originStationId && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.originStationId.message}</p>
)}
</div>
{/* To */}
<div className="space-y-2 md:col-span-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">To</label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-primary" />
<select
{...register('destinationStationId')}
className="w-full pl-11 pr-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent disabled:bg-gray-50 dark:disabled:bg-gray-800 disabled:cursor-not-allowed text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
disabled={isLoading}
>
<option value="">Select arrival</option>
{stations?.map((s) => (
<option key={s.id} value={s.id} disabled={s.id === originId}>{s.name}</option>
))}
</select>
</div>
{errors.destinationStationId && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.destinationStationId.message}</p>
)}
</div>
{/* Date */}
<div className="space-y-2 relative z-30 md:col-span-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Date</label>
<ModernDatePicker
value={watch('departureDate') ? new Date(watch('departureDate') + 'T00:00:00') : undefined}
onChange={(date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
setValue('departureDate', `${year}-${month}-${day}`);
}}
minDate={new Date()}
placeholder="Select date"
/>
{errors.departureDate && (
<p className="text-red-600 dark:text-red-400 text-sm">{errors.departureDate.message}</p>
)}
</div>
</div>
{/* Row 2: Passengers, Nationality, Promo Code */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end mb-4">
{/* Passengers Dropdown */}
<div className="space-y-2 relative z-20">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Passengers</label>
<button
type="button"
onClick={() => setIsPassengerOpen(!isPassengerOpen)}
className="w-full px-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 flex items-center justify-between hover:border-primary transition-colors"
>
<span className="flex items-center gap-2">
<Users className="w-4 h-4 text-primary" />
{(adultCount || 1) + (childCount || 0)} Passenger{((adultCount || 1) + (childCount || 0)) !== 1 ? 's' : ''}
</span>
<ChevronDown className={`w-4 h-4 transition-transform text-primary ${isPassengerOpen ? 'rotate-180' : ''}`} />
</button>
{/* Passenger Dropdown Menu */}
{isPassengerOpen && (
<>
<div className="fixed inset-0 z-40" onClick={() => setIsPassengerOpen(false)} />
<div className="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg z-50 p-4 space-y-4">
{/* Adults */}
<div>
<div className="flex items-center justify-between mb-2">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Adults</div>
<div className="text-xs text-gray-500 dark:text-gray-400">5 years</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const current = adultCount || 1;
if (current > 1) setValue('adultCount', current - 1);
}}
className="w-7 h-7 rounded-full border border-gray-300 dark:border-gray-600 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
disabled={(adultCount || 1) <= 1}
>
<Minus className="w-4 h-4 text-primary" />
</button>
<span className="w-6 text-center font-semibold text-gray-900 dark:text-gray-100">{adultCount || 1}</span>
<button
type="button"
onClick={() => {
const current = adultCount || 1;
if (current < 9) setValue('adultCount', current + 1);
}}
className="w-7 h-7 rounded-full border border-gray-300 dark:border-gray-600 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
disabled={(adultCount || 1) >= 9}
>
<Plus className="w-4 h-4 text-primary" />
</button>
</div>
</div>
</div>
{/* Children */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Children</div>
<div className="text-xs text-gray-500 dark:text-gray-400">&lt;5 years First free</div>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => {
const current = childCount || 0;
if (current > 0) setValue('childCount', current - 1);
}}
className="w-7 h-7 rounded-full border border-gray-300 dark:border-gray-600 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
disabled={(childCount || 0) <= 0}
>
<Minus className="w-4 h-4 text-primary" />
</button>
<span className="w-6 text-center font-semibold text-gray-900 dark:text-gray-100">{childCount || 0}</span>
<button
type="button"
onClick={() => {
const current = childCount || 0;
if (current < 9) setValue('childCount', current + 1);
}}
className="w-7 h-7 rounded-full border border-gray-300 dark:border-gray-600 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50"
disabled={(childCount || 0) >= 9}
>
<Plus className="w-4 h-4 text-primary" />
</button>
</div>
</div>
</div>
</div>
</>
)}
</div>
{/* Nationality */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Nationality</label>
<select
{...register('nationality')}
className="w-full px-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="ETHIOPIAN">Ethiopian</option>
<option value="DJIBOUTIAN">Djiboutian</option>
<option value="OTHER">Other</option>
</select>
</div>
{/* Promo Code */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Promo Code (Optional)</label>
<input
type="text"
placeholder="Enter promo code"
className="w-full px-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-base bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/>
</div>
</div>
{/* Row 3: Search Button */}
<div>
<button
type="submit"
className="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-3.5 px-6 rounded-lg transition-all duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl"
>
<Search className="w-5 h-5 text-white" />
<span>Search Train</span>
</button>
</div>
</div>
</form>
</div>
);
}