diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index 515eb4449..719e80f18 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -188,7 +188,7 @@ export default function ConfirmationPage() { if (!bookingId || !pnr) return null; return ( -
+
{/* Success Header */} @@ -456,26 +456,29 @@ export default function ConfirmationPage() {
- {/* Action Buttons */} -
- -
+ {/* Action Buttons — voucher/ticket only makes sense once the booking is + actually confirmed; a pending-payment booking has no real ticket yet. */} + {isConfirmed && ( +
+ +
+ )} {/* New Booking Button */}
)} - {children} + {children}
); } diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx index 5e5c13b3e..da81c5cd8 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx @@ -1024,7 +1024,7 @@ export default function PassengersPage() { if (!formInitialized || faydaCompleting) { return ( -
+
@@ -1043,7 +1043,7 @@ export default function PassengersPage() {
-

Passenger details

+

Passenger details

{fields.map((field, index) => { 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 3e9cecbc7..6ce8b5f66 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 @@ -188,10 +188,10 @@ export default function PaymentPage() { if (!bookingId && !pnr) { return ( -
+
-

Loading payment details...

+

Loading payment details...

); @@ -372,10 +372,10 @@ export default function PaymentPage() { ); return ( -
+
-

Complete payment

+

Complete payment

{/* Reservation confirmation banner */}
diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx index fa52aad32..fb44dce41 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/results/page.tsx @@ -534,7 +534,7 @@ export default function ResultsPage() { e.stopPropagation(); handleSelect(classModal, isOutbound); }} - className="mt-3 w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-gradient-to-r from-[rgb(20,113,76)] to-[rgb(16,95,65)] hover:from-[rgb(16,89,60)] hover:to-[rgb(12,75,50)] text-white font-bold text-sm rounded-xl transition-all shadow-md shadow-primary/30 hover:shadow-lg active:scale-[0.98]" + className="btn-primary w-full mt-3 text-sm active:scale-[0.98]" > {isRoundTrip && isOutbound @@ -652,9 +652,9 @@ export default function ResultsPage() {
-
+
-
+
{schedule.departureAt ? formatTime(schedule.departureAt) : "--:--"} @@ -669,9 +669,9 @@ export default function ResultsPage() {
-
-
- +
+
+ {durationStr}
@@ -679,15 +679,15 @@ export default function ResultsPage() {
{schedule.stops && schedule.stops.length > 0 && ( -
- +
+ {schedule.stops.length - 2} stops
)}
-
+
{schedule.arrivalAt ? formatTime(schedule.arrivalAt) : "--:--"} @@ -749,7 +749,7 @@ export default function ResultsPage() { if (isLoading) { return ( -
+
{/* Progress Header */} @@ -952,7 +952,7 @@ export default function ResultsPage() { (results?.alternativeInbound || []).length === 0; if (isRoundTripNoResults) { return ( -
+
@@ -974,7 +974,7 @@ export default function ResultsPage() { const hasAlternatives = alternativeOutbound.length > 0; return ( -
+
{renderClassModal()}
@@ -1013,7 +1013,7 @@ export default function ResultsPage() { } return ( -
+
@@ -1032,7 +1032,7 @@ export default function ResultsPage() { } return ( -
+
{renderClassModal()} diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index 57b098dc1..251f8ae8b 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -636,10 +636,10 @@ export default function ReviewPage() { ); return ( -
+
-

+

{packageName ? `Review your ${packageName} booking` : 'Review your booking'}

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 574b08487..02c1fcd35 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 @@ -608,6 +608,12 @@ export default function SearchPage() { reValidateMode: "onSubmit", defaultValues: { tripType: "ONE_WAY", + // originStationId/destinationStationId must default to "" rather than being omitted — + // Zod's base string type check runs before .min(1, "..."), so an `undefined` value hits + // its generic "Required" message instead of our custom one. An empty string is still a + // string, so .min() (and its custom message) is what actually fires. + originStationId: "", + destinationStationId: "", adultCount: 1, childCount: 0, // No default nationality — the user must explicitly pick one. Left blank (not a valid @@ -827,19 +833,21 @@ export default function SearchPage() { /> )} - {/* ── 90vh hero with banner image ── */} - {/* Round trip stacks an extra Return Date field into the widget on mobile, which grows + {/* ── 90vh hero with banner image (desktop) / top-aligned widget only (mobile) ── */} + {/* Round trip stacks an extra Return Date field into the widget on desktop, which grows upward from its bottom-anchored position — give the hero extra height there so the - widget's top edge doesn't creep up into the sticky header. */} + widget's top edge doesn't creep up into the sticky header. On mobile the widget is + in normal flow (not bottom-anchored), so this only applies at md: and up. */}
- {/* Background image with zoom - fully isolated */} -
+ {/* Background image with zoom — desktop only; mobile drops the hero image entirely + so the booking widget can sit at the top and use the available space. */} +
- {/* Gradient overlay */} -
-
+ {/* Gradient overlay — desktop only (exists to keep the hero text readable over the image) */} +
+
- {/* Hero headline — top area */} -
+ {/* Hero headline — desktop only; removed on mobile along with the banner image */} +

Where are you
headed today? @@ -867,14 +875,28 @@ export default function SearchPage() {

- {/* ── Widget — absolutely positioned at bottom with margin ── */} + {/* ── Widget — top-aligned, normal flow on mobile; absolutely positioned at + bottom on desktop over the hero image. z-[35] sits above the floating + support chat launcher (z-30) but below the sidebar/tab bar (z-40), so it + never covers either. Modals opened from inside the widget (date picker + etc.) are portaled to — see ModernDatePicker — so they aren't + capped by this wrapper's own stacking context. ── */}
+ {/* Mobile-only heading — desktop keeps the version overlaid on the hero image above */} +
+

+ Where are you headed today? +

+

+ Book your train journey across East Africa +

+
-
+
{error && (
⚠️ @@ -950,6 +972,11 @@ export default function SearchPage() {
+ {hasInteracted && errors.originStationId && ( +

+ {errors.originStationId.message} +

+ )}
@@ -998,6 +1025,11 @@ export default function SearchPage() {
+ {hasInteracted && errors.destinationStationId && ( +

+ {errors.destinationStationId.message} +

+ )}
+ {errors.departureDate && ( +

+ {errors.departureDate.message} +

+ )}
{tripType === "ROUND_TRIP" && (
@@ -1047,6 +1085,7 @@ export default function SearchPage() { : new Date() } placeholder="Select return date" + error={!!errors.returnDate} />
{errors.returnDate && ( @@ -1079,7 +1118,7 @@ export default function SearchPage() {
{errors.departureDate && ( @@ -1219,7 +1259,7 @@ export default function SearchPage() {
{errors.departureDate && ( @@ -1357,6 +1398,7 @@ export default function SearchPage() { : new Date() } placeholder="Select date" + error={!!errors.returnDate} />
{errors.returnDate && ( @@ -1482,7 +1524,7 @@ export default function SearchPage() {
{isLoading ? ( -
-
-

- Loading seat map... -

+
+
+ + +
+
+ {Array.from({ length: 24 }).map((_, i) => ( + + ))} +
) : error ? (
diff --git a/apps/edr-passenger-web/portal/src/app/contact/page.tsx b/apps/edr-passenger-web/portal/src/app/contact/page.tsx index f2c0f4296..669a99d0b 100644 --- a/apps/edr-passenger-web/portal/src/app/contact/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/contact/page.tsx @@ -3,16 +3,18 @@ import { useEffect, useState } from 'react'; import { getTranslation, Language, useLanguage } from '@/lib/i18n'; import { Phone, Mail, MapPin, Send, Loader } from 'lucide-react'; +import { Footer } from '@/components/Footer'; const styles = ` .contact-hero { padding: 60px 20px; - background: linear-gradient(to bottom right, rgb(20, 113, 76), transparent); + background: #ffffff; text-align: center; color: #111827; } - + .dark .contact-hero { + background: #111827; color: #f3f4f6; } @@ -370,6 +372,7 @@ export default function Contact() {
+