Update contact information

This commit is contained in:
Roba Boru
2026-07-10 21:53:42 +03:00
parent ca7a50c548
commit a7a9b22bbe
5 changed files with 171 additions and 20 deletions

View File

@@ -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 (
<div className="booking-page">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<div className="card p-6">
<div className="flex items-center gap-4">
<div className="relative">
<div className="w-12 h-12 rounded-full border-4 border-primary/20 border-t-primary animate-spin" />
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-1">
Searching for trains...
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Finding the best options for your journey
</p>
</div>
</div>
<div className="mt-4 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full"
style={{
animation: "progressBar 2s ease-in-out infinite",
}}
/>
</div>
</div>
</div>
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="card animate-pulse">
<div className="flex flex-col lg:flex-row lg:items-center gap-6">
<div className="flex-1">
<div className="flex items-center gap-3 mb-4">
<div
className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
<div className="flex-1 space-y-2">
<div
className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-24"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-center space-y-2">
<div
className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
<div
className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-12"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
</div>
<div className="flex-1 h-0.5 bg-gray-200 dark:bg-gray-700" />
<div className="text-center space-y-2">
<div
className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-16"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
<div
className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-12"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
</div>
</div>
</div>
<div className="lg:border-l dark:border-gray-700 lg:pl-6 lg:min-w-[220px]">
<div className="text-center lg:text-right space-y-2">
<div
className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mx-auto lg:ml-auto lg:mr-0"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
<div
className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-24 mx-auto lg:ml-auto lg:mr-0"
style={{ animation: "shimmer 1.5s ease-in-out infinite" }}
/>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@@ -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<string[]>(() => {
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() {
)}
<button
type="submit"
disabled={isLoading}
className="btn-primary w-full text-sm"
disabled={isLoading || isSearching}
className="btn-primary w-full text-sm flex items-center justify-center gap-2 disabled:opacity-80"
>
<Search className="w-5 h-5" />
Search
{isSearching ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Searching...
</>
) : (
<>
<Search className="w-5 h-5" />
Search
</>
)}
</button>
</div>
@@ -1207,11 +1229,20 @@ export default function SearchPage() {
{/* Search */}
<button
type="submit"
disabled={isLoading}
className="flex-shrink-0 flex items-center justify-center gap-2 px-5 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all transform hover:-translate-y-0.5 shadow-lg hover:shadow-xl disabled:opacity-50"
disabled={isLoading || isSearching}
className="flex-shrink-0 flex items-center justify-center gap-2 px-5 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all transform hover:-translate-y-0.5 shadow-lg hover:shadow-xl disabled:opacity-80"
>
<Search className="w-5 h-5" />
Search
{isSearching ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Searching...
</>
) : (
<>
<Search className="w-5 h-5" />
Search
</>
)}
</button>
</div>
) : (
@@ -1328,11 +1359,20 @@ export default function SearchPage() {
{/* Search */}
<button
type="submit"
disabled={isLoading}
className="flex-shrink-0 flex items-center justify-center gap-2 px-5 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all shadow-lg hover:shadow-xl disabled:opacity-50"
disabled={isLoading || isSearching}
className="flex-shrink-0 flex items-center justify-center gap-2 px-5 py-3.5 bg-[rgb(20,113,76)] hover:bg-[rgb(16,89,60)] text-white font-bold text-sm rounded-xl transition-all shadow-lg hover:shadow-xl disabled:opacity-80"
>
<Search className="w-5 h-5" />
Search
{isSearching ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Searching...
</>
) : (
<>
<Search className="w-5 h-5" />
Search
</>
)}
</button>
</div>
)}

View File

@@ -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: '#' },
];

View File

@@ -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<string, string> = {
@@ -203,10 +209,12 @@ export default function AppSidebar() {
)}
</div>
<ChangePasswordModal
isOpen={showChangePassword}
onClose={() => setShowChangePassword(false)}
/>
{showChangePassword && (
<ChangePasswordModal
isOpen={showChangePassword}
onClose={() => setShowChangePassword(false)}
/>
)}
</aside>
);
}

View File

@@ -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' });
}