diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index 34f1fada1..d271e0042 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -1,74 +1,84 @@ -'use client'; +"use client"; -import { useRouter } from 'next/navigation'; -import { useBookingStore } from '@/lib/booking-store'; -import { usePaymentStore } from '@/lib/payment-store'; -import { useMutation } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api-client'; -import { useState, useEffect } from 'react'; -import { CreditCard, Smartphone, Wallet, Loader2, CheckCircle } from 'lucide-react'; +import { useRouter } from "next/navigation"; +import { useBookingStore } from "@/lib/booking-store"; +import { usePaymentStore } from "@/lib/payment-store"; +import { useMutation } from "@tanstack/react-query"; +import { apiClient } from "@/lib/api-client"; +import { useState, useEffect } from "react"; +import { + CreditCard, + Smartphone, + Wallet, + Loader2, + CheckCircle, +} from "lucide-react"; // Mock payment methods with Ethiopian providers const paymentMethods = [ - { - id: 'TELEBIRR', - name: 'Telebirr', - icon: Smartphone, - description: 'Pay with Telebirr mobile money', - color: 'bg-orange-50 border-orange-200 hover:border-orange-400' + { + id: "TELEBIRR", + name: "Telebirr", + icon: Smartphone, + description: "Pay with Telebirr mobile money", + color: "bg-orange-50 border-orange-200 hover:border-orange-400", }, - { - id: 'CBE_BIRR', - name: 'CBE Birr', - icon: Smartphone, - description: 'Pay with CBE Birr', - color: 'bg-blue-50 border-blue-200 hover:border-blue-400' + { + id: "CBE_BIRR", + name: "CBE Birr", + icon: Smartphone, + description: "Pay with CBE Birr", + color: "bg-blue-50 border-blue-200 hover:border-blue-400", }, - { - id: 'EBIRR', - name: 'eBirr', - icon: Smartphone, - description: 'Pay with eBirr', - color: 'bg-green-50 border-green-200 hover:border-green-400' + { + id: "EBIRR", + name: "eBirr", + icon: Smartphone, + description: "Pay with eBirr", + color: "bg-green-50 border-green-200 hover:border-green-400", }, - { - id: 'CARD', - name: 'Card Payment', - icon: CreditCard, - description: 'Pay with credit/debit card', - color: 'bg-purple-50 border-purple-200 hover:border-purple-400' + { + id: "CARD", + name: "Card Payment", + icon: CreditCard, + description: "Pay with credit/debit card", + color: "bg-purple-50 border-purple-200 hover:border-purple-400", }, - { - id: 'WALLET', - name: 'Wallet', - icon: Wallet, - description: 'Pay from your wallet balance', - color: 'bg-indigo-50 border-indigo-200 hover:border-indigo-400' + { + id: "WALLET", + name: "Wallet", + icon: Wallet, + description: "Pay from your wallet balance", + color: "bg-indigo-50 border-indigo-200 hover:border-indigo-400", }, ]; export default function PaymentPage() { const router = useRouter(); const { bookingId, pnr, selectedSchedule, passengers } = useBookingStore(); - const { selectedCurrency, setPaymentIntent, updateStatus } = usePaymentStore(); + const { selectedCurrency, setPaymentIntent, updateStatus } = + usePaymentStore(); const [selectedMethod, setSelectedMethod] = useState(null); const [isProcessing, setIsProcessing] = useState(false); // Calculate total amount - const baseFare = passengers.reduce((sum) => sum + (selectedSchedule?.baseFareAdult || 0), 0); + const baseFare = passengers.reduce( + (sum) => sum + (selectedSchedule?.baseFareAdult || 0), + 0, + ); const totalAmount = baseFare; const paymentMutation = useMutation({ mutationFn: async (data: any) => { // Try to call the real API, fallback to mock if it fails try { - return await apiClient.post('/payments/intent', data); + return await apiClient.post("/payments/intent", data); } catch (error) { - console.log('Payment API not available, using mock payment'); + console.log("Payment API not available, using mock payment"); // Mock payment response return { paymentIntentId: `mock-payment-${Date.now()}`, - status: 'PENDING', + status: "PENDING", amountMinor: data.amountMinor, currency: data.currency, method: data.method, @@ -77,27 +87,30 @@ export default function PaymentPage() { }, onSuccess: async (data: any) => { setPaymentIntent(data.paymentIntentId); - updateStatus('PROCESSING'); - + updateStatus("PROCESSING"); + // Simulate payment processing - await new Promise(resolve => setTimeout(resolve, 2000)); - + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Generate tickets after successful payment try { await generateTickets(); - updateStatus('SUCCEEDED'); - router.push('/booking/confirmation'); + updateStatus("SUCCEEDED"); + router.push("/booking/confirmation"); } catch (error) { - console.error('Ticket generation failed:', error); + console.error("Ticket generation failed:", error); // Still proceed to confirmation even if ticket generation fails - updateStatus('SUCCEEDED'); - router.push('/booking/confirmation'); + updateStatus("SUCCEEDED"); + router.push("/booking/confirmation"); } }, onError: (error: any) => { - console.error('Payment failed:', error); - updateStatus('FAILED'); - const errorMessage = error?.response?.data?.message || error?.message || 'Payment failed. Please try again.'; + console.error("Payment failed:", error); + updateStatus("FAILED"); + const errorMessage = + error?.response?.data?.message || + error?.message || + "Payment failed. Please try again."; alert(errorMessage); setIsProcessing(false); }, @@ -106,19 +119,21 @@ export default function PaymentPage() { const generateTickets = async () => { // Try to generate tickets via API, fallback to mock try { - await apiClient.post('/tickets/generate', { + await apiClient.post("/tickets/generate", { bookingId, pnr, }); } catch (error) { - console.log('Ticket API not available, tickets will be generated on confirmation page'); + console.log( + "Ticket API not available, tickets will be generated on confirmation page", + ); // Mock ticket generation - tickets will be displayed on confirmation page } }; const handlePayment = async () => { if (!selectedMethod || !bookingId) { - alert('Please select a payment method'); + alert("Please select a payment method"); return; } @@ -137,12 +152,14 @@ export default function PaymentPage() { // Add a small delay to allow state to be set from previous page const timer = setTimeout(() => { if (!bookingId || !pnr) { - console.log('Payment page: Missing booking data, redirecting to search'); - console.log('bookingId:', bookingId, 'pnr:', pnr); - router.push('/booking/search'); + console.log( + "Payment page: Missing booking data, redirecting to search", + ); + console.log("bookingId:", bookingId, "pnr:", pnr); + router.push("/booking/search"); } }, 500); - + return () => clearTimeout(timer); }, [bookingId, pnr, router]); @@ -161,9 +178,12 @@ export default function PaymentPage() {
-

