mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Update contact information
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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: '#' },
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user