diff --git a/apps/edr-passenger-web/portal/src/app/booking/results/loading.tsx b/apps/edr-passenger-web/portal/src/app/booking/results/loading.tsx
new file mode 100644
index 000000000..72a278a1a
--- /dev/null
+++ b/apps/edr-passenger-web/portal/src/app/booking/results/loading.tsx
@@ -0,0 +1,103 @@
+// Next.js renders this automatically the instant navigation to /booking/results
+// begins — before the route's JS has even finished downloading/compiling and
+// well before the page component mounts or its data fetch starts. That closes
+// the "I clicked Search and nothing happened" gap: previously there was no
+// visual feedback at all until the route fully loaded and hit its own isLoading
+// state. Mirrors that same isLoading skeleton so the transition is seamless.
+export default function ResultsLoading() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Searching for trains...
+
+
+ Finding the best options for your journey
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
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 59ae5f3f8..d8e485fd6 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
@@ -552,6 +552,17 @@ export default function SearchPage() {
"origin" | "destination" | null
>(null);
const [hasInteracted, setHasInteracted] = useState(false);
+ // Immediate feedback the moment Search is clicked — router.push() itself
+ // doesn't paint anything until the target route's JS has loaded, which
+ // otherwise reads as a dead click.
+ const [isSearching, setIsSearching] = useState(false);
+
+ // Warms the results route's JS chunk ahead of time so clicking Search
+ // doesn't have to wait for it to download/compile on top of the actual
+ // search request.
+ useEffect(() => {
+ router.prefetch("/booking/results");
+ }, [router]);
const [recentStationIds, setRecentStationIds] = useState(() => {
try {
return JSON.parse(localStorage.getItem("edr_recent_stations") || "[]");
@@ -689,6 +700,7 @@ export default function SearchPage() {
const onSubmit = (data: SearchForm) => {
setHasInteracted(true);
+ setIsSearching(true);
// Clear previous booking selections and search cache before starting a new search
clearBooking();
setSearchCriteria(data);
@@ -715,6 +727,7 @@ export default function SearchPage() {
// origin/destination/date errors, which is noisier than fixing things one step at a time.
const onInvalid = (formErrors: typeof errors) => {
setHasInteracted(true);
+ setIsSearching(false);
const hasOtherErrors = Object.keys(formErrors).some((k) => k !== "nationality");
if (formErrors.nationality && !hasOtherErrors) {
setPassengerModalOpen(true);
@@ -1066,11 +1079,20 @@ export default function SearchPage() {
)}
@@ -1207,11 +1229,20 @@ export default function SearchPage() {
{/* Search */}
) : (
@@ -1328,11 +1359,20 @@ export default function SearchPage() {
{/* Search */}
)}
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 669a99d0b..9f0d2d474 100644
--- a/apps/edr-passenger-web/portal/src/app/contact/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/contact/page.tsx
@@ -275,8 +275,8 @@ export default function Contact() {
};
const contactInfo = [
- { icon: Phone, title: t('contact.phone'), value: '+251 911 000 000', link: 'tel:+251911000000' },
- { icon: Mail, title: t('contact.email'), value: 'support@edr.et', link: 'mailto:support@edr.et' },
+ { icon: Phone, title: t('contact.phone'), value: '9546', link: 'tel:9546' },
+ { icon: Mail, title: t('contact.email'), value: 'edr_@edrsc.com', link: 'mailto:edr_@edrsc.com' },
{ icon: MapPin, title: t('contact.address'), value: 'Addis Ababa, Ethiopia', link: '#' },
];
diff --git a/apps/edr-passenger-web/portal/src/components/AppSidebar.tsx b/apps/edr-passenger-web/portal/src/components/AppSidebar.tsx
index 6d47b8b5a..0acb5d9ea 100644
--- a/apps/edr-passenger-web/portal/src/components/AppSidebar.tsx
+++ b/apps/edr-passenger-web/portal/src/components/AppSidebar.tsx
@@ -14,12 +14,18 @@ import {
} from 'lucide-react';
import Link from 'next/link';
import Image from 'next/image';
+import dynamic from 'next/dynamic';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { useAuthStore } from '@/lib/auth-store';
-import ChangePasswordModal from '@/components/ChangePasswordModal';
import { BOOKING_STEPS } from '@/components/ProgressIndicator';
+// AppSidebar renders on every page via the root layout, so anything imported
+// here ships to every visitor's first load — but this modal is only ever
+// reachable by an already-authenticated user opening the account dropdown.
+// Code-split it out instead of paying for it on every page/every visitor.
+const ChangePasswordModal = dynamic(() => import('@/components/ChangePasswordModal'), { ssr: false });
+
// Mirrors booking/layout.tsx's stepMap — the linear booking flow routes that
// get a vertical step list instead of the standard nav highlighting.
const BOOKING_STEP_MAP: Record = {
@@ -203,10 +209,12 @@ export default function AppSidebar() {
)}
- setShowChangePassword(false)}
- />
+ {showChangePassword && (
+ setShowChangePassword(false)}
+ />
+ )}
);
}
diff --git a/apps/edr-passenger-web/portal/src/lib/generate-voucher.ts b/apps/edr-passenger-web/portal/src/lib/generate-voucher.ts
index 6ff31aeaf..ca22538be 100644
--- a/apps/edr-passenger-web/portal/src/lib/generate-voucher.ts
+++ b/apps/edr-passenger-web/portal/src/lib/generate-voucher.ts
@@ -358,7 +358,7 @@ function drawFooter(doc: jsPDF, createdAt: string): void {
hairline(doc, PAGE_MARGIN, footerY, pageWidth - PAGE_MARGIN);
doc.setFontSize(7.5); doc.setTextColor(...MUTED); doc.setFont('helvetica', 'normal');
- doc.text('support@edr.com · +251-11-XXX-XXXX · www.edr.com', pageWidth / 2, footerY + 6, { align: 'center' });
+ doc.text('edr_@edrsc.com · 9546 · www.edr.com', pageWidth / 2, footerY + 6, { align: 'center' });
doc.setFontSize(6.5);
doc.text(`Issued ${new Date(createdAt).toLocaleString('en-US')}`, pageWidth / 2, footerY + 10.5, { align: 'center' });
}