mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 04:48:18 +00:00
Production cleanups
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Mail, Phone, MapPin } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useLanguage, getTranslation, Language } from '@/lib/i18n';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -21,83 +20,27 @@ export function Footer() {
|
||||
<footer className="bg-[rgb(20_113_76)] dark:bg-gray-900 text-white py-12">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
|
||||
{/* Company Info */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">
|
||||
Ethio-Djibouti Railway
|
||||
</h4>
|
||||
<p className="text-sm text-gray-100 my-3">Connecting East Africa with train travel.
|
||||
</p>
|
||||
<h4 className="font-semibold mb-2">Ethio-Djibouti Railway</h4>
|
||||
<p className="text-sm text-gray-100 my-3">Connecting East Africa with train travel.</p>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2 text-gray-100">
|
||||
<Phone className="w-4 h-4" />
|
||||
<a href="tel:9546" className="hover:text-gray-200 transition">
|
||||
9546
|
||||
</a>
|
||||
<Phone className="w-4 h-4 flex-shrink-0" />
|
||||
<a href="tel:9546" className="hover:text-gray-200 transition">9546</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-100">
|
||||
<Mail className="w-4 h-4" />
|
||||
<a href="mailto:edr_@edrsc.com" className="hover:text-gray-200 transition">
|
||||
edr_@edrsc.com
|
||||
</a>
|
||||
<Mail className="w-4 h-4 flex-shrink-0" />
|
||||
<a href="mailto:edr_@edrsc.com" className="hover:text-gray-200 transition">edr_@edrsc.com</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-gray-100">
|
||||
<MapPin className="w-4 h-4" />
|
||||
<MapPin className="w-4 h-4 flex-shrink-0" />
|
||||
<span>Furi, Sheger City</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">{t('nav.home')}</h4>
|
||||
<ul className="space-y-2 text-sm text-gray-100">
|
||||
<li>
|
||||
<Link href="/" className="hover:text-gray-200 transition">
|
||||
{t('nav.home')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/services" className="hover:text-gray-200 transition">
|
||||
{t('nav.services')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about" className="hover:text-gray-200 transition">
|
||||
{t('nav.about')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" className="hover:text-gray-200 transition">
|
||||
{t('nav.contact')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Support */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">{t('footer.support')}</h4>
|
||||
<ul className="space-y-2 text-sm text-gray-100">
|
||||
<li>
|
||||
<Link href="/help" className="hover:text-gray-200 transition">
|
||||
{t('nav.help')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-gray-200 transition">
|
||||
{t('footer.privacy')}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-gray-200 transition">
|
||||
{t('footer.terms')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Social Media */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">{t('footer.follow')}</h4>
|
||||
@@ -118,21 +61,10 @@ export function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-white border-opacity-20 dark:border-gray-700 pt-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center text-sm text-gray-100">
|
||||
<p>
|
||||
© 2024 Ethio-Djibouti Railway. {t('footer.rights')}
|
||||
</p>
|
||||
<div className="flex gap-6 mt-4 md:mt-0">
|
||||
<a href="#" className="hover:text-gray-200 transition">
|
||||
{t('footer.privacy')}
|
||||
</a>
|
||||
<a href="#" className="hover:text-gray-200 transition">
|
||||
{t('footer.terms')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-white border-opacity-20 dark:border-gray-700 pt-6">
|
||||
<p className="text-sm text-gray-100 text-center md:text-left">
|
||||
© {new Date().getFullYear()} Ethio-Djibouti Railway. {t('footer.rights')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -277,15 +277,15 @@ function FeaturedCard({ pkg }: { pkg: HolidayPackage }) {
|
||||
</div>
|
||||
|
||||
{/* Bottom: price + CTA */}
|
||||
<div className="flex items-end justify-between pt-5 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-3 pt-5 border-t border-gray-100 dark:border-gray-800">
|
||||
{price ? (
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-400 uppercase tracking-wider font-medium">
|
||||
Starting from
|
||||
</p>
|
||||
<p className="text-3xl font-extrabold text-primary leading-none mt-1">
|
||||
<p className="text-2xl font-extrabold text-primary leading-none mt-1">
|
||||
{price.currency}{" "}
|
||||
<span className="text-2xl">
|
||||
<span className="text-xl">
|
||||
{price.amount.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
@@ -300,7 +300,7 @@ function FeaturedCard({ pkg }: { pkg: HolidayPackage }) {
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<span className="inline-flex items-center gap-2 bg-[rgb(20,113,76)] group-hover:bg-[rgb(16,89,60)] text-white text-sm font-bold px-6 py-3.5 rounded-xl shadow-lg group-hover:shadow-xl transition-all duration-200 group-hover:gap-3">
|
||||
<span className="inline-flex items-center justify-center gap-2 bg-[rgb(20,113,76)] group-hover:bg-[rgb(16,89,60)] text-white text-sm font-bold px-5 py-3 rounded-xl shadow-lg group-hover:shadow-xl transition-all duration-200 group-hover:gap-3 w-full sm:w-auto">
|
||||
View Package
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
@@ -384,20 +384,20 @@ function PackageCard({ pkg }: { pkg: HolidayPackage }) {
|
||||
</div>
|
||||
|
||||
{/* Price + CTA */}
|
||||
<div className="mt-auto pt-3.5 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between">
|
||||
<div className="mt-auto pt-3.5 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between gap-2">
|
||||
{price ? (
|
||||
<div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] text-gray-400 uppercase tracking-wide">
|
||||
From
|
||||
</p>
|
||||
<p className="text-base font-extrabold text-primary">
|
||||
<p className="text-sm font-extrabold text-primary truncate">
|
||||
{fmtPrice(price.amount * 100, price.currency)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<span className="text-xs text-primary font-bold flex items-center gap-1 group-hover:gap-2 transition-all">
|
||||
<span className="flex-shrink-0 text-xs text-white bg-primary font-bold flex items-center gap-1 group-hover:gap-2 transition-all px-3 py-2 rounded-lg">
|
||||
View <ArrowRight className="w-3.5 h-3.5" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,22 @@ 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 { MapPin, Users, Search, Plus, Minus, ChevronDown, Globe } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
function useDarkMode() {
|
||||
const [dark, setDark] = useState(() =>
|
||||
typeof window !== 'undefined' && document.documentElement.classList.contains('dark')
|
||||
);
|
||||
useEffect(() => {
|
||||
const obs = new MutationObserver(() =>
|
||||
setDark(document.documentElement.classList.contains('dark'))
|
||||
);
|
||||
obs.observe(document.documentElement, { attributeFilter: ['class'] });
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
return dark;
|
||||
}
|
||||
import ModernDatePicker from '@/components/ModernDatePicker';
|
||||
|
||||
const searchSchema = z.object({
|
||||
@@ -36,18 +50,104 @@ interface SearchWidgetProps {
|
||||
onSearch?: () => void;
|
||||
}
|
||||
|
||||
const NATIONALITIES = [
|
||||
{ value: 'ETHIOPIAN', label: 'Ethiopian' },
|
||||
{ value: 'DJIBOUTIAN', label: 'Djiboutian' },
|
||||
{ value: 'OTHER', label: 'Other' },
|
||||
] as const;
|
||||
|
||||
// Reusable custom dropdown
|
||||
function CustomSelect({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
placeholder,
|
||||
icon,
|
||||
error,
|
||||
disabled,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
options: { value: string; label: string; disabled?: boolean }[];
|
||||
placeholder: string;
|
||||
icon?: React.ReactNode;
|
||||
error?: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const selected = options.find((o) => o.value === value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
const dark = useDarkMode();
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => !disabled && setOpen((o) => !o)}
|
||||
className={`w-full ${icon ? 'pl-11' : 'pl-4'} pr-10 py-3.5 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-base text-left flex items-center gap-2
|
||||
bg-white dark:bg-gray-800
|
||||
disabled:opacity-60 disabled:cursor-not-allowed
|
||||
${error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}
|
||||
transition-colors`}
|
||||
style={{ color: dark ? '#ffffff' : '#111827' }}
|
||||
>
|
||||
{icon && <span className="absolute left-3 top-1/2 -translate-y-1/2">{icon}</span>}
|
||||
<span style={{ color: selected ? (dark ? '#ffffff' : '#111827') : (dark ? '#6b7280' : '#9ca3af') }}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<ChevronDown className={`absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-400 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl z-50 max-h-60 overflow-y-auto">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
disabled={opt.disabled}
|
||||
onClick={() => { onChange(opt.value); setOpen(false); }}
|
||||
className={`w-full text-left px-4 py-3 text-sm transition-colors
|
||||
${opt.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer'}
|
||||
${opt.value === value
|
||||
? 'text-primary font-semibold bg-primary/5 dark:bg-primary/10'
|
||||
: 'text-gray-900 dark:text-white'
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps) {
|
||||
const router = useRouter();
|
||||
const setSearchCriteria = useBookingStore((s) => s.setSearchCriteria);
|
||||
const [isPassengerOpen, setIsPassengerOpen] = useState(false);
|
||||
const dark = useDarkMode();
|
||||
|
||||
const { data: stations, isLoading } = useQuery<Station[]>({
|
||||
queryKey: ['stations'],
|
||||
queryFn: async () => await apiClient.get('/stations') as Station[],
|
||||
});
|
||||
|
||||
const { register, handleSubmit, watch, setValue, clearErrors, formState: { errors } } = useForm<SearchForm>({
|
||||
// @ts-ignore - ZodEffects type compatibility issue
|
||||
const { handleSubmit, watch, setValue, clearErrors, formState: { errors } } = useForm<SearchForm>({
|
||||
// @ts-ignore
|
||||
resolver: zodResolver(searchSchema),
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
@@ -63,16 +163,16 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
|
||||
});
|
||||
|
||||
const originId = watch('originStationId');
|
||||
const destinationId = watch('destinationStationId');
|
||||
const nationality = watch('nationality');
|
||||
const adultCount = watch('adultCount');
|
||||
const childCount = watch('childCount');
|
||||
|
||||
const stationOptions = (stations ?? []).map((s) => ({ value: s.id, label: s.name }));
|
||||
const destinationOptions = stationOptions.map((s) => ({ ...s, disabled: s.value === originId }));
|
||||
|
||||
const onSubmit = (data: SearchForm) => {
|
||||
setSearchCriteria({
|
||||
...data,
|
||||
adultCount: data.adultCount,
|
||||
childCount: data.childCount,
|
||||
nationality: data.nationality,
|
||||
});
|
||||
setSearchCriteria({ ...data });
|
||||
const params = new URLSearchParams({
|
||||
origin: data.originStationId,
|
||||
destination: data.destinationStationId,
|
||||
@@ -89,172 +189,114 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
|
||||
<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', {
|
||||
onChange: (e) => {
|
||||
if (e.target.value) clearErrors('originStationId');
|
||||
}
|
||||
})}
|
||||
className={`w-full pl-11 pr-4 py-3.5 border 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 ${
|
||||
errors.originStationId ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<option value="">Select departure</option>
|
||||
{stations?.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">From</label>
|
||||
<CustomSelect
|
||||
value={originId}
|
||||
onChange={(val) => { setValue('originStationId', val); clearErrors('originStationId'); }}
|
||||
options={stationOptions}
|
||||
placeholder="Select departure"
|
||||
icon={<MapPin className="w-5 h-5 text-primary" />}
|
||||
error={!!errors.originStationId}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.originStationId && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.originStationId.message}</p>
|
||||
<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', {
|
||||
onChange: (e) => {
|
||||
if (e.target.value) clearErrors('destinationStationId');
|
||||
}
|
||||
})}
|
||||
className={`w-full pl-11 pr-4 py-3.5 border 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 ${
|
||||
errors.destinationStationId ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
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>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">To</label>
|
||||
<CustomSelect
|
||||
value={destinationId}
|
||||
onChange={(val) => { setValue('destinationStationId', val); clearErrors('destinationStationId'); }}
|
||||
options={destinationOptions}
|
||||
placeholder="Select arrival"
|
||||
icon={<MapPin className="w-5 h-5 text-primary" />}
|
||||
error={!!errors.destinationStationId}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.destinationStationId && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.destinationStationId.message}</p>
|
||||
<p className="text-red-600 dark:text-red-400 text-sm">{errors.destinationStationId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<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">Date</label>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">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}`);
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
setValue('departureDate', `${y}-${m}-${d}`);
|
||||
clearErrors('departureDate');
|
||||
}}
|
||||
minDate={new Date()}
|
||||
placeholder="Select date"
|
||||
/>
|
||||
{errors.departureDate && (
|
||||
<p className="text-red-600 dark:text-red-400 text-sm mt-1">{errors.departureDate.message}</p>
|
||||
<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 */}
|
||||
{/* Passengers */}
|
||||
<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>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">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"
|
||||
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 text-base bg-white dark:bg-gray-800 flex items-center justify-between hover:border-primary transition-colors"
|
||||
style={{ color: dark ? '#ffffff' : '#111827' }}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="flex items-center gap-2" style={{ color: 'inherit' }}>
|
||||
<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' : ''}`} />
|
||||
<ChevronDown className={`w-4 h-4 text-gray-400 transition-transform ${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 className="absolute top-full left-0 right-0 mt-1 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl z-50 p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">Adults</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">≥5 years</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => { const c = adultCount || 1; if (c > 1) setValue('adultCount', c - 1); }} disabled={(adultCount || 1) <= 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-40">
|
||||
<Minus className="w-3.5 h-3.5 text-primary" />
|
||||
</button>
|
||||
<span className="w-5 text-center font-semibold text-gray-900 dark:text-white">{adultCount || 1}</span>
|
||||
<button type="button" onClick={() => { const c = adultCount || 1; if (c < 9) setValue('adultCount', c + 1); }} disabled={(adultCount || 1) >= 9} 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-40">
|
||||
<Plus className="w-3.5 h-3.5 text-primary" />
|
||||
</button>
|
||||
</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"><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 className="border-t border-gray-100 dark:border-gray-700 pt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">Children</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400"><5 years · First free</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => { const c = childCount || 0; if (c > 0) setValue('childCount', c - 1); }} disabled={(childCount || 0) <= 0} 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-40">
|
||||
<Minus className="w-3.5 h-3.5 text-primary" />
|
||||
</button>
|
||||
<span className="w-5 text-center font-semibold text-gray-900 dark:text-white">{childCount || 0}</span>
|
||||
<button type="button" onClick={() => { const c = childCount || 0; if (c < 9) setValue('childCount', c + 1); }} disabled={(childCount || 0) >= 9} 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-40">
|
||||
<Plus className="w-3.5 h-3.5 text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,49 +306,35 @@ export function SearchWidget({ fullWidth = false, onSearch }: SearchWidgetProps)
|
||||
|
||||
{/* 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>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Nationality</label>
|
||||
<CustomSelect
|
||||
value={nationality}
|
||||
onChange={(val) => setValue('nationality', val as SearchForm['nationality'])}
|
||||
options={NATIONALITIES.map((n) => ({ value: n.value, label: n.label }))}
|
||||
placeholder="Select nationality"
|
||||
icon={<Globe className="w-5 h-5 text-primary" />}
|
||||
/>
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">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"
|
||||
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 text-base bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Search Button */}
|
||||
<div>
|
||||
{/* Debug info - remove after testing */}
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="mb-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p className="text-sm font-medium text-red-800 dark:text-red-200 mb-1">Validation Errors:</p>
|
||||
<ul className="text-xs text-red-700 dark:text-red-300 list-disc list-inside">
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<li key={key}>{key}: {value?.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</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>
|
||||
{/* Search Button */}
|
||||
<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" />
|
||||
<span>Search Train</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user