feat: implement hero section with multi-trip booking widget and backend infrastructure

This commit is contained in:
AnuragJangid18 2026-04-04 22:04:48 +05:30
parent 44995cb5db
commit e7365b06ac
21 changed files with 4104 additions and 113 deletions

23
.env Normal file
View file

@ -0,0 +1,23 @@
# This was inserted by `prisma init`:
# Environment variables declared in this file are NOT automatically loaded by Prisma.
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
# The following `prisma+postgres` URL is similar to the URL produced by running a local Prisma Postgres
# server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.
# Replace 'YOUR_PASSWORD_HERE' with your actual MySQL Root Password
DATABASE_URL="mysql://root:Z41kfdw8e8%40sql@localhost:3306/roadrentals"
# Individual DB vars for the Express server (mariadb driver)
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=Z41kfdw8e8@sql
DB_NAME=roadrentals

2
.gitignore vendored
View file

@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/src/generated/prisma

386
backend/index.js Normal file
View file

@ -0,0 +1,386 @@
import "dotenv/config";
import express from "express";
import mysql from "mysql2/promise";
// Create a connection pool to MySQL
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "roadrentals",
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
const app = express();
app.use(express.json());
const checkToken = (req, res, next) => {
console.log("Welcome");
next();
};
app.use(checkToken);
app.get("/", (req, res) => {
res.json({ status: 1, message: "Home Page API" });
});
app.get("/api/news", (req, res) => {
res.json({ status: 1, message: "News API" });
});
// Database Connected Endpoint
app.post("/api/auth/register", async (req, res) => {
try {
const { name, email, mobile_number, password } = req.body;
if (!name || !email || !mobile_number || !password) {
return res.status(400).json({ detail: "Missing required fields" });
}
// Search the MySQL Database
const [users] = await pool.execute("SELECT * FROM User WHERE email = ?", [email]);
const existingUser = users[0];
if (existingUser) {
return res.status(409).json({ detail: "Email already registered" });
}
// Insert new user row into MySQL table
const [result] = await pool.execute(
"INSERT INTO User (name, email, mobile_number, password) VALUES (?, ?, ?, ?)",
[name, email, mobile_number, password]
);
const newUser = {
id: result.insertId,
name,
email,
mobile_number
};
return res.status(201).json({ status: 1, message: "User registered successfully", user: newUser });
} catch (error) {
console.error("Database Error:", error);
return res.status(500).json({ detail: "Internal Server Error" });
}
});
// Create a New Booking
app.post("/api/bookings", async (req, res) => {
try {
const {
tripType,
pickupLocation,
dropLocation,
pickupDateTime,
returnDateTime,
packageType,
carName,
totalAmount
} = req.body;
if (!tripType || !pickupLocation || !pickupDateTime) {
return res.status(400).json({ detail: "Missing required booking details" });
}
const [result] = await pool.execute(
`INSERT INTO Bookings (trip_type, pickup_location, drop_location, pickup_datetime, return_datetime, package_type, car_name, total_amount)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
tripType,
pickupLocation,
dropLocation || null,
new Date(pickupDateTime),
returnDateTime ? new Date(returnDateTime) : null,
packageType || null,
carName || null,
totalAmount || null
]
);
return res.status(201).json({
status: 1,
message: "Booking requested successfully!",
bookingId: result.insertId
});
} catch (error) {
console.error("Booking Error:", error);
return res.status(500).json({ detail: "Failed to create booking" });
}
});
// Pricing Engine & Car Data
const VEHICLES = [
// LUXURY TIER
{
id: 1,
name: "Audi A6 Premium",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "audi_a6",
baseFarePerDay: 12000,
driverAllowancePerDay: 600,
extraKmRate: 45,
includedKmPerDay: 250,
badge: "Executive",
features: ["Leather Interior", "Dual Climate", "Premium Audio", "Sunroof"]
},
{
id: 2,
name: "BMW 7 Series",
type: "LUXURY",
seats: "4+1",
luggage: "3 Bags",
image: "bmw_7_series",
baseFarePerDay: 25000,
driverAllowancePerDay: 800,
extraKmRate: 85,
includedKmPerDay: 200,
badge: "VVIP Choice",
features: ["Rear-seat Entertainment", "Massage Seats", "Quiet Cabin", "Chauffeur Driven"]
},
{
id: 3,
name: "Mercedes S-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_s_class",
baseFarePerDay: 28000,
driverAllowancePerDay: 800,
extraKmRate: 90,
includedKmPerDay: 200,
badge: "Ultimate Luxury",
features: ["Ambient Lighting", "Burmester Sound", "Premium Chauffeur"]
},
// SUV/MUV TIER
{
id: 4,
name: "Toyota Fortuner 4x4",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "fortuner",
baseFarePerDay: 6500,
driverAllowancePerDay: 500,
extraKmRate: 25,
includedKmPerDay: 300,
badge: "Advanture King",
features: ["Off-road capable", "Powerful AC", "Spacious", "Rugged Built"]
},
{
id: 5,
name: "Innova Crysta",
type: "MUV",
seats: "7+1",
luggage: "3 Bags",
image: "innova_crysta",
baseFarePerDay: 4500,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Best Seller",
features: ["Clean Interiors", "Captain Seats", "Smooth Ride", "Carrier Available"]
},
{
id: 6,
name: "Toyota Innova",
type: "MUV",
seats: "7+1",
luggage: "2 Bags",
image: "innova",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Value MUV",
features: ["Comfortable", "Spacious", "Economical for Family"]
},
// SEDAN TIER
{
id: 7,
name: "Toyota Camry",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "camry",
baseFarePerDay: 4200,
driverAllowancePerDay: 400,
extraKmRate: 18,
includedKmPerDay: 300,
badge: "Corporate Elite",
features: ["Hybrid Tech", "Quiet Ride", "Executive Space"]
},
{
id: 8,
name: "Toyota Etios",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "etios",
baseFarePerDay: 2800,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic",
features: ["Spacious Boot", "Good Legroom", "Standard AC"]
},
{
id: 9,
name: "Swift Dzire",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "dzire",
baseFarePerDay: 2600,
driverAllowancePerDay: 400,
extraKmRate: 13,
includedKmPerDay: 300,
badge: "Economic Choice",
features: ["Popular", "Quick Maneuver", "Efficient AC"]
},
// GROUP TRAVEL
{
id: 11,
name: "Audi Q7 Luxury SUV",
type: "SUV",
seats: "6+1",
luggage: "4 Bags",
image: "audi_q7",
baseFarePerDay: 15000,
driverAllowancePerDay: 700,
extraKmRate: 55,
includedKmPerDay: 250,
badge: "Luxury SUV",
features: ["Quattro 4x4", "Panoramic Sunroof", "Premium Audio", "Heated Seats"]
},
{
id: 12,
name: "BMW 5 Series",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "bmw_5_series",
baseFarePerDay: 14000,
driverAllowancePerDay: 600,
extraKmRate: 50,
includedKmPerDay: 250,
badge: "Business Class",
features: ["Dynamic Drive", "Luxury Interiors", "Silent Hybrid", "Touchscreen Control"]
},
{
id: 13,
name: "Mercedes E-Class",
type: "LUXURY",
seats: "4+1",
luggage: "2 Bags",
image: "mercedes_e_class",
baseFarePerDay: 14500,
driverAllowancePerDay: 600,
extraKmRate: 52,
includedKmPerDay: 250,
badge: "Timeless Elegance",
features: ["Soft Close Doors", "Ambient Light", "Professional Driver", "WIFI Enabled"]
},
{
id: 14,
name: "Toyota Corolla Altis",
type: "SEDAN",
seats: "4+1",
luggage: "2 Bags",
image: "corolla_altis",
baseFarePerDay: 3800,
driverAllowancePerDay: 400,
extraKmRate: 16,
includedKmPerDay: 300,
badge: "Executive Sedan",
features: ["Durable", "Comfortable Suspension", "Clean Air Filter"]
}
];
// Lead Capture
app.post("/api/leads", async (req, res) => {
try {
const { name, mobile_number, tripType, pickupLocation, dropLocation } = req.body;
if (!name || !mobile_number) return res.status(400).json({ detail: "Name and Mobile are required" });
await pool.execute(
`INSERT INTO Leads (name, mobile_number, trip_type, pickup_location, drop_location) VALUES (?, ?, ?, ?, ?)`,
[name, mobile_number, tripType || null, pickupLocation || null, dropLocation || null]
);
res.status(201).json({ status: 1, message: "Lead captured" });
} catch (error) {
console.error("Lead Error:", error);
res.status(500).json({ detail: "Failed to capture lead" });
}
});
// Centralized Pricing Calculator
app.post("/api/pricing/calculate", async (req, res) => {
try {
const { tripType, pickupDate, returnDate, packageType } = req.body;
const start = new Date(pickupDate);
const end = returnDate ? new Date(returnDate) : start;
const diffTime = Math.abs(end - start);
const durationDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) || 1;
const results = VEHICLES.map(vehicle => {
let totalBaseFare = vehicle.baseFarePerDay * durationDays;
let totalDriverAllowance = vehicle.driverAllowancePerDay * durationDays;
// Special Case: Local Packages
if (tripType === 'local' && packageType) {
if (packageType.includes("4Hr")) totalBaseFare = 1500;
else if (packageType.includes("8Hr")) totalBaseFare = 2500;
else if (packageType.includes("12Hr")) totalBaseFare = 3500;
totalDriverAllowance = 0;
}
// Special Case: Airport Transfers (Flat Rates)
if (tripType === 'airport') {
if (vehicle.type === 'SEDAN') totalBaseFare = 1200;
else if (vehicle.type === 'SUV') totalBaseFare = 2200;
else totalBaseFare = 1800; // MUV/Other
totalDriverAllowance = 0;
}
const grandTotal = totalBaseFare + totalDriverAllowance;
const advancePayment = Math.round(grandTotal * 0.20);
return {
...vehicle,
calc: {
days: durationDays,
baseFare: totalBaseFare,
driverAllowance: totalDriverAllowance,
totalKmIncluded: vehicle.includedKmPerDay * (tripType === 'airport' ? 0.2 : durationDays), // Approx for airport
grandTotal,
advancePayment,
math: tripType === 'airport' || tripType === 'local'
? `Fixed ${tripType.charAt(0).toUpperCase() + tripType.slice(1)} Rate`
: `(₹${vehicle.baseFarePerDay} x ${durationDays} Days) + ₹${totalDriverAllowance} Driver`
},
inclusions: ["AC", "Professional Driver", tripType === 'airport' ? "Airport Toll Included" : "First " + (vehicle.includedKmPerDay * durationDays) + " Kms"],
exclusions: ["Parking Charges", "Extra Waiting Time", "Inter-state Permits"]
};
});
res.json({ status: 1, data: results });
} catch (error) {
console.error("Pricing Error:", error);
res.status(500).json({ detail: "Failed to calculate pricing" });
}
});
const PORT = 8000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

2633
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,12 +7,20 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"start-backend": "node backend/index.js"
},
"dependencies": {
"@react-oauth/google": "^0.13.4",
"dotenv": "^17.4.0",
"express": "^5.2.1",
"lodash": "^4.18.1",
"lucide-react": "^1.7.0",
"mysql2": "^3.20.0",
"react": "^19.2.4",
"react-dom": "^19.2.4"
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
@ -25,7 +33,9 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"postcss": "^8.5.8",
"prisma": "^7.6.0",
"tailwindcss": "^3.4.19",
"tsx": "^4.21.0",
"vite": "^8.0.1"
}
}

14
prisma.config.ts Normal file
View file

@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

View file

@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`phone` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `User_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "mysql"

22
prisma/schema.prisma Normal file
View file

@ -0,0 +1,22 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Get a free hosted Postgres database in seconds: `npx create-db`
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "mysql"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
phone String
password String
createdAt DateTime @default(now())
}

View file

@ -0,0 +1,39 @@
import mysql from "mysql2/promise";
import "dotenv/config";
const run = async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "roadrentals"
});
try {
console.log("Creating Bookings table...");
await pool.execute(`
CREATE TABLE IF NOT EXISTS Bookings (
id INT AUTO_INCREMENT PRIMARY KEY,
trip_type VARCHAR(50) NOT NULL,
pickup_location TEXT NOT NULL,
drop_location TEXT,
pickup_datetime DATETIME NOT NULL,
return_datetime DATETIME,
package_type VARCHAR(100),
car_name VARCHAR(100),
total_amount DECIMAL(10, 2),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log("Bookings table successfully created or already exists!");
} catch (e) {
console.error("Error creating table:", e.message);
} finally {
await pool.end();
}
};
run();

View file

@ -0,0 +1,35 @@
import mysql from "mysql2/promise";
import "dotenv/config";
const run = async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "roadrentals"
});
try {
console.log("Creating Leads table...");
await pool.execute(`
CREATE TABLE IF NOT EXISTS Leads (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
mobile_number VARCHAR(15) NOT NULL,
trip_type VARCHAR(50),
pickup_location TEXT,
drop_location TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
console.log("Leads table successfully created!");
} catch (e) {
console.error("Error creating table:", e.message);
} finally {
await pool.end();
}
};
run();

View file

@ -0,0 +1,23 @@
import mysql from "mysql2/promise";
import "dotenv/config";
const run = async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST || "localhost",
user: process.env.DB_USER || "root",
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME || "roadrentals"
});
try {
console.log("Renaming column 'phone' to 'mobile_number'...");
await pool.execute("ALTER TABLE User CHANGE COLUMN phone mobile_number VARCHAR(255) NOT NULL");
console.log("Column successfully renamed!");
} catch (e) {
console.error("Error modifying table:", e.message);
} finally {
await pool.end();
}
};
run();

View file

@ -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,19 +10,29 @@ function App() {
const [user, setUser] = useState(null);
return (
<Router>
<main className="min-h-screen">
<Navbar
user={user}
<Routes>
<Route
path="/"
element={
<HomePage
onLoginClick={() => setIsAuthModalOpen(true)}
user={user}
/>
<HeroSection />
<CabsSection />
<ServicesSection />
<PromoBanners />
<ReviewsSection />
<WhyChooseUs />
<NewsSection />
<Footer />
}
/>
<Route
path="/search"
element={
<SearchPage
onLoginClick={() => setIsAuthModalOpen(true)}
user={user}
/>
}
/>
</Routes>
<AuthModal
isOpen={isAuthModalOpen}
onClose={() => setIsAuthModalOpen(false)}
@ -36,6 +40,7 @@ function App() {
setUser={setUser}
/>
</main>
</Router>
);
}

View file

@ -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>

View 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;

View file

@ -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;

View 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;

View file

@ -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
View 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
View 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;

View file

@ -7,9 +7,8 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://192.168.1.27:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
target: 'http://localhost:8000',
changeOrigin: true
}
}
}