feat: implement hero section with multi-trip booking widget and backend infrastructure
This commit is contained in:
parent
44995cb5db
commit
e7365b06ac
21 changed files with 4104 additions and 113 deletions
63
src/App.jsx
63
src/App.jsx
|
|
@ -1,13 +1,7 @@
|
|||
import { useState } from "react";
|
||||
import Navbar from "./components/Navbar/Navbar";
|
||||
import HeroSection from "./components/Hero/HeroSection";
|
||||
import CabsSection from "./components/Cabs/CabsSection";
|
||||
import ServicesSection from "./components/Services/ServicesSection";
|
||||
import PromoBanners from "./components/Promotions/PromoBanners";
|
||||
import ReviewsSection from "./components/Reviews/ReviewsSection";
|
||||
import WhyChooseUs from "./components/Highlights/WhyChooseUs";
|
||||
import NewsSection from "./components/News/NewsSection";
|
||||
import Footer from "./components/Footer/Footer";
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import SearchPage from "./pages/SearchPage";
|
||||
import AuthModal from "./components/Auth/AuthModal";
|
||||
import "./index.css";
|
||||
|
||||
|
|
@ -16,26 +10,37 @@ function App() {
|
|||
const [user, setUser] = useState(null);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Navbar
|
||||
user={user}
|
||||
onLoginClick={() => setIsAuthModalOpen(true)}
|
||||
/>
|
||||
<HeroSection />
|
||||
<CabsSection />
|
||||
<ServicesSection />
|
||||
<PromoBanners />
|
||||
<ReviewsSection />
|
||||
<WhyChooseUs />
|
||||
<NewsSection />
|
||||
<Footer />
|
||||
<AuthModal
|
||||
isOpen={isAuthModalOpen}
|
||||
onClose={() => setIsAuthModalOpen(false)}
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
/>
|
||||
</main>
|
||||
<Router>
|
||||
<main className="min-h-screen">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<HomePage
|
||||
onLoginClick={() => setIsAuthModalOpen(true)}
|
||||
user={user}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
element={
|
||||
<SearchPage
|
||||
onLoginClick={() => setIsAuthModalOpen(true)}
|
||||
user={user}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
<AuthModal
|
||||
isOpen={isAuthModalOpen}
|
||||
onClose={() => setIsAuthModalOpen(false)}
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
/>
|
||||
</main>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const PhoneInputScreen = ({ onRequestOtp, onCancel, onGoogleSuccess, onRegisterS
|
|||
// Register Form State
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [mobile_number, setMobileNumber] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
|
|
@ -20,7 +20,7 @@ const PhoneInputScreen = ({ onRequestOtp, onCancel, onGoogleSuccess, onRegisterS
|
|||
});
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!name || !email || !phone || !password) {
|
||||
if (!name || !email || !mobile_number || !password) {
|
||||
setErrorMsg("Please fill all fields.");
|
||||
return;
|
||||
}
|
||||
|
|
@ -33,12 +33,12 @@ const PhoneInputScreen = ({ onRequestOtp, onCancel, onGoogleSuccess, onRegisterS
|
|||
const response = await api.post("/api/auth/register", {
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
mobile_number,
|
||||
password,
|
||||
});
|
||||
|
||||
// Pass the successful name/email down the line dynamically
|
||||
onRegisterSuccess({ name: response.user?.name || name, email: response.user?.email || email, phone: response.user?.phone || phone });
|
||||
onRegisterSuccess({ name: response.user?.name || name, email: response.user?.email || email, mobile_number: response.user?.mobile_number || mobile_number });
|
||||
|
||||
} catch (err) {
|
||||
if (err.message.includes("409") || err.message.toLowerCase().includes("conflict") || err.message.toLowerCase().includes("already registered")) {
|
||||
|
|
@ -163,8 +163,8 @@ const PhoneInputScreen = ({ onRequestOtp, onCancel, onGoogleSuccess, onRegisterS
|
|||
<input
|
||||
type="tel"
|
||||
placeholder="Mobile Number"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
value={mobile_number}
|
||||
onChange={(e) => setMobileNumber(e.target.value)}
|
||||
className="flex-1 w-full outline-none px-4 py-3.5 text-sm font-semibold rounded-r-xl placeholder:font-normal"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
120
src/components/Hero/DateTimePicker.jsx
Normal file
120
src/components/Hero/DateTimePicker.jsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React from "react";
|
||||
import ReactDatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { Calendar, Clock } from "lucide-react";
|
||||
|
||||
// Custom theme for react-datepicker
|
||||
const calendarStyles = `
|
||||
.react-datepicker {
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.3);
|
||||
background: rgba(255, 255, 255, 0.98) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex !important;
|
||||
overflow: hidden;
|
||||
height: 280px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
.react-datepicker__header {
|
||||
background: transparent;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.react-datepicker__month-container {
|
||||
float: none !important;
|
||||
flex: 1;
|
||||
}
|
||||
.react-datepicker__time-container {
|
||||
float: none !important;
|
||||
width: 85px !important;
|
||||
border-left: 1px solid #f1f5f9;
|
||||
height: 280px !important;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.react-datepicker__time-box {
|
||||
width: 100% !important;
|
||||
height: 240px !important; /* Avoid header height */
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
.react-datepicker__time-list {
|
||||
padding: 0 !important;
|
||||
overflow-y: auto !important;
|
||||
height: calc(280px - 40px) !important;
|
||||
}
|
||||
.react-datepicker__time-list-item {
|
||||
height: auto !important;
|
||||
padding: 10px 0 !important;
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: #475569 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.react-datepicker__time-list-item:hover {
|
||||
background: #e2e8f0 !important;
|
||||
color: #4f46e5 !important;
|
||||
}
|
||||
.react-datepicker__time-list-item--selected {
|
||||
background-color: #4f46e5 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.react-datepicker__day-name, .react-datepicker__day {
|
||||
width: 2.2rem;
|
||||
line-height: 2.2rem;
|
||||
margin: 0.1rem;
|
||||
border-radius: 0.6rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b !important;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.react-datepicker__day--selected {
|
||||
background-color: #4f46e5 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.react-datepicker__day--keyboard-selected {
|
||||
background-color: #f1f5f9;
|
||||
color: #4f46e5 !important;
|
||||
}
|
||||
.react-datepicker__current-month, .react-datepicker__time-header {
|
||||
font-weight: 800 !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #0f172a !important;
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
.react-datepicker__navigation {
|
||||
top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const DateTimePicker = ({ label, selected, onChange, placeholder, minDate = new Date() }) => {
|
||||
return (
|
||||
<div className="flex-1 min-w-[200px] px-4 py-3 relative">
|
||||
<style>{calendarStyles}</style>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-dark-700 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">{label}</p>
|
||||
<div className="relative group">
|
||||
<ReactDatePicker
|
||||
selected={selected}
|
||||
onChange={onChange}
|
||||
showTimeSelect
|
||||
dateFormat="MMMM d, yyyy h:mm aa"
|
||||
minDate={minDate}
|
||||
placeholderText={placeholder}
|
||||
className="text-sm font-semibold text-dark-900 placeholder:text-gray-400 bg-transparent border-none outline-none w-full cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimePicker;
|
||||
|
|
@ -1,34 +1,30 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import heroBg from "../../assets/hero_bg.png";
|
||||
import LocationAutocomplete from "./LocationAutocomplete";
|
||||
import DateTimePicker from "./DateTimePicker";
|
||||
import { ArrowRight, Plane, Navigation, Repeat, MapPin } from "lucide-react";
|
||||
|
||||
const tripTypes = [
|
||||
{
|
||||
key: "round",
|
||||
label: "Round Trip",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 2l4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>
|
||||
),
|
||||
icon: <Repeat className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
key: "oneway",
|
||||
label: "One Way",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
),
|
||||
icon: <ArrowRight className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
key: "local",
|
||||
label: "Local Trip",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||
),
|
||||
icon: <Navigation className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
key: "airport",
|
||||
label: "Airport Trip",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"/></svg>
|
||||
),
|
||||
icon: <Plane className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -39,41 +35,108 @@ const localPackages = [
|
|||
];
|
||||
|
||||
const HeroSection = () => {
|
||||
const navigate = useNavigate();
|
||||
const [activeTrip, setActiveTrip] = useState("round");
|
||||
const [pickupLocation, setPickupLocation] = useState("");
|
||||
const [dropLocation, setDropLocation] = useState("");
|
||||
const [pickupDate, setPickupDate] = useState(null);
|
||||
const [returnDate, setReturnDate] = useState(null);
|
||||
const [selectedPackage, setSelectedPackage] = useState(localPackages[0]);
|
||||
const [airportName, setAirportName] = useState("");
|
||||
|
||||
const handleSearch = () => {
|
||||
if (!pickupLocation) {
|
||||
alert("Please enter a pickup location.");
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
tripType: activeTrip,
|
||||
pickup: pickupLocation,
|
||||
drop: dropLocation || airportName || "",
|
||||
pickupDate: pickupDate?.toISOString() || "",
|
||||
returnDate: returnDate?.toISOString() || "",
|
||||
package: activeTrip === 'local' ? selectedPackage : "",
|
||||
});
|
||||
|
||||
navigate(`/search?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
const getFormFields = () => {
|
||||
switch (activeTrip) {
|
||||
case "round":
|
||||
return (
|
||||
<>
|
||||
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||
<LocationAutocomplete
|
||||
label="From City"
|
||||
placeholder="Enter Pickup City"
|
||||
value={pickupLocation}
|
||||
onChange={setPickupLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="destination" label="To City" placeholder="Enter Drop Location" />
|
||||
<LocationAutocomplete
|
||||
label="To City"
|
||||
placeholder="Enter Drop Location"
|
||||
value={dropLocation}
|
||||
icon="destination"
|
||||
onChange={setDropLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||
<DateTimePicker
|
||||
label="Pickup date & time"
|
||||
placeholder="Select Date"
|
||||
selected={pickupDate}
|
||||
onChange={setPickupDate}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="calendar" label="Return date & time" placeholder="Select Date" />
|
||||
<DateTimePicker
|
||||
label="Return date & time"
|
||||
placeholder="Select Date"
|
||||
selected={returnDate}
|
||||
onChange={setReturnDate}
|
||||
minDate={pickupDate || new Date()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case "oneway":
|
||||
return (
|
||||
<>
|
||||
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||
<LocationAutocomplete
|
||||
label="From City"
|
||||
placeholder="Enter Pickup City"
|
||||
value={pickupLocation}
|
||||
onChange={setPickupLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="destination" label="To City" placeholder="Enter Drop Location" />
|
||||
<LocationAutocomplete
|
||||
label="To City"
|
||||
placeholder="Enter Drop Location"
|
||||
value={dropLocation}
|
||||
icon="destination"
|
||||
onChange={setDropLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||
<DateTimePicker
|
||||
label="Pickup date & time"
|
||||
placeholder="Select Date"
|
||||
selected={pickupDate}
|
||||
onChange={setPickupDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case "local":
|
||||
return (
|
||||
<>
|
||||
<FormField icon="location" label="From City" placeholder="Enter Pickup City" />
|
||||
<LocationAutocomplete
|
||||
label="From City"
|
||||
placeholder="Enter Pickup City"
|
||||
value={pickupLocation}
|
||||
onChange={setPickupLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<div className="flex-1 min-w-[160px] px-4 py-3">
|
||||
<div className="flex-1 min-w-[200px] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-primary-600 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="20" height="14" x="2" y="7" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>
|
||||
<Navigation className="w-4 h-4 text-primary-600 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">Select Package</p>
|
||||
<select
|
||||
|
|
@ -89,17 +152,38 @@ const HeroSection = () => {
|
|||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||
<DateTimePicker
|
||||
label="Pickup date & time"
|
||||
placeholder="Select Date"
|
||||
selected={pickupDate}
|
||||
onChange={setPickupDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case "airport":
|
||||
return (
|
||||
<>
|
||||
<FormField icon="location" label="From" placeholder="Enter Pickup Location" />
|
||||
<LocationAutocomplete
|
||||
label="From"
|
||||
placeholder="Enter Pickup Location"
|
||||
value={pickupLocation}
|
||||
onChange={setPickupLocation}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="destination" label="Airport" placeholder="Enter Airport Name" />
|
||||
<LocationAutocomplete
|
||||
label="Airport"
|
||||
placeholder="Enter Airport Name"
|
||||
value={airportName}
|
||||
icon="destination"
|
||||
onChange={setAirportName}
|
||||
/>
|
||||
<Divider />
|
||||
<FormField icon="calendar" label="Pickup date & time" placeholder="Select Date" />
|
||||
<DateTimePicker
|
||||
label="Pickup date & time"
|
||||
placeholder="Select Date"
|
||||
selected={pickupDate}
|
||||
onChange={setPickupDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
|
|
@ -127,7 +211,7 @@ const HeroSection = () => {
|
|||
</h1>
|
||||
|
||||
{/* Booking widget */}
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
{/* Trip tabs */}
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="inline-flex gap-1 p-1.5 bg-white/90 backdrop-blur-sm rounded-2xl shadow-lg border border-white/20">
|
||||
|
|
@ -157,13 +241,16 @@ const HeroSection = () => {
|
|||
|
||||
{/* Form */}
|
||||
<div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-2">
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-0">
|
||||
<div className="flex flex-col xl:flex-row items-stretch xl:items-center gap-1 sm:gap-0">
|
||||
{getFormFields()}
|
||||
|
||||
{/* CTA Button */}
|
||||
<button className="flex-shrink-0 inline-flex items-center justify-center gap-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold text-sm px-6 py-3.5 rounded-xl transition-all duration-200 shadow-md shadow-primary-600/20 hover:shadow-lg hover:shadow-primary-600/30 cursor-pointer mx-2 sm:mx-0 sm:ml-auto">
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className="flex-shrink-0 inline-flex items-center justify-center gap-2 bg-primary-600 hover:bg-primary-700 text-white font-semibold text-sm px-6 py-4 rounded-xl transition-all duration-200 shadow-md shadow-primary-600/20 hover:shadow-lg hover:shadow-primary-600/30 cursor-pointer m-1 xl:ml-auto"
|
||||
>
|
||||
Start Booking Now
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -175,38 +262,8 @@ const HeroSection = () => {
|
|||
|
||||
/* --- Sub-components --- */
|
||||
|
||||
const FormField = ({ icon, label, placeholder }) => {
|
||||
const icons = {
|
||||
location: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-red-500 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
),
|
||||
destination: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-primary-600 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
),
|
||||
calendar: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-dark-700 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[160px] px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{icons[icon]}
|
||||
<div>
|
||||
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">{label}</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
className="text-sm font-semibold text-dark-900 placeholder:text-gray-400 bg-transparent border-none outline-none w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Divider = () => (
|
||||
<div className="hidden sm:block w-px h-10 bg-gray-200 self-center flex-shrink-0" />
|
||||
<div className="hidden xl:block w-px h-10 bg-gray-200 self-center flex-shrink-0 mx-2" />
|
||||
);
|
||||
|
||||
export default HeroSection;
|
||||
|
|
|
|||
109
src/components/Hero/LocationAutocomplete.jsx
Normal file
109
src/components/Hero/LocationAutocomplete.jsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { MapPin, Loader2 } from "lucide-react";
|
||||
|
||||
const LocationAutocomplete = ({ label, value, onChange, placeholder, icon = "location" }) => {
|
||||
const [query, setQuery] = useState(value || "");
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const fetchSuggestions = async (searchText) => {
|
||||
if (searchText.length < 3) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(`https://photon.komoot.io/api?q=${encodeURIComponent(searchText)}&limit=5`);
|
||||
const data = await response.json();
|
||||
|
||||
const results = data.features.map(feature => {
|
||||
const { name, city, state, country } = feature.properties;
|
||||
const mainText = name || city;
|
||||
const secondaryText = [city, state, country].filter(Boolean).join(", ");
|
||||
return {
|
||||
id: feature.geometry.coordinates.join(","),
|
||||
mainText,
|
||||
secondaryText,
|
||||
description: `${mainText}${secondaryText ? `, ${secondaryText}` : ""}`
|
||||
};
|
||||
});
|
||||
|
||||
setSuggestions(results);
|
||||
setShowDropdown(results.length > 0);
|
||||
} catch (error) {
|
||||
console.error("Error fetching location suggestions:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedFetch = useRef(debounce(fetchSuggestions, 500)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (query !== value) {
|
||||
debouncedFetch(query);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setShowDropdown(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (suggestion) => {
|
||||
setQuery(suggestion.description);
|
||||
onChange(suggestion.description);
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[200px] px-4 py-3 relative" ref={dropdownRef}>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className={`w-4 h-4 ${icon === 'location' ? 'text-red-500' : 'text-primary-600'} flex-shrink-0`} />
|
||||
<div className="flex-1">
|
||||
<p className="text-[11px] text-badge-text font-medium leading-none mb-1">{label}</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onFocus={() => query.length >= 3 && setShowDropdown(true)}
|
||||
placeholder={placeholder}
|
||||
className="text-sm font-semibold text-dark-900 placeholder:text-gray-400 bg-transparent border-none outline-none w-full"
|
||||
/>
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-400 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Suggestions */}
|
||||
{showDropdown && suggestions.length > 0 && (
|
||||
<div className="absolute left-0 right-0 top-full mt-2 bg-white/95 backdrop-blur-md rounded-xl shadow-2xl border border-gray-100 z-50 overflow-hidden animate-fadeInUp">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelect(suggestion)}
|
||||
className="w-full text-left px-5 py-3 hover:bg-gray-50 transition-colors flex flex-col border-b border-gray-50 last:border-none"
|
||||
>
|
||||
<span className="text-sm font-bold text-dark-900">{suggestion.mainText}</span>
|
||||
<span className="text-[11px] text-gray-500 truncate">{suggestion.secondaryText}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationAutocomplete;
|
||||
|
|
@ -5,10 +5,10 @@ const Navbar = ({ onLoginClick, user }) => {
|
|||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Home", href: "#hero" },
|
||||
{ label: "Our Fleet", href: "#fleet" },
|
||||
{ label: "About", href: "#about" },
|
||||
{ label: "Contact", href: "#contact" },
|
||||
{ label: "Home", href: "/#hero" },
|
||||
{ label: "Our Fleet", href: "/#fleet" },
|
||||
{ label: "About", href: "/#about" },
|
||||
{ label: "Contact", href: "/#contact" },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -53,7 +53,7 @@ const Navbar = ({ onLoginClick, user }) => {
|
|||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16 md:h-[72px]">
|
||||
{/* Logo */}
|
||||
<a href="#" className="flex-shrink-0 hover:opacity-90 transition-opacity">
|
||||
<a href="/" className="flex-shrink-0 hover:opacity-90 transition-opacity">
|
||||
<Logo />
|
||||
</a>
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ const Navbar = ({ onLoginClick, user }) => {
|
|||
{/* Book Now CTA */}
|
||||
<div className="hidden md:flex items-center">
|
||||
<a
|
||||
href="#hero"
|
||||
href="/#hero"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 border-2 border-primary-600 text-primary-600 font-semibold text-sm rounded-xl hover:bg-primary-600 hover:text-white transition-all duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2" /><line x1="16" x2="16" y1="2" y2="6" /><line x1="8" x2="8" y1="2" y2="6" /><line x1="3" x2="21" y1="10" y2="10" /></svg>
|
||||
|
|
@ -112,7 +112,7 @@ const Navbar = ({ onLoginClick, user }) => {
|
|||
</a>
|
||||
))}
|
||||
<a
|
||||
href="#hero"
|
||||
href="/#hero"
|
||||
className="block mx-4 mt-3 px-4 py-2.5 text-center bg-primary-600 text-white font-semibold text-sm rounded-xl"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
|
|
|
|||
31
src/pages/HomePage.jsx
Normal file
31
src/pages/HomePage.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import HeroSection from "../components/Hero/HeroSection";
|
||||
import CabsSection from "../components/Cabs/CabsSection";
|
||||
import ServicesSection from "../components/Services/ServicesSection";
|
||||
import PromoBanners from "../components/Promotions/PromoBanners";
|
||||
import ReviewsSection from "../components/Reviews/ReviewsSection";
|
||||
import WhyChooseUs from "../components/Highlights/WhyChooseUs";
|
||||
import NewsSection from "../components/News/NewsSection";
|
||||
import Navbar from "../components/Navbar/Navbar";
|
||||
import Footer from "../components/Footer/Footer";
|
||||
|
||||
const HomePage = ({ onLoginClick, user }) => {
|
||||
return (
|
||||
<>
|
||||
<Navbar
|
||||
user={user}
|
||||
onLoginClick={onLoginClick}
|
||||
/>
|
||||
<HeroSection />
|
||||
<CabsSection />
|
||||
<ServicesSection />
|
||||
<PromoBanners />
|
||||
<ReviewsSection />
|
||||
<WhyChooseUs />
|
||||
<NewsSection />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
478
src/pages/SearchPage.jsx
Normal file
478
src/pages/SearchPage.jsx
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { useSearchParams, Link } from "react-router-dom";
|
||||
import {
|
||||
Calendar, MapPin, Users, Luggage, ShieldCheck,
|
||||
ArrowLeft, Info, CheckCircle2, Loader2, X,
|
||||
ChevronRight, ArrowRight, User, Phone
|
||||
} from "lucide-react";
|
||||
import Navbar from "../components/Navbar/Navbar";
|
||||
import Footer from "../components/Footer/Footer";
|
||||
import { api } from "../lib/api";
|
||||
|
||||
// Import local assets
|
||||
import innovaCrystaImg from "../assets/vehicles/innova_crysta.png";
|
||||
import innovaImg from "../assets/vehicles/innova.png";
|
||||
import fortunerImg from "../assets/vehicles/fortuner.png";
|
||||
import dzireImg from "../assets/vehicles/dzire.png";
|
||||
import etiosImg from "../assets/vehicles/etios.png";
|
||||
import audiA6Img from "../assets/vehicles/audi_a6.png";
|
||||
import audiQ7Img from "../assets/vehicles/audi_q7.png";
|
||||
import bmw5Img from "../assets/vehicles/bmw_5_series.png";
|
||||
import bmw7Img from "../assets/vehicles/bmw_7_series.png";
|
||||
import camryImg from "../assets/vehicles/camry.png";
|
||||
import corollaImg from "../assets/vehicles/corolla_altis.png";
|
||||
import mercEImg from "../assets/vehicles/mercedes_e_class.png";
|
||||
import mercSImg from "../assets/vehicles/mercedes_s_class.png";
|
||||
import commuterImg from "../assets/vehicles/toyota_commuter.png";
|
||||
|
||||
const carImages = {
|
||||
innova_crysta: innovaCrystaImg,
|
||||
innova: innovaImg,
|
||||
fortuner: fortunerImg,
|
||||
dzire: dzireImg,
|
||||
etios: etiosImg,
|
||||
audi_a6: audiA6Img,
|
||||
audi_q7: audiQ7Img,
|
||||
bmw_5_series: bmw5Img,
|
||||
bmw_7_series: bmw7Img,
|
||||
camry: camryImg,
|
||||
corolla_altis: corollaImg,
|
||||
mercedes_e_class: mercEImg,
|
||||
mercedes_s_class: mercSImg,
|
||||
toyota_commuter: commuterImg,
|
||||
ertiga: dzireImg, // Fallback
|
||||
};
|
||||
|
||||
const LeadCaptureModal = ({ isOpen, onSubmit, searchData }) => {
|
||||
const [name, setName] = useState("");
|
||||
const [mobile, setMobile] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await api.post("/api/leads", {
|
||||
name,
|
||||
mobile_number: mobile,
|
||||
tripType: searchData.tripType,
|
||||
pickupLocation: searchData.pickup,
|
||||
dropLocation: searchData.drop
|
||||
});
|
||||
onSubmit();
|
||||
} catch (err) {
|
||||
console.error("Lead Error:", err);
|
||||
// Still allow them to see results even if lead fails, or show error
|
||||
onSubmit();
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-dark-900/40 backdrop-blur-md animate-fadeIn">
|
||||
<div className="bg-white rounded-3xl p-8 max-w-md w-full shadow-2xl animate-scaleIn">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-primary-50 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<User className="w-8 h-8 text-primary-600" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black text-dark-900 mb-2">Check Special Prices</h2>
|
||||
<p className="text-gray-500 text-sm">Enter your details to view exclusive discounted rates for your trip.</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="relative">
|
||||
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
placeholder="Your Full Name"
|
||||
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none focus:border-primary-600 focus:bg-white transition-all font-semibold"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
required
|
||||
type="tel"
|
||||
placeholder="Mobile Number"
|
||||
className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl outline-none focus:border-primary-600 focus:bg-white transition-all font-semibold"
|
||||
value={mobile}
|
||||
onChange={(e) => setMobile(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-black py-4 rounded-2xl transition-all shadow-lg shadow-primary-600/20 flex items-center justify-center gap-2 group"
|
||||
>
|
||||
{isSubmitting ? <Loader2 className="w-5 h-5 animate-spin" /> : (
|
||||
<>
|
||||
View Deals
|
||||
<ArrowRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-[10px] text-gray-400 mt-6 font-bold uppercase tracking-widest">
|
||||
No spam. Only trip updates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchPage = ({ onLoginClick, user }) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isBooking, setIsBooking] = useState(false);
|
||||
const [bookingSuccess, setBookingSuccess] = useState(null);
|
||||
const [showLeadModal, setShowLeadModal] = useState(!user); // Don't show if user logged in
|
||||
const [cars, setCars] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Extract search details
|
||||
const tripType = searchParams.get("tripType") || "round";
|
||||
const pickupLocation = searchParams.get("pickup") || "Not Specified";
|
||||
const dropLocation = searchParams.get("drop") || "";
|
||||
const pickupDateStr = searchParams.get("pickupDate");
|
||||
const returnDateStr = searchParams.get("returnDate");
|
||||
const packageType = searchParams.get("package") || "";
|
||||
|
||||
const pickupDate = pickupDateStr ? new Date(pickupDateStr) : new Date();
|
||||
const returnDate = returnDateStr ? new Date(returnDateStr) : null;
|
||||
|
||||
useEffect(() => {
|
||||
fetchPricing();
|
||||
}, [tripType, pickupDateStr, returnDateStr, packageType]);
|
||||
|
||||
const fetchPricing = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await api.post("/api/pricing/calculate", {
|
||||
tripType,
|
||||
pickupDate: pickupDateStr,
|
||||
returnDate: returnDateStr,
|
||||
packageType
|
||||
});
|
||||
setCars(response.data);
|
||||
} catch (err) {
|
||||
console.error("Pricing Fetch Error:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookNow = async (car) => {
|
||||
// If not logged in, ask for lead first (Leads-first flow)
|
||||
if (!user && showLeadModal) {
|
||||
// Modal is already handling this or will appear
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBooking(true);
|
||||
try {
|
||||
const response = await api.post("/api/bookings", {
|
||||
tripType,
|
||||
pickupLocation,
|
||||
dropLocation,
|
||||
pickupDateTime: pickupDate.toISOString(),
|
||||
returnDateTime: returnDate ? returnDate.toISOString() : null,
|
||||
packageType,
|
||||
carName: car.name,
|
||||
totalAmount: car.calc.grandTotal
|
||||
});
|
||||
|
||||
setBookingSuccess(response.bookingId);
|
||||
} catch (err) {
|
||||
alert("Failed to create booking: " + err.message);
|
||||
} finally {
|
||||
setIsBooking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<Navbar onLoginClick={onLoginClick} user={user} />
|
||||
|
||||
{/* Lead Capture Modal */}
|
||||
<LeadCaptureModal
|
||||
isOpen={showLeadModal}
|
||||
onSubmit={() => setShowLeadModal(false)}
|
||||
searchData={{ tripType, pickup: pickupLocation, drop: dropLocation }}
|
||||
/>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 pt-28 flex-grow">
|
||||
|
||||
{/* Search Summary Header */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6 animate-fadeIn">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link to="/" className="flex items-center gap-2 text-primary-600 font-bold text-sm hover:translate-x-[-4px] transition-transform w-fit uppercase tracking-wider">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Modify Search
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-y-3 gap-x-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-primary-50 p-2 rounded-lg text-primary-600 font-bold">
|
||||
<MapPin className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">Pickup</p>
|
||||
<p className="text-sm font-bold text-dark-900">{pickupLocation}</p>
|
||||
</div>
|
||||
</div>
|
||||
{dropLocation && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-red-50 p-2 rounded-lg text-red-500 font-bold">
|
||||
<MapPin className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">Drop Location</p>
|
||||
<p className="text-sm font-bold text-dark-900">{dropLocation}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-orange-50 p-2 rounded-lg text-orange-600 font-bold">
|
||||
<Calendar className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest">Schedule</p>
|
||||
<p className="text-sm font-bold text-dark-900">
|
||||
{pickupDate.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
{returnDate && ` - ${returnDate.toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 pb-20">
|
||||
|
||||
{/* Main Listings */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-xl font-black text-dark-900 uppercase tracking-tight">Available Fleets</h2>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest bg-white px-3 py-1.5 rounded-full border border-gray-100 shadow-sm">
|
||||
Found {cars.length} Options
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="bg-white rounded-3xl p-20 flex flex-col items-center justify-center shadow-sm border border-gray-100">
|
||||
<Loader2 className="w-10 h-10 text-primary-600 animate-spin mb-4" />
|
||||
<p className="text-gray-500 font-bold animate-pulse uppercase tracking-widest text-xs">Finding Best Fares...</p>
|
||||
</div>
|
||||
) : cars.map((car) => (
|
||||
<div key={car.id} className="bg-white rounded-3xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-2xl hover:shadow-primary-600/5 transition-all duration-500 group">
|
||||
<div className="flex flex-col xl:flex-row p-6 gap-8">
|
||||
{/* Car Image Wrapper */}
|
||||
<div className="xl:w-[280px] bg-gray-50 rounded-2xl flex items-center justify-center p-6 relative overflow-hidden group-hover:bg-primary-50/30 transition-colors">
|
||||
<img src={carImages[car.image]} alt={car.name} className="w-full h-auto object-contain transform group-hover:scale-110 transition-transform duration-700 drop-shadow-xl" />
|
||||
|
||||
{/* Badge */}
|
||||
{car.badge && (
|
||||
<div className="absolute top-4 left-4 bg-primary-600 text-white text-[9px] font-black px-3 py-1.5 rounded-full shadow-lg uppercase tracking-widest animate-bounceIn">
|
||||
{car.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-4 right-4 bg-white/90 backdrop-blur-sm px-2.5 py-1.5 rounded-xl border border-white flex items-center gap-1.5 shadow-sm">
|
||||
<span className="text-yellow-500 font-black tracking-tighter">★★★★★</span>
|
||||
<span className="text-[10px] font-black text-dark-900">Top Rated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Car Details */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-dark-900 group-hover:text-primary-600 transition-colors">{car.name}</h3>
|
||||
<div className="flex items-center gap-4 mt-1.5">
|
||||
<span className="text-[10px] font-black text-primary-600 bg-primary-50 px-2.5 py-1 rounded-lg uppercase tracking-widest">{car.type}</span>
|
||||
<div className="flex items-center gap-3 text-gray-400">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span className="text-[11px] font-bold">{car.seats}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Luggage className="w-3.5 h-3.5" />
|
||||
<span className="text-[11px] font-bold">{car.luggage}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-3xl font-black text-dark-900 leading-none">₹{car.calc.grandTotal}</p>
|
||||
<p className="text-[9px] font-black text-gray-400 mt-2 uppercase tracking-widest border-t border-gray-50 pt-2 inline-block">Final Amount</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transparency Math Block */}
|
||||
<div className="bg-primary-50/50 rounded-2xl p-4 mb-6 border border-primary-100/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-black text-primary-900 uppercase tracking-widest flex items-center gap-1.5">
|
||||
<Info className="w-3.5 h-3.5" />
|
||||
Transparent Breakdown
|
||||
</span>
|
||||
<span className="text-[11px] font-black text-primary-600">{car.calc.math}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase tracking-widest font-mono">20% Booking Advance</span>
|
||||
<span className="text-sm font-black text-primary-600">Pay ₹{car.calc.advancePayment} to Confirm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inclusions / Exclusions */}
|
||||
<div className="grid grid-cols-2 gap-4 pb-6 border-b border-gray-50">
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-green-600 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||
<CheckCircle2 className="w-3 h-3" /> Included
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{car.inclusions.map((inc, i) => (
|
||||
<li key={i} className="text-[11px] font-bold text-gray-500 flex items-center gap-1.5">
|
||||
<div className="w-1 h-1 bg-green-500 rounded-full" /> {inc}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-red-500 uppercase tracking-widest mb-2 flex items-center gap-1.5">
|
||||
<X className="w-3 h-3" /> Excluded
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{car.exclusions.map((exc, i) => (
|
||||
<li key={i} className="text-[11px] font-bold text-gray-400 flex items-center gap-1.5 line-through decoration-red-200">
|
||||
{exc}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Available Distance</span>
|
||||
<span className="text-sm font-black text-dark-900">{car.calc.totalKmIncluded} Km Included</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleBookNow(car)}
|
||||
disabled={isBooking}
|
||||
className="bg-dark-900 hover:bg-primary-600 text-white font-black text-xs px-8 py-4 rounded-2xl transition-all duration-300 shadow-xl shadow-dark-900/10 hover:shadow-primary-600/30 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 group"
|
||||
>
|
||||
{isBooking ? <Loader2 className="w-4 h-4 animate-spin text-white" /> : (
|
||||
<>
|
||||
Book for ₹{car.calc.advancePayment} <ChevronRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pricing Summary Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="sticky top-28 space-y-6">
|
||||
<div className="bg-white rounded-3xl shadow-sm border border-gray-100 p-8 overflow-hidden relative group">
|
||||
{/* Decorative element */}
|
||||
<div className="absolute -top-10 -right-10 w-32 h-32 bg-primary-50 rounded-full opacity-50 blur-3xl group-hover:bg-primary-100 transition-colors duration-500" />
|
||||
|
||||
<h2 className="text-xl font-black text-dark-900 mb-8 flex items-center gap-3 relative z-10">
|
||||
Terms & Policy
|
||||
< ShieldCheck className="w-5 h-5 text-green-500" />
|
||||
</h2>
|
||||
|
||||
<div className="space-y-5 relative z-10">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 w-5 h-5 bg-green-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<CheckCircle2 className="w-3 h-3 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-dark-900 mb-0.5">Toll & State Permits</p>
|
||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">To be paid by traveller as per actuals during the trip.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 w-5 h-5 bg-primary-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Info className="w-3 h-3 text-primary-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-dark-900 mb-0.5">Extra Kilimoteres</p>
|
||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Charged at standard base rates mentioned on the car cards.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-1 w-5 h-5 bg-orange-50 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Calendar className="w-3 h-3 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-dark-900 mb-0.5">Night Charge</p>
|
||||
<p className="text-[11px] font-bold text-gray-500 leading-relaxed">Driver night allowance applicable after 10:00 PM.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 p-5 bg-gray-50 rounded-2xl border border-gray-100 text-center">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1.5">Need Custom Help?</p>
|
||||
<a href="tel:+917975942520" className="text-sm font-black text-dark-900 hover:text-primary-600 transition-colors">+91 7975942520</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-dark-900 rounded-3xl p-8 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary-600/20 rounded-full blur-3xl" />
|
||||
<h3 className="text-lg font-black mb-4 flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5 text-primary-400" />
|
||||
Refund Guarantee
|
||||
</h3>
|
||||
<p className="text-xs font-bold text-gray-400 leading-relaxed">
|
||||
Cancel up to 24 hours in advance and receive a full refund of your 20% booking deposit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Success Modal */}
|
||||
{bookingSuccess && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-dark-900/60 backdrop-blur-sm animate-fadeIn">
|
||||
<div className="bg-white rounded-[40px] p-10 max-w-sm w-full text-center shadow-3xl animate-scaleIn border border-white">
|
||||
<div className="w-24 h-24 bg-green-50 rounded-[35px] flex items-center justify-center mx-auto mb-8 shadow-inner">
|
||||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-black text-dark-900 mb-3 tracking-tighter">Trip Confirmed!</h2>
|
||||
<p className="text-gray-500 text-sm mb-10 leading-relaxed px-2 font-bold">
|
||||
We've received your booking. Our professional driver will reach out to you shortly.
|
||||
</p>
|
||||
<div className="bg-gray-50/80 rounded-[28px] p-6 mb-10 border border-gray-100">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">Booking Transaction ID</p>
|
||||
<p className="text-2xl font-black text-dark-900 tracking-tighter">#RR-{bookingSuccess}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setBookingSuccess(null)}
|
||||
className="w-full bg-dark-900 hover:bg-black text-white font-black py-5 rounded-2xl transition-all shadow-xl shadow-dark-900/20 active:scale-95"
|
||||
>
|
||||
Done & Back to Home
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
||||
Loading…
Add table
Add a link
Reference in a new issue