Add src/app/booking/page.tsx
This commit is contained in:
391
src/app/booking/page.tsx
Normal file
391
src/app/booking/page.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
|
||||
import NavbarStyleApple from "@/components/navbar/NavbarStyleApple/NavbarStyleApple";
|
||||
import HeroOverlay from "@/components/sections/hero/HeroOverlay";
|
||||
import FooterBase from "@/components/sections/footer/FooterBase";
|
||||
import { useState } from "react";
|
||||
import { MapPin, Calendar, Users, Search, Heart, Clock, Users2 } from "lucide-react";
|
||||
|
||||
interface SearchFormData {
|
||||
departure: string;
|
||||
arrival: string;
|
||||
date: string;
|
||||
passengers: number;
|
||||
}
|
||||
|
||||
interface Bus {
|
||||
id: string;
|
||||
name: string;
|
||||
departure: string;
|
||||
arrival: string;
|
||||
departureTime: string;
|
||||
arrivalTime: string;
|
||||
duration: string;
|
||||
price: number;
|
||||
availableSeats: number;
|
||||
totalSeats: number;
|
||||
amenities: string[];
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface Seat {
|
||||
id: string;
|
||||
number: number;
|
||||
isAvailable: boolean;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const mockBuses: Bus[] = [
|
||||
{
|
||||
id: "1", name: "Premium Express", departure: "Cairo", arrival: "Alexandria", departureTime: "08:00 AM", arrivalTime: "11:00 AM", duration: "3h", price: 100,
|
||||
availableSeats: 12,
|
||||
totalSeats: 40,
|
||||
amenities: ["WiFi", "Air Conditioning", "USB Charging"],
|
||||
image: "http://img.b2bpic.net/free-photo/most-comfortable-means-transportation-business-people_329181-2791.jpg"
|
||||
},
|
||||
{
|
||||
id: "2", name: "Comfort Deluxe", departure: "Cairo", arrival: "Alexandria", departureTime: "10:30 AM", arrivalTime: "01:30 PM", duration: "3h", price: 150,
|
||||
availableSeats: 8,
|
||||
totalSeats: 30,
|
||||
amenities: ["WiFi", "Air Conditioning", "USB Charging", "Snacks"],
|
||||
image: "http://img.b2bpic.net/free-photo/empty-subway-train-barcelona-spain_1268-17854.jpg"
|
||||
},
|
||||
{
|
||||
id: "3", name: "Economy Max", departure: "Cairo", arrival: "Alexandria", departureTime: "02:00 PM", arrivalTime: "05:00 PM", duration: "3h", price: 80,
|
||||
availableSeats: 25,
|
||||
totalSeats: 50,
|
||||
amenities: ["Air Conditioning"],
|
||||
image: "http://img.b2bpic.net/free-photo/hand-with-credit-card-laptop_1232-619.jpg"
|
||||
}
|
||||
];
|
||||
|
||||
const generateSeats = (totalSeats: number): Seat[] => {
|
||||
return Array.from({ length: totalSeats }, (_, i) => ({
|
||||
id: `seat-${i + 1}`,
|
||||
number: i + 1,
|
||||
isAvailable: Math.random() > 0.3,
|
||||
isSelected: false
|
||||
}));
|
||||
};
|
||||
|
||||
export default function BookingPage() {
|
||||
const [searchData, setSearchData] = useState<SearchFormData>({
|
||||
departure: "", arrival: "", date: "", passengers: 1
|
||||
});
|
||||
const [searchResults, setSearchResults] = useState<Bus[]>([]);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [selectedBus, setSelectedBus] = useState<Bus | null>(null);
|
||||
const [seats, setSeats] = useState<Seat[]>([]);
|
||||
const [selectedSeats, setSelectedSeats] = useState<string[]>([]);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (searchData.departure && searchData.arrival && searchData.date) {
|
||||
setSearchResults(mockBuses);
|
||||
setHasSearched(true);
|
||||
setSelectedBus(null);
|
||||
setSeats([]);
|
||||
setSelectedSeats([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectBus = (bus: Bus) => {
|
||||
setSelectedBus(bus);
|
||||
setSeats(generateSeats(bus.totalSeats));
|
||||
setSelectedSeats([]);
|
||||
};
|
||||
|
||||
const handleSeatToggle = (seatId: string) => {
|
||||
const seat = seats.find(s => s.id === seatId);
|
||||
if (!seat || !seat.isAvailable) return;
|
||||
|
||||
if (selectedSeats.includes(seatId)) {
|
||||
setSelectedSeats(selectedSeats.filter(s => s !== seatId));
|
||||
} else if (selectedSeats.length < searchData.passengers) {
|
||||
setSelectedSeats([...selectedSeats, seatId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setSearchData(prev => ({
|
||||
...prev,
|
||||
[name]: name === "passengers" ? parseInt(value) : value
|
||||
}));
|
||||
};
|
||||
|
||||
const totalPrice = selectedBus ? selectedBus.price * selectedSeats.length : 0;
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
defaultButtonVariant="directional-hover"
|
||||
defaultTextAnimation="reveal-blur"
|
||||
borderRadius="pill"
|
||||
contentWidth="compact"
|
||||
sizing="mediumLargeSizeLargeTitles"
|
||||
background="aurora"
|
||||
cardStyle="inset"
|
||||
primaryButtonStyle="gradient"
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="medium"
|
||||
>
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleApple
|
||||
brandName="BusTicket"
|
||||
navItems={[
|
||||
{ name: "Book Now", id: "/booking" },
|
||||
{ name: "Home", id: "/" },
|
||||
{ name: "FAQ", id: "faq" },
|
||||
{ name: "Contact", id: "contact" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="hero" data-section="hero">
|
||||
<HeroOverlay
|
||||
title="Find & Book Your Perfect Bus"
|
||||
description="Search from hundreds of buses, select your seats, and book instantly"
|
||||
tag="Easy Booking"
|
||||
buttons={[
|
||||
{ text: "Back Home", href: "/" }
|
||||
]}
|
||||
imageSrc="http://img.b2bpic.net/free-photo/most-comfortable-means-transportation-business-people_329181-2791.jpg"
|
||||
imageAlt="Bus booking platform"
|
||||
showBlur={true}
|
||||
showDimOverlay={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="search" data-section="search" className="w-full bg-gradient-to-b from-transparent via-transparent to-background py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<form onSubmit={handleSearch} className="bg-card rounded-2xl p-8 shadow-lg border border-border/20">
|
||||
<h2 className="text-2xl font-bold mb-6 text-foreground">Search Buses</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="flex flex-col">
|
||||
<label className="text-sm font-medium text-foreground/70 mb-2">From</label>
|
||||
<input
|
||||
type="text"
|
||||
name="departure"
|
||||
value={searchData.departure}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Departure city"
|
||||
className="px-4 py-2 rounded-lg bg-background border border-border/20 text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className="text-sm font-medium text-foreground/70 mb-2">To</label>
|
||||
<input
|
||||
type="text"
|
||||
name="arrival"
|
||||
value={searchData.arrival}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Arrival city"
|
||||
className="px-4 py-2 rounded-lg bg-background border border-border/20 text-foreground placeholder-foreground/50 focus:outline-none focus:border-primary-cta"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className="text-sm font-medium text-foreground/70 mb-2">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
value={searchData.date}
|
||||
onChange={handleInputChange}
|
||||
className="px-4 py-2 rounded-lg bg-background border border-border/20 text-foreground focus:outline-none focus:border-primary-cta"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<label className="text-sm font-medium text-foreground/70 mb-2">Passengers</label>
|
||||
<select
|
||||
name="passengers"
|
||||
value={searchData.passengers}
|
||||
onChange={handleInputChange}
|
||||
className="px-4 py-2 rounded-lg bg-background border border-border/20 text-foreground focus:outline-none focus:border-primary-cta"
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="px-6 py-2 bg-primary-cta text-white rounded-lg hover:bg-primary-cta/90 transition-colors flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
<Search size={18} />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasSearched && !selectedBus && (
|
||||
<div id="bus-list" data-section="bus-list" className="w-full py-12 px-4">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-8 text-foreground">Available Buses</h2>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{searchResults.map(bus => (
|
||||
<div
|
||||
key={bus.id}
|
||||
className="bg-card rounded-xl p-6 border border-border/20 hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => handleSelectBus(bus)}
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-6 items-center">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-foreground mb-1">{bus.name}</h3>
|
||||
<p className="text-sm text-foreground/60">{bus.departure} → {bus.arrival}</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-2xl font-bold text-foreground">{bus.departureTime}</p>
|
||||
<p className="text-xs text-foreground/60">{bus.duration}</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-2xl font-bold text-foreground">{bus.arrivalTime}</p>
|
||||
<p className="text-xs text-foreground/60">Arrival</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-foreground/70">Seats Available</p>
|
||||
<p className="text-xl font-bold text-accent">{bus.availableSeats}/{bus.totalSeats}</p>
|
||||
<div className="flex gap-1 mt-2">
|
||||
{bus.amenities.map((amenity, i) => (
|
||||
<span key={i} className="text-xs bg-primary-cta/10 text-primary-cta px-2 py-1 rounded">
|
||||
{amenity}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-3xl font-bold text-primary-cta">{bus.price} EGP</p>
|
||||
<p className="text-sm text-foreground/60 mt-2">per seat</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedBus && (
|
||||
<div id="seat-selection" data-section="seat-selection" className="w-full py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<button
|
||||
onClick={() => setSelectedBus(null)}
|
||||
className="mb-6 px-4 py-2 bg-secondary-cta text-foreground rounded-lg hover:bg-secondary-cta/80 transition-colors"
|
||||
>
|
||||
← Back to Buses
|
||||
</button>
|
||||
|
||||
<div className="bg-card rounded-xl p-8 border border-border/20">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-2">{selectedBus.name}</h2>
|
||||
<p className="text-foreground/60">{selectedBus.departure} → {selectedBus.arrival} | {selectedBus.departureTime}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold text-foreground mb-4">Select Your Seats</h3>
|
||||
<div className="bg-background rounded-lg p-6 mb-4">
|
||||
<p className="text-sm text-foreground/60 mb-4 text-center">Select {searchData.passengers} seat{searchData.passengers !== 1 ? 's' : ''}</p>
|
||||
|
||||
<div className="grid grid-cols-6 md:grid-cols-10 gap-2 justify-center max-w-md mx-auto">
|
||||
{seats.map(seat => (
|
||||
<button
|
||||
key={seat.id}
|
||||
onClick={() => handleSeatToggle(seat.id)}
|
||||
disabled={!seat.isAvailable}
|
||||
className={`aspect-square rounded-lg text-xs font-semibold transition-colors ${
|
||||
!seat.isAvailable
|
||||
? "bg-foreground/20 text-foreground/40 cursor-not-allowed"
|
||||
: selectedSeats.includes(seat.id)
|
||||
? "bg-primary-cta text-white"
|
||||
: "bg-accent/20 text-foreground hover:bg-accent/40"
|
||||
}`}
|
||||
>
|
||||
{seat.number}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 justify-center text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-accent/20"></div>
|
||||
<span className="text-foreground/70">Available</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-primary-cta"></div>
|
||||
<span className="text-foreground/70">Selected</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-foreground/20"></div>
|
||||
<span className="text-foreground/70">Booked</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSeats.length > 0 && (
|
||||
<div className="border-t border-border/20 pt-6">
|
||||
<div className="mb-4">
|
||||
<p className="text-foreground/70 mb-2">Selected Seats:</p>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{selectedSeats.map(seatId => {
|
||||
const seat = seats.find(s => s.id === seatId);
|
||||
return (
|
||||
<span key={seatId} className="bg-primary-cta/20 text-primary-cta px-3 py-1 rounded-lg text-sm font-medium">
|
||||
Seat {seat?.number}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pt-4 border-t border-border/20">
|
||||
<div>
|
||||
<p className="text-foreground/70 text-sm">Total Price:</p>
|
||||
<p className="text-3xl font-bold text-primary-cta">{totalPrice} EGP</p>
|
||||
</div>
|
||||
<button className="px-8 py-3 bg-primary-cta text-white rounded-lg hover:bg-primary-cta/90 transition-colors font-bold">
|
||||
Proceed to Payment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="footer" data-section="footer">
|
||||
<FooterBase
|
||||
logoText="BusTicket"
|
||||
copyrightText="© 2025 | BusTicket Egypt. All rights reserved."
|
||||
columns={[
|
||||
{
|
||||
title: "Quick Links", items: [
|
||||
{ label: "Book Tickets", href: "/booking" },
|
||||
{ label: "Track Bus", href: "#" },
|
||||
{ label: "Routes", href: "#" },
|
||||
{ label: "Promotions", href: "#" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Company", items: [
|
||||
{ label: "About Us", href: "#" },
|
||||
{ label: "Careers", href: "#" },
|
||||
{ label: "Blog", href: "#" },
|
||||
{ label: "Contact", href: "contact" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Support", items: [
|
||||
{ label: "Help Center", href: "faq" },
|
||||
{ label: "Safety", href: "#" },
|
||||
{ label: "Terms & Conditions", href: "#" },
|
||||
{ label: "Privacy Policy", href: "#" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user