Complete payment

+

+ Complete payment +

- Booking reference: {pnr} + Booking reference:{" "} + {pnr}

{/* Payment Processing Overlay */} @@ -173,15 +193,23 @@ export default function PaymentPage() { {paymentMutation.isSuccess ? ( <> -

Payment successful!

-

Generating your tickets...

+

+ Payment successful! +

+

+ Generating your tickets... +

) : ( <> -

Processing payment

-

Please wait while we process your payment...

+

+ Processing payment +

+

+ Please wait while we process your payment... +

)}
@@ -190,29 +218,46 @@ export default function PaymentPage() { {/* Order Summary */}
-

Order summary

+

+ Order summary +

Route - {selectedSchedule?.origin} → {selectedSchedule?.destination} + + {selectedSchedule?.origin} → {selectedSchedule?.destination} +
Train - {selectedSchedule?.trainNumber} + + {selectedSchedule?.trainNumber} +
{selectedSchedule?.selectedSeatClassName && (
- Class - {selectedSchedule.selectedSeatClassName.replace(/_/g, ' ')} + + Class + + + {selectedSchedule.selectedSeatClassName.replace(/_/g, " ")} +
)}
- Passengers - {passengers.length} passenger{passengers.length !== 1 ? 's' : ''} + + Passengers + + + {passengers.length} passenger + {passengers.length !== 1 ? "s" : ""} +
- Total amount + + Total amount + ETB {(totalAmount / 100).toFixed(2)} @@ -223,7 +268,9 @@ export default function PaymentPage() { {/* Payment Methods */}
-

Select payment method

+

+ Select payment method +

{paymentMethods.map((method) => { const Icon = method.icon; @@ -235,19 +282,29 @@ export default function PaymentPage() { disabled={isProcessing} className={`w-full p-4 rounded-lg border-2 transition-all text-left ${ isSelected - ? 'border-primary bg-primary/10 dark:bg-primary/20 shadow-md' - : 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-primary dark:hover:border-primary' - } ${isProcessing ? 'opacity-50 cursor-not-allowed' : ''}`} + ? "border-primary bg-primary/10 dark:bg-primary/20 shadow-md" + : "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-primary dark:hover:border-primary" + } ${isProcessing ? "opacity-50 cursor-not-allowed" : ""}`} >
-
- +
+
-

{method.name}

-

{method.description}

+

+ {method.name} +

+

+ {method.description} +

{isSelected && (
@@ -267,7 +324,9 @@ export default function PaymentPage() { onClick={handlePayment} disabled={!selectedMethod || isProcessing} className={`btn-primary w-full py-4 text-lg font-semibold ${ - !selectedMethod || isProcessing ? 'opacity-50 cursor-not-allowed' : '' + !selectedMethod || isProcessing + ? "opacity-50 cursor-not-allowed" + : "" }`} > {isProcessing ? ( @@ -279,7 +338,7 @@ export default function PaymentPage() { `Pay ETB ${(totalAmount / 100).toFixed(2)}` )} - +
- + {/* Error Message */} {paymentMutation.isError && (

- ⚠️ Payment failed. Please try again or contact support if the problem persists. + ⚠️ Payment failed. Please try again or contact support if the + problem persists.

)} @@ -301,7 +361,8 @@ export default function PaymentPage() { {/* Security Notice */}

- 🔒 Your payment is secure and encrypted. We do not store your payment information. + 🔒 Your payment is secure and encrypted. We do not store your + payment information.

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..819b59d6d 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 @@ -10,7 +10,7 @@ import { apiClient } from '@/lib/api-client'; import { useBookingStore } from '@/lib/booking-store'; import { Station } from '@/types'; import { - Train, MapPin, ArrowRight, ArrowLeftRight, Plus, Minus, Search, + MapPin, ArrowRight, ArrowLeftRight, Plus, Minus, Search, Users, ChevronDown, Gift, Check, X, ChevronLeft, Clock, Zap, } from 'lucide-react'; import { useEffect, useRef, useState, useCallback } from 'react'; @@ -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

@@ -383,7 +393,6 @@ export default function SearchPage() { const setSearchCriteria = useBookingStore((s) => s.setSearchCriteria); const { user, isAuthenticated } = useAuthStore(); - const [isPassengerOpen, setIsPassengerOpen] = useState(false); const [passengerModalOpen, setPassengerModalOpen] = useState(false); const [promoVisible, setPromoVisible] = useState(false); const [promoCode, setPromoCode] = useState(''); @@ -395,13 +404,23 @@ 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'], queryFn: async () => await apiClient.get('/stations') as Station[], }); - const { register, handleSubmit, watch, setValue, formState: { errors } } = useForm({ + const { handleSubmit, watch, setValue, formState: { errors } } = useForm({ resolver: zodResolver(searchSchema as any), defaultValues: { adultCount: 1, @@ -437,7 +456,7 @@ export default function SearchPage() { useEffect(() => { const handler = (e: MouseEvent) => { if (passengerRef.current && !passengerRef.current.contains(e.target as Node)) { - setIsPassengerOpen(false); + setPassengerModalOpen(false); } }; document.addEventListener('mousedown', handler); @@ -517,14 +536,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 +578,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..5e717705f 100644 --- a/apps/edr-passenger-web/portal/src/app/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/page.tsx @@ -1,438 +1,10 @@ -'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; - } -`; +import { Suspense } from 'react'; +import SearchPage from '@/app/booking/search/page'; 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')} - - -
-
-
- + + + ); }