diff --git a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx index 5b2ad3e9b..98ce04738 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/search/page.tsx @@ -169,14 +169,18 @@ function StationModal({ function PassengerModal({ adultCount, childCount, + nationality, onChangeAdult, onChangeChild, + onChangeNationality, onClose, }: { adultCount: number; childCount: number; + nationality: string; onChangeAdult: (n: number) => void; onChangeChild: (n: number) => void; + onChangeNationality: (v: string) => void; onClose: () => void; }) { const rows = [ @@ -184,30 +188,31 @@ function PassengerModal({ { label: 'Children', sub: '< 5 years โ€ข First child free', val: childCount, min: 0, max: 9, onChange: onChangeChild }, ]; + const natOptions = [ + { value: 'ETHIOPIAN', label: '๐Ÿ‡ช๐Ÿ‡น Ethiopian' }, + { value: 'DJIBOUTIAN', label: '๐Ÿ‡ฉ๐Ÿ‡ฏ Djiboutian' }, + { value: 'OTHER', label: '๐ŸŒ Other' }, + ]; + return ( <> - {/* Backdrop */}
- {/* Bottom sheet */}
- {/* Handle */}
- {/* Header */}
-

Passengers

+

Passengers & Nationality

-
- {/* Rows */}
{rows.map(({ label, sub, val, min, max, onChange }, i) => (
@@ -218,35 +223,38 @@ function PassengerModal({

{sub}

- {val} -
))} +
+

Nationality

+
+ {natOptions.map((opt) => ( + + ))} +
+
- {/* Done */}
-
@@ -265,6 +273,7 @@ function StationDropdown({ onSelect, error, recentIds, + onOpen, }: { stations: Station[]; value: string; @@ -273,6 +282,7 @@ function StationDropdown({ onSelect: (s: Station) => void; error?: string; recentIds: string[]; + onOpen?: () => void; }) { const [query, setQuery] = useState(''); const [open, setOpen] = useState(false); @@ -315,7 +325,7 @@ function StationDropdown({ ref={inputRef} value={displayValue} onChange={(e) => { setQuery(e.target.value); setOpen(true); }} - onFocus={() => { setQuery(''); setOpen(true); }} + onFocus={() => { setQuery(''); setOpen(true); onOpen?.(); }} placeholder={placeholder} className="w-full pl-10 pr-8 py-3.5 bg-transparent rounded-xl focus:outline-none text-sm text-gray-900 dark:text-white placeholder-gray-400" /> @@ -331,7 +341,7 @@ function StationDropdown({
{open && ( -
+
{!query && recentIds.length > 0 && (

Recent

@@ -395,6 +405,16 @@ export default function SearchPage() { try { return JSON.parse(localStorage.getItem('edr_recent_stations') || '[]'); } catch { return []; } }); const passengerRef = useRef(null); + const widgetRef = useRef(null); + + const scrollWidgetIntoView = () => { + const el = widgetRef.current; + if (!el) return; + const headerHeight = 64; + const marginTop = 24; + const top = el.getBoundingClientRect().top + window.scrollY - headerHeight - marginTop; + window.scrollTo({ top, behavior: 'smooth' }); + }; const { data: stations = [], isLoading, error } = useQuery({ queryKey: ['stations'], @@ -517,14 +537,16 @@ export default function SearchPage() { }; return ( -
+
{/* Passenger modal (mobile) */} {passengerModalOpen && ( setValue('adultCount', n)} onChangeChild={(n) => setValue('childCount', n)} + onChangeNationality={(v) => setValue('nationality', v as any)} onClose={() => setPassengerModalOpen(false)} /> )} @@ -557,299 +579,219 @@ export default function SearchPage() { /> )} - {/* Hero Banner */} -
-
-
-
-
+ {/* โ”€โ”€ 90vh hero with banner image โ”€โ”€ */} +
+ {/* Background image */} +
+ {/* Gradient overlay */} +
+
+ + {/* Hero headline โ€” top area */} +
+

+ Where are you
headed today? +

+

Book your train journey across East Africa

-
+ + {/* โ”€โ”€ Widget โ€” absolutely positioned at bottom with margin โ”€โ”€ */} +
-

- Where are you headed? -

-

Search and book train tickets fast & easy

-
-
-
+
+
- {/* Search Card โ€” pulled up over the hero */} -
-
- -
- - {/* Error banner */} - {error && ( -
- โš ๏ธ - Unable to load stations. Please check your connection. -
- )} - -
- - {/* โ”€โ”€ Row 1 (mobile stacked): Stations + Date โ”€โ”€ */} - - {/* Mobile station fields */} -
-
- - - {errors.originStationId &&

{errors.originStationId.message}

} -
-
-
- - -
- - {errors.destinationStationId &&

{errors.destinationStationId.message}

} -
- {/* Date (mobile) */} -
- -
- setValue('departureDate', `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`)} - minDate={new Date()} - placeholder="Select date" - /> -
- {errors.departureDate &&

{errors.departureDate.message}

} -
-
- - {/* Desktop Row 1: From [swap] To + Date (3 equal cols) */} -
- {/* From + To with swap */} -
-
- - { setValue('originStationId', s.id); if (s.id) saveRecent(s.id); }} error={errors.originStationId?.message} /> - {errors.originStationId &&

{errors.originStationId.message}

} -
- -
- - { setValue('destinationStationId', s.id); if (s.id) saveRecent(s.id); }} error={errors.destinationStationId?.message} /> - {errors.destinationStationId &&

{errors.destinationStationId.message}

} -
-
- {/* Date */} -
- -
- setValue('departureDate', `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`)} - minDate={new Date()} - placeholder="Select date" - /> -
- {errors.departureDate &&

{errors.departureDate.message}

} -
-
- - {/* Route preview pill */} - {originStation && destStation && ( -
- - {originStation.name} - - {destStation.name} + {error && ( +
+ โš ๏ธ + Unable to load stations. Please check your connection.
)} - {/* โ”€โ”€ Row 2: Passengers | Nationality | Search Button โ”€โ”€ */} -
+
- {/* Passengers */} -
- - - {/* Mobile: opens bottom-sheet modal */} - +
+
+
+ + +
+ +
+
+ +
+ setValue('departureDate', `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`)} + minDate={new Date()} placeholder="Select date" + /> +
+
+ {/* Pax + Nationality combined trigger */} + - - {/* sm+: inline dropdown */} -
- - {isPassengerOpen && ( -
- {[ - { label: 'Adults', sub: 'โ‰ฅ 5 years', key: 'adultCount' as const, val: adultCount || 1, min: 1, max: 9 }, - { label: 'Children', sub: '< 5 years โ€ข First free', key: 'childCount' as const, val: childCount || 0, min: 0, max: 9 }, - ].map(({ label, sub, key, val, min, max }, i) => ( -
- {i > 0 &&
} -
-
-

{label}

-

{sub}

-
-
- - {val} - -
-
-
- ))} - -
- )} -
-
- - {/* Nationality */} -
- - -
- - {/* Search Button */} - -
- - {/* โ”€โ”€ Promo Code (collapsed by default) โ”€โ”€ */} -
- {!promoVisible ? ( - - ) : ( -
-
-
- - { setPromoCode(e.target.value.toUpperCase()); if (promoValidation) setPromoValidation(null); }} - placeholder="Enter promo code" - onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleValidatePromo())} - className="w-full pl-9 pr-3 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 transition-all" - autoFocus - /> -
- - -
- {promoValidation && ( -
- {promoValidation.valid && } - {promoValidation.message} -
- )} +
+ + {/* Desktop: single row โ€” From [swap] To | Date | Pax+Nat | Search */} +
+ {/* From */} +
+ + { setValue('originStationId', s.id); if (s.id) saveRecent(s.id); }} error={errors.originStationId?.message} onOpen={scrollWidgetIntoView} /> + {errors.originStationId &&

{errors.originStationId.message}

}
- )} + {/* Swap */} + + {/* To */} +
+ + { setValue('destinationStationId', s.id); if (s.id) saveRecent(s.id); }} error={errors.destinationStationId?.message} onOpen={scrollWidgetIntoView} /> + {errors.destinationStationId &&

{errors.destinationStationId.message}

} +
+ {/* Divider */} +
+ {/* Date */} +
+ +
+ setValue('departureDate', `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`)} + minDate={new Date()} placeholder="Select date" + /> +
+ {errors.departureDate &&

{errors.departureDate.message}

} +
+ {/* Divider */} +
+ {/* Pax + Nationality combined โ€” opens shared modal */} +
+ + +
+ {/* Search */} + +
+ + {/* Promo */} +
+ {!promoVisible ? ( + + ) : ( +
+
+
+ + { setPromoCode(e.target.value.toUpperCase()); if (promoValidation) setPromoValidation(null); }} + placeholder="Enter promo code" + onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleValidatePromo())} + className="w-full pl-9 pr-3 py-2.5 border-2 border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm bg-white placeholder-gray-400" + autoFocus /> +
+ + +
+ {promoValidation && ( +
+ {promoValidation.valid && } + {promoValidation.message} +
+ )} +
+ )} +
+
-
-
- + +
+
+
- {/* โ”€โ”€ Popular Routes โ”€โ”€ */} -
+ {/* Popular Routes โ€” below hero */} +
+
+

Popular Routes

{POPULAR_ROUTES.map((route, idx) => ( -
+ {/* Promotions Section */} +
+
+
+
+ ๐ŸŽ +

Offers & Promotions

+
+
+ + {/* Wide promo card */} +
+
+
+
+
+
+ + Limited Time + +

+ 20% Off Weekend
Travel +

+

+ Book any weekend journey and save 20%. Valid for all seat classes. +

+
+
+ Valid until 31 Dec 2024 + + Book now + +
+
+
+ + {/* Narrow promo cards */} +
+
+
+
+
+
+ New +

Family Package

+

4 tickets for the price of 3

+
+
+ + Learn more + +
+
+
+ +
+
+
+
+
+ Student +

Student Discount

+

15% off with valid student ID

+
+
+ + Learn more + +
+
+
+
+ +
+
+
+
+
); diff --git a/apps/edr-passenger-web/portal/src/app/page.tsx b/apps/edr-passenger-web/portal/src/app/page.tsx index b86db2ffe..c48063991 100644 --- a/apps/edr-passenger-web/portal/src/app/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/page.tsx @@ -1,438 +1 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { getTranslation, Language, useLanguage } from '@/lib/i18n'; -import Link from 'next/link'; -import { SearchWidget } from '@/components/SearchWidget'; -import { Zap, Heart, Shield, Clock, ArrowRight, Train, MapPin, Calendar } from 'lucide-react'; -import { useQuery } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api-client'; -import { Station } from '@/types'; - -const styles = ` - .hero-section { - position: relative; - overflow: hidden; - background: linear-gradient(to bottom right, rgb(20, 113, 76), transparent); - padding: 80px 20px; - text-align: center; - color: #111827; - } - - .dark .hero-section { - background: linear-gradient(to bottom right, rgb(20, 113, 76), transparent); - color: #f3f4f6; - } - - .hero-content { - max-width: 100%; - margin: 0 auto; - padding: 0 20px; - } - - .hero-heading { - font-size: clamp(1rem, 3vw, 2.75rem); - font-weight: 700; - margin-bottom: 24px; - color: #ffffff; - line-height: 1.2; - } - - .dark .hero-heading { - color: #ffffff; - } - - .hero-subheading { - font-size: clamp(1rem, 3vw, 1.5rem); - color: #4b5563; - margin-bottom: 32px; - } - - .dark .hero-subheading { - color: #9ca3af; - } - - .stats-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - padding: 32px 16px; - border-top: 1px solid #e5e7eb; - border-bottom: 1px solid #e5e7eb; - margin-top: 48px; - } - - .dark .stats-grid { - border-top-color: #374151; - border-bottom-color: #374151; - } - - .stat-item { - text-align: center; - } - - .stat-number { - font-size: 2rem; - font-weight: 700; - color: rgb(20, 113, 76); - } - - .stat-label { - font-size: 0.875rem; - color: #6b7280; - } - - .dark .stat-label { - color: #9ca3af; - } - - .features-section { - padding: 80px 20px; - background-color: #ffffff; - } - - .dark .features-section { - background-color: #111827; - } - - .section-title { - text-align: center; - font-size: 2rem; - font-weight: 700; - margin-bottom: 40px; - color: #111827; - } - - .dark .section-title { - color: #f3f4f6; - } - - .features-grid { - max-width: 72rem; - margin: 0 auto; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 24px; - } - - .feature-card { - background-color: white; - border: 2px solid #f3f4f6; - border-radius: 18px; - padding: 24px; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - text-align: left; - } - - .dark .feature-card { - background-color: #1f2937; - border-color: #374151; - } - - .feature-card:hover { - border-color: rgb(20, 113, 76); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - transform: translateY(-8px); - } - - .feature-icon-bg { - width: 48px; - height: 48px; - background-color: rgb(20, 113, 76); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16px; - } - - .feature-title { - font-weight: 700; - color: #111827; - margin-bottom: 8px; - font-size: 1.125rem; - } - - .dark .feature-title { - color: #f3f4f6; - } - - .feature-desc { - font-size: 0.875rem; - color: #6b7280; - } - - .dark .feature-desc { - color: #9ca3af; - } - - .cta-section { - padding: 80px 20px; - background: #f3f4f6; - text-align: center; - } - - .dark .cta-section { - background: #111827; - } - - .cta-content { - max-width: 42rem; - margin: 0 auto; - } - - .cta-title { - font-size: 2rem; - font-weight: 700; - margin-bottom: 24px; - color: #111827; - } - - .dark .cta-title { - color: white; - } - - .cta-text { - font-size: 1.125rem; - color: #4b5563; - margin-bottom: 32px; - } - - .dark .cta-text { - color: #e0e7ff; - } - - .cta-button { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 16px 32px; - background-color: white; - color: rgb(20 113 76 / var(--tw-bg-opacity, 1)); - font-weight: 700; - border-radius: 12px; - border: none; - cursor: pointer; - transition: all 0.2s; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - text-decoration: none; - } - - .cta-button:hover { - background-color: #f0f9ff; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15); - transform: scale(1.05); - } - - @media (max-width: 640px) { - .stats-grid { - grid-template-columns: 1fr; - } - } - - @keyframes bounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-10px); - } - } - - .bounce { - animation: bounce 2s infinite; - } - - .bounce:nth-child(2) { - animation-delay: 0.2s; - } - - .bounce:nth-child(3) { - animation-delay: 0.4s; - } - - .search-widget-transparent { - background-color: rgba(255, 255, 255, 0.95) !important; - backdrop-filter: blur(10px); - border-color: rgba(255, 255, 255, 0.2) !important; - } - - .dark .search-widget-transparent { - background-color: rgba(31, 41, 55, 0.95) !important; - border-color: rgba(55, 65, 81, 0.2) !important; - } -`; - -export default function Home() { - const [lang, setLang] = useState('en'); - const { getLang } = useLanguage(); - const t = (key: string) => getTranslation(lang, key); - - useEffect(() => { - setLang(getLang()); - const handleLanguageChange = (e: any) => setLang(e.detail); - window.addEventListener('languageChange', handleLanguageChange); - return () => window.removeEventListener('languageChange', handleLanguageChange); - }, [getLang]); - - const { data: stations } = useQuery({ - queryKey: ['stations'], - queryFn: async () => await apiClient.get('/stations') as Station[], - }); - - const getStationByName = (name: string) => { - if (!stations) return null; - const exactMatch = stations.find(s => s.name.toLowerCase() === name.toLowerCase()); - if (exactMatch) return exactMatch; - return stations.find(s => s.name.toLowerCase().includes(name.toLowerCase())); - }; - - const handlePopularRoute = (fromName: string, toName: string) => { - const origin = getStationByName(fromName); - const destination = getStationByName(toName); - - if (origin && destination) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }; - - const popularRoutes = [ - { from: 'Sebeta', to: 'Nagad', duration: '12h' }, - { from: 'Sebeta', to: 'Diredawa', duration: '8h' }, - { from: 'Diredawa', to: 'Nagad', duration: '4h' }, - ]; - - const features = [ - { - icon: Heart, - title: t('home.comfortable'), - desc: t('home.comfortDesc'), - }, - { - icon: Zap, - title: t('home.affordable'), - desc: t('home.affordableDesc'), - }, - { - icon: Shield, - title: t('home.safe'), - desc: t('home.safeDesc'), - }, - { - icon: Clock, - title: t('home.fast'), - desc: t('home.fastDesc'), - }, - ]; - - const highlights = [ - { - icon: Train, - title: 'Modern fleet', - desc: 'Comfortable trains with modern amenities', - }, - { - icon: MapPin, - title: '21 stations', - desc: 'Connecting Ethiopia and Djibouti', - }, - { - icon: Calendar, - title: 'Easy booking', - desc: 'Book tickets in just a few clicks', - }, - ]; - - return ( - <> - -
- {/* Hero Section */} -
-
-

{t('home.hero')}

-

{t('home.heroSub')}

- - - - {/* Highlights */} -
- {highlights.map((highlight, idx) => { - const Icon = highlight.icon; - return ( -
-
- -
-

{highlight.title}

-

{highlight.desc}

-
- ); - })} -
-
-
- - {/* Popular Routes Section */} -
-
-

Popular Routes

-
- {popularRoutes.map((route, idx) => ( - - ))} -
-
-
- - {/* Features Section */} -
-

{t('home.features')}

-
- {features.map((feature, idx) => { - const Icon = feature.icon; - return ( -
-
- -
-

{feature.title}

-

{feature.desc}

-
- ); - })} -
-
- - {/* CTA Section */} -
-
-

Ready to start your journey?

-

Book your train tickets in just a few minutes and enjoy a comfortable ride.

- - {t('home.cta')} - - -
-
-
- - ); -} +export { default } from '@/app/booking/search/page';