This tutorial will guide you through creating a basic online bus ticket booking project using HTML, CSS, and JavaScript. This project will allow users to search for buses based on their requirements, view available buses and select seats for booking. We’ll be leveraging Ionicons for icons to enhance the user interface.
Step 1: Adding Header Assets
Include necessary assets in the header of your HTML document. This will typically involve linking to external libraries or stylesheets that are required for the functionality and styling of your project.
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
Step 2: Creating the HTML Structure
The HTML provides the basic structure of your bus booking web page, including sections for searching, displaying results, and selecting seats.
<div class="container">
<div class="header">
<h1>
<ion-icon name="bus-outline"></ion-icon>
QuickBus
</h1>
<p>Your trusted partner for comfortable bus journeys across Pakistan</p>
</div>
<!-- Search Section -->
<div class="search-section">
<form class="search-form" id="searchForm">
<div class="form-group" style="position: relative;">
<label for="from">
<ion-icon name="location-outline"></ion-icon>
From
</label>
<select id="from" required>
<option value="">Select departure city</option>
<option value="karachi">Karachi</option>
<option value="lahore">Lahore</option>
<option value="islamabad">Islamabad</option>
<option value="rawalpindi">Rawalpindi</option>
<option value="faisalabad">Faisalabad</option>
<option value="multan">Multan</option>
<option value="dgkhan">Dera Ghazi Khan</option>
<option value="peshawar">Peshawar</option>
<option value="quetta">Quetta</option>
<option value="sialkot">Sialkot</option>
<option value="gujranwala">Gujranwala</option>
<option value="bahawalpur">Bahawalpur</option>
</select>
<button type="button" class="swap-cities" id="swapCities" title="Swap cities">
<ion-icon name="swap-vertical-outline"></ion-icon>
</button>
</div>
<div class="form-group">
<label for="to">
<ion-icon name="location-outline"></ion-icon>
To
</label>
<select id="to" required>
<option value="">Select destination city</option>
<option value="karachi">Karachi</option>
<option value="lahore">Lahore</option>
<option value="islamabad">Islamabad</option>
<option value="rawalpindi">Rawalpindi</option>
<option value="faisalabad">Faisalabad</option>
<option value="multan">Multan</option>
<option value="dgkhan">Dera Ghazi Khan</option>
<option value="peshawar">Peshawar</option>
<option value="quetta">Quetta</option>
<option value="sialkot">Sialkot</option>
<option value="gujranwala">Gujranwala</option>
<option value="bahawalpur">Bahawalpur</option>
</select>
</div>
<div class="form-group">
<label for="date">
<ion-icon name="calendar-outline"></ion-icon>
Departure Date
</label>
<input type="date" id="date" required>
<ion-icon name="calendar-outline" class="input-icon"></ion-icon>
</div>
<div class="form-group">
<label for="passengers">
<ion-icon name="people-outline"></ion-icon>
Passengers
</label>
<select id="passengers">
<option value="1">1 Passenger</option>
<option value="2">2 Passengers</option>
<option value="3">3 Passengers</option>
<option value="4">4 Passengers</option>
<option value="5">5 Passengers</option>
<option value="6">6 Passengers</option>
</select>
</div>
<button type="submit" class="search-btn" id="searchBtn">
<ion-icon name="search-outline"></ion-icon>
Search Buses
</button>
</form>
</div>
<!-- Loading Animation -->
<div class="loading" id="loadingSection">
<ion-icon name="reload-outline"></ion-icon>
<p>Searching for available buses...</p>
</div>
<!-- Results Section -->
<div class="results-section" id="resultsSection">
<div class="results-header">
<h2>
<ion-icon name="bus-outline"></ion-icon>
Available Buses
</h2>
<p id="routeInfo"></p>
</div>
<div class="filter-controls">
<button class="filter-btn active" data-filter="all">
<ion-icon name="list-outline"></ion-icon>
All Buses
</button>
<button class="filter-btn" data-filter="price">
<ion-icon name="trending-down-outline"></ion-icon>
Lowest Price
</button>
<button class="filter-btn" data-filter="departure">
<ion-icon name="time-outline"></ion-icon>
Earliest Departure
</button>
<button class="filter-btn" data-filter="duration">
<ion-icon name="hourglass-outline"></ion-icon>
Shortest Duration
</button>
</div>
<div id="busResults"></div>
</div>
<!-- Seat Selection Section -->
<div class="seat-section" id="seatSection">
<div class="results-header">
<h2>
<ion-icon name="airplane-outline"></ion-icon>
Select Your Seats
</h2>
<p id="selectedBusInfo"></p>
</div>
<div class="seat-layout" id="seatLayout">
<div class="bus-front">
<ion-icon name="car-outline"></ion-icon>
Driver
</div>
<!-- Seat layout will be generated by JavaScript -->
</div>
<div class="seat-legend">
<div class="legend-item">
<div class="legend-seat" style="background: white; border: 2px solid #bdc3c7;"></div>
<span><ion-icon name="checkmark-circle-outline"></ion-icon> Available</span>
</div>
<div class="legend-item">
<div class="legend-seat" style="background: #27ae60;"></div>
<span><ion-icon name="person-outline"></ion-icon> Selected</span>
</div>
<div class="legend-item">
<div class="legend-seat" style="background: #e74c3c;"></div>
<span><ion-icon name="close-circle-outline"></ion-icon> Booked</span>
</div>
</div>
<div class="booking-summary">
<div class="summary-item">
<span><ion-icon name="airplane-outline"></ion-icon>Selected Seats:</span>
<span id="selectedSeats">None</span>
</div>
<div class="summary-item">
<span><ion-icon name="card-outline"></ion-icon>Ticket Price:</span>
<span id="ticketPrice">PKR 0</span>
</div>
<div class="summary-item">
<span><ion-icon name="receipt-outline"></ion-icon>Service Charges:</span>
<span>PKR 100</span>
</div>
<div class="summary-item total">
<span><ion-icon name="calculator-outline"></ion-icon>Total Amount:</span>
<span id="totalAmount">PKR 100</span>
</div>
<button class="proceed-btn" id="proceedBtn" disabled>
<ion-icon name="card-outline"></ion-icon>
Proceed to Payment
</button>
</div>
</div>
</div>
Step 3: Styling with CSS
The CSS stylesheet is responsible for making your website visually appealing and user-friendly. It adds visual enhancements such as color schemes, layout, and responsive design.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 30px 0;
margin-bottom: 30px;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.header h1 {
text-align: center;
color: #2c3e50;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.header p {
text-align: center;
color: #7f8c8d;
font-size: 1.1rem;
}
/* Search Section */
.search-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 40px;
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.search-form {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr auto;
gap: 20px;
align-items: end;
}
.form-group {
display: flex;
flex-direction: column;
position: relative;
}
.form-group label {
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
}
.form-group input, .form-group select {
padding: 15px 45px 15px 15px;
border: 2px solid #e0e6ed;
border-radius: 12px;
font-size: 1rem;
transition: all 0.3s ease;
background: white;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
.input-icon {
position: absolute;
right: 15px;
bottom: 18px;
color: #7f8c8d;
font-size: 1.2rem;
pointer-events: none;
}
.swap-cities {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: #667eea;
color: white;
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 3px 10px rgba(102, 126, 234, 0.3);
}
.swap-cities:hover {
background: #5a6fd8;
transform: translateY(-50%) rotate(180deg);
}
.search-btn {
padding: 15px 35px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
display: flex;
align-items: center;
gap: 10px;
}
.search-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.search-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
}
/* Loading Animation */
.loading {
display: none;
text-align: center;
padding: 40px;
color: #667eea;
}
.loading ion-icon {
font-size: 3rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Results Section */
.results-section {
display: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 30px;
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.results-header {
margin-bottom: 30px;
text-align: center;
}
.results-header h2 {
color: #2c3e50;
font-size: 1.8rem;
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.filter-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.filter-btn {
padding: 10px 20px;
background: white;
border: 2px solid #e0e6ed;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
.filter-btn.active, .filter-btn:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
.bus-card {
background: white;
border: 2px solid #f8f9fa;
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
position: relative;
}
.bus-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
border-color: #667eea;
}
.bus-info {
display: grid;
grid-template-columns: 2fr 1.5fr 1fr 1fr auto;
gap: 20px;
align-items: center;
}
.bus-details h3 {
color: #2c3e50;
font-size: 1.3rem;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.bus-details p {
color: #7f8c8d;
font-size: 0.9rem;
margin-bottom: 5px;
}
.bus-features {
display: flex;
gap: 10px;
margin-top: 10px;
}
.feature-tag {
background: #e8f4fd;
color: #3498db;
padding: 3px 8px;
border-radius: 12px;
font-size: 0.8rem;
display: flex;
align-items: center;
gap: 4px;
}
.timing {
text-align: center;
}
.timing .route {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 10px;
}
.timing .time {
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.timing .duration {
color: #7f8c8d;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 5px;
}
.price {
text-align: center;
}
.price .amount {
font-size: 1.5rem;
font-weight: 700;
color: #27ae60;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.price .currency {
font-size: 0.9rem;
color: #7f8c8d;
}
.seats-available {
text-align: center;
color: #e67e22;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.select-btn {
padding: 12px 25px;
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: white;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.select-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(39, 174, 96, 0.3);
}
/* Seat Selection */
.seat-section {
display: none;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 30px;
border-radius: 20px;
margin-bottom: 30px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.seat-layout {
max-width: 400px;
margin: 0 auto;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
position: relative;
}
.bus-front {
text-align: center;
margin-bottom: 20px;
padding: 10px;
background: #667eea;
color: white;
border-radius: 10px 10px 5px 5px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.seat-row {
display: grid;
grid-template-columns: 1fr 0.5fr 1fr;
gap: 10px;
margin-bottom: 10px;
align-items: center;
}
.seat {
width: 40px;
height: 40px;
border: 2px solid #bdc3c7;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.3s ease;
background: white;
position: relative;
}
.seat::before {
content: '🪑';
font-size: 0.6rem;
position: absolute;
opacity: 0.3;
}
.seat.available:hover {
background: #3498db;
color: white;
transform: scale(1.1);
}
.seat.selected {
background: #27ae60;
color: white;
border-color: #27ae60;
}
.seat.booked {
background: #e74c3c;
color: white;
border-color: #e74c3c;
cursor: not-allowed;
}
.seat-legend {
display: flex;
justify-content: center;
gap: 30px;
margin: 30px 0;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-seat {
width: 20px;
height: 20px;
border-radius: 4px;
}
.booking-summary {
background: #f8f9fa;
padding: 25px;
border-radius: 15px;
margin-top: 30px;
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 8px 0;
}
.summary-item ion-icon {
margin-right: 8px;
}
.total {
border-top: 2px solid #e0e6ed;
padding-top: 15px;
font-weight: 700;
font-size: 1.2rem;
color: #2c3e50;
}
.proceed-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
margin-top: 20px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.proceed-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.proceed-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.no-results {
text-align: center;
padding: 60px 20px;
color: #7f8c8d;
}
.no-results ion-icon {
font-size: 4rem;
margin-bottom: 20px;
color: #bdc3c7;
}
.no-results h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #5a6c7d;
}
.no-results p {
font-size: 1rem;
line-height: 1.5;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
flex-direction: column;
gap: 10px;
}
.search-section {
padding: 20px;
}
.search-form {
grid-template-columns: 1fr;
gap: 15px;
}
.filter-controls {
gap: 10px;
}
.bus-info {
grid-template-columns: 1fr;
gap: 15px;
text-align: center;
}
.timing .route {
flex-direction: column;
gap: 10px;
}
.seat-row {
grid-template-columns: 1fr 0.3fr 1fr;
}
.seat {
width: 35px;
height: 35px;
font-size: 0.7rem;
}
.seat-legend {
gap: 15px;
}
}
@media (max-width: 480px) {
.header h1 {
font-size: 1.5rem;
}
.search-section, .results-section, .seat-section {
padding: 15px;
}
.seat {
width: 30px;
height: 30px;
font-size: 0.6rem;
}
.filter-controls {
flex-direction: column;
align-items: center;
}
}
Step 4: Adding JavaScript Functionality
The JavaScript code adds interactivity and dynamic elements to the booking process. This includes the search functionality, filtering results, and seat selection.
const busData = {
'karachi-lahore': [
{
id: 1,
name: 'Daewoo Express',
operator: 'Daewoo Pakistan Express Bus Service',
departure: '08:00',
arrival: '20:30',
duration: '12h 30m',
distance: 1200,
price: 2800,
seats: 15,
features: ['AC', 'WiFi', 'Entertainment'],
rating: 4.5,
busType: 'luxury'
},
{
id: 2,
name: 'Faisal Movers Gold',
operator: 'Faisal Movers',
departure: '14:30',
arrival: '03:00',
duration: '12h 30m',
distance: 1200,
price: 3200,
seats: 8,
features: ['AC', 'Reclining Seats', 'Refreshments'],
rating: 4.7,
busType: 'luxury'
},
{
id: 3,
name: 'Niazi Express',
operator: 'Niazi Express',
departure: '22:15',
arrival: '11:00',
duration: '12h 45m',
distance: 1200,
price: 2500,
seats: 22,
features: ['AC', 'TV'],
rating: 4.2,
busType: 'standard'
}
],
'lahore-karachi': [
{
id: 4,
name: 'Bilal Travels',
operator: 'Bilal Travels',
departure: '07:30',
arrival: '20:15',
duration: '12h 45m',
distance: 1200,
price: 2600,
seats: 18,
features: ['AC', 'Charging Points'],
rating: 4.3,
busType: 'standard'
},
{
id: 5,
name: 'Skyways Executive',
operator: 'Skyways',
departure: '15:45',
arrival: '04:30',
duration: '12h 45m',
distance: 1200,
price: 3800,
seats: 5,
features: ['AC', 'Luxury Seats', 'WiFi', 'Entertainment'],
rating: 4.8,
busType: 'luxury'
}
],
'islamabad-karachi': [
{
id: 6,
name: 'Kohistan Express',
operator: 'Kohistan Express',
departure: '09:00',
arrival: '01:30',
duration: '16h 30m',
distance: 1400,
price: 3500,
seats: 12,
features: ['AC', 'Refreshments'],
rating: 4.4,
busType: 'standard'
},
{
id: 7,
name: 'Manthar Express VIP',
operator: 'Manthar Express',
departure: '20:30',
arrival: '14:00',
duration: '17h 30m',
distance: 1400,
price: 4200,
seats: 7,
features: ['AC', 'Luxury Seats', 'WiFi', 'Meals'],
rating: 4.6,
busType: 'luxury'
}
],
'lahore-islamabad': [
{
id: 8,
name: 'Metro Bus Service',
operator: 'Punjab Mass Transit',
departure: '06:30',
arrival: '10:00',
duration: '3h 30m',
distance: 380,
price: 800,
seats: 25,
features: ['AC'],
rating: 4.1,
busType: 'economy'
},
{
id: 9,
name: 'Waraich Express',
operator: 'Waraich Express',
departure: '12:45',
arrival: '16:30',
duration: '3h 45m',
distance: 380,
price: 1200,
seats: 16,
features: ['AC', 'Charging Points'],
rating: 4.3,
busType: 'standard'
}
],
'islamabad-lahore': [
{
id: 10,
name: 'Daewoo Express',
operator: 'Daewoo Pakistan Express Bus Service',
departure: '08:15',
arrival: '12:00',
duration: '3h 45m',
distance: 380,
price: 900,
seats: 20,
features: ['AC', 'WiFi'],
rating: 4.5,
busType: 'standard'
},
{
id: 11,
name: 'Faisal Movers',
operator: 'Faisal Movers',
departure: '18:00',
arrival: '21:45',
duration: '3h 45m',
distance: 380,
price: 1100,
seats: 14,
features: ['AC', 'Reclining Seats'],
rating: 4.4,
busType: 'standard'
}
],
'multan-karachi': [
{
id: 12,
name: 'New Khan Road Ways',
operator: 'New Khan Road Ways',
departure: '19:30',
arrival: '06:00',
duration: '10h 30m',
distance: 900,
price: 2200,
seats: 19,
features: ['AC', 'TV'],
rating: 4.0,
busType: 'standard'
},
{
id: 13,
name: 'Rehman Travels',
operator: 'Rehman Travels',
departure: '21:00',
arrival: '07:45',
duration: '10h 45m',
distance: 900,
price: 2500,
seats: 11,
features: ['AC', 'Refreshments', 'WiFi'],
rating: 4.3,
busType: 'standard'
}
]
};
let selectedBus = null;
let selectedSeats = [];
let seatPrice = 0;
let allBuses = [];
let currentFilter = 'all';
// Set minimum date to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('date').min = today;
document.getElementById('date').value = today;
// Swap cities functionality
document.getElementById('swapCities').addEventListener('click', function() {
const fromSelect = document.getElementById('from');
const toSelect = document.getElementById('to');
const fromValue = fromSelect.value;
const toValue = toSelect.value;
fromSelect.value = toValue;
toSelect.value = fromValue;
});
// Search form handler
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const from = document.getElementById('from').value;
const to = document.getElementById('to').value;
const date = document.getElementById('date').value;
if (!from || !to) {
showNotification('Please select both departure and destination cities', 'error');
return;
}
if (from === to) {
showNotification('Departure and destination cities cannot be the same', 'error');
return;
}
const selectedDate = new Date(date);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (selectedDate < today) {
showNotification('Please select a valid departure date', 'error');
return;
}
searchBuses(from, to, date);
});
function searchBuses(from, to, date) {
showLoading(true);
// Simulate API call delay
setTimeout(() => {
const route = `${from}-${to}`;
const reverseRoute = `${to}-${from}`;
// Get buses for both directions
let buses = [...(busData[route] || [])];
// If no direct route found, check reverse route
if (buses.length === 0) {
buses = [...(busData[reverseRoute] || [])];
}
// Add some randomization to seat availability and prices for realism
buses = buses.map(bus => ({
...bus,
seats: Math.max(1, bus.seats - Math.floor(Math.random() * 10)),
price: bus.price + (Math.random() > 0.5 ? Math.floor(Math.random() * 200) : -Math.floor(Math.random() * 100))
}));
allBuses = buses;
displayResults(from, to, date, buses);
showLoading(false);
}, 1500);
}
function displayResults(from, to, date, buses) {
const resultsSection = document.getElementById('resultsSection');
const busResults = document.getElementById('busResults');
const routeInfo = document.getElementById('routeInfo');
const fromCity = capitalizeFirst(from);
const toCity = capitalizeFirst(to);
const formattedDate = formatDate(date);
routeInfo.innerHTML = `
<ion-icon name="location-outline"></ion-icon>
${fromCity} → ${toCity} • ${formattedDate} • ${buses.length} bus(es) found
`;
if (buses.length === 0) {
busResults.innerHTML = `
<div class="no-results">
<ion-icon name="sad-outline"></ion-icon>
<h3>No buses available</h3>
<p>Sorry, we couldn't find any buses for this route on the selected date.<br>
Try selecting a different date or check alternative routes.</p>
</div>
`;
} else {
busResults.innerHTML = buses.map(bus => createBusCard(bus)).join('');
}
resultsSection.style.display = 'block';
resultsSection.scrollIntoView({ behavior: 'smooth' });
}
function createBusCard(bus) {
const features = bus.features.map(feature => {
const iconMap = {
'AC': 'snow-outline',
'WiFi': 'wifi-outline',
'Entertainment': 'tv-outline',
'Reclining Seats': 'bed-outline',
'Refreshments': 'restaurant-outline',
'Luxury Seats': 'car-sport-outline',
'Charging Points': 'battery-charging-outline',
'TV': 'desktop-outline',
'Meals': 'fast-food-outline'
};
return `
<div class="feature-tag">
<ion-icon name="${iconMap[feature] || 'checkmark-outline'}"></ion-icon>
${feature}
</div>
`;
}).join('');
const seatColor = bus.seats <= 5 ? '#e74c3c' : bus.seats <= 10 ? '#f39c12' : '#27ae60';
return `
<div class="bus-card" data-bus-id="${bus.id}">
<div class="bus-info">
<div class="bus-details">
<h3>
<ion-icon name="bus-outline"></ion-icon>
${bus.name}
</h3>
<p><ion-icon name="business-outline"></ion-icon> ${bus.operator}</p>
<p><ion-icon name="star-outline"></ion-icon> ${bus.rating}/5 • ${bus.distance}km</p>
<div class="bus-features">
${features}
</div>
</div>
<div class="timing">
<div class="route">
<div class="time">${bus.departure}</div>
<div class="duration">
<ion-icon name="arrow-forward-outline"></ion-icon>
${bus.duration}
</div>
<div class="time">${bus.arrival}</div>
</div>
</div>
<div class="price">
<div class="amount">
<ion-icon name="card-outline"></ion-icon>
${bus.price}
</div>
<div class="currency">PKR per seat</div>
</div>
<div class="seats-available" style="color: ${seatColor}">
<ion-icon name="people-outline"></ion-icon>
${bus.seats} seats left
</div>
<button class="select-btn" onclick="selectBus(${bus.id})" ${bus.seats === 0 ? 'disabled' : ''}>
<ion-icon name="checkmark-outline"></ion-icon>
${bus.seats === 0 ? 'Sold Out' : 'Select Seats'}
</button>
</div>
</div>
`;
}
// Filter functionality
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
const filter = this.dataset.filter;
currentFilter = filter;
applyFilter(filter);
});
});
function applyFilter(filter) {
let sortedBuses = [...allBuses];
switch(filter) {
case 'price':
sortedBuses.sort((a, b) => a.price - b.price);
break;
case 'departure':
sortedBuses.sort((a, b) => a.departure.localeCompare(b.departure));
break;
case 'duration':
sortedBuses.sort((a, b) => {
const durationA = parseInt(a.duration.split('h')[0]) * 60 + parseInt(a.duration.split('h')[1]);
const durationB = parseInt(b.duration.split('h')[0]) * 60 + parseInt(b.duration.split('h')[1]);
return durationA - durationB;
});
break;
default:
// Keep original order
break;
}
const busResults = document.getElementById('busResults');
if (sortedBuses.length > 0) {
busResults.innerHTML = sortedBuses.map(bus => createBusCard(bus)).join('');
}
}
function selectBus(busId) {
// Find the selected bus from all routes
selectedBus = null;
for (const route in busData) {
const bus = busData[route].find(b => b.id === busId);
if (bus) {
selectedBus = bus;
break;
}
}
if (!selectedBus) return;
seatPrice = selectedBus.price;
selectedSeats = [];
updateBookingSummary();
document.getElementById('selectedBusInfo').innerHTML = `
<ion-icon name="bus-outline"></ion-icon>
${selectedBus.name} - ${selectedBus.operator}
<span style="color: #7f8c8d;">
(${selectedBus.departure} - ${selectedBus.arrival})
</span>
`;
generateSeatLayout();
document.getElementById('seatSection').style.display = 'block';
document.getElementById('seatSection').scrollIntoView({ behavior: 'smooth' });
}
function generateSeatLayout() {
const seatLayout = document.getElementById('seatLayout');
const existingSeats = seatLayout.querySelectorAll('.seat-row');
existingSeats.forEach(row => row.remove());
// Generate 10 rows of seats (40 seats total)
for (let row = 1; row <= 10; row++) {
const seatRow = document.createElement('div');
seatRow.className = 'seat-row';
// Left side seats (A, B)
const leftSide = document.createElement('div');
leftSide.style.display = 'flex';
leftSide.style.gap = '10px';
leftSide.appendChild(createSeatElement(row, 'A'));
leftSide.appendChild(createSeatElement(row, 'B'));
// Aisle
const aisle = document.createElement('div');
aisle.style.textAlign = 'center';
aisle.style.color = '#bdc3c7';
aisle.style.fontSize = '0.8rem';
aisle.textContent = '—';
// Right side seats (C, D)
const rightSide = document.createElement('div');
rightSide.style.display = 'flex';
rightSide.style.gap = '10px';
rightSide.appendChild(createSeatElement(row, 'C'));
rightSide.appendChild(createSeatElement(row, 'D'));
seatRow.appendChild(leftSide);
seatRow.appendChild(aisle);
seatRow.appendChild(rightSide);
seatLayout.appendChild(seatRow);
}
}
function createSeatElement(row, letter) {
const seatNumber = `${row}${letter}`;
const seat = document.createElement('div');
seat.className = 'seat';
seat.id = `seat-${seatNumber}`;
seat.textContent = seatNumber;
// Randomly make some seats booked for demonstration (30% chance)
const isBooked = Math.random() < 0.3;
if (isBooked) {
seat.classList.add('booked');
} else {
seat.classList.add('available');
seat.onclick = () => toggleSeat(seatNumber);
}
return seat;
}
function toggleSeat(seatNumber) {
const seatElement = document.getElementById(`seat-${seatNumber}`);
if (seatElement.classList.contains('selected')) {
// Deselect seat
seatElement.classList.remove('selected');
seatElement.classList.add('available');
selectedSeats = selectedSeats.filter(seat => seat !== seatNumber);
} else if (seatElement.classList.contains('available')) {
// Select seat
const passengers = parseInt(document.getElementById('passengers').value);
if (selectedSeats.length < passengers) {
seatElement.classList.remove('available');
seatElement.classList.add('selected');
selectedSeats.push(seatNumber);
} else {
showNotification(`You can only select ${passengers} seat(s) based on passenger count.`, 'warning');
}
}
updateBookingSummary();
}
function updateBookingSummary() {
const selectedSeatsElement = document.getElementById('selectedSeats');
const ticketPriceElement = document.getElementById('ticketPrice');
const totalAmountElement = document.getElementById('totalAmount');
const proceedBtn = document.getElementById('proceedBtn');
selectedSeatsElement.textContent = selectedSeats.length > 0 ? selectedSeats.join(', ') : 'None';
const ticketTotal = selectedSeats.length * seatPrice;
ticketPriceElement.textContent = `PKR ${ticketTotal}`;
const serviceCharges = 100;
const totalAmount = ticketTotal + serviceCharges;
totalAmountElement.textContent = `PKR ${totalAmount}`;
proceedBtn.disabled = selectedSeats.length === 0;
}
// Proceed to payment handler
document.getElementById('proceedBtn').addEventListener('click', function() {
if (selectedSeats.length === 0) return;
const bookingDetails = {
bus: selectedBus,
seats: selectedSeats,
totalAmount: selectedSeats.length * seatPrice + 100
};
showNotification('Redirecting to payment gateway...', 'success');
setTimeout(() => {
alert(`🎉 Booking Confirmed!\n\n` +
`Bus: ${selectedBus.name}\n` +
`Operator: ${selectedBus.operator}\n` +
`Departure: ${selectedBus.departure}\n` +
`Seats: ${selectedSeats.join(', ')}\n` +
`Total: PKR ${bookingDetails.totalAmount}\n\n` +
`Thank you for choosing QuickBus!\n` +
`Your e-ticket will be sent to your email.`);
resetBooking();
}, 2000);
});
function showLoading(show) {
const loadingSection = document.getElementById('loadingSection');
const resultsSection = document.getElementById('resultsSection');
if (show) {
loadingSection.style.display = 'block';
resultsSection.style.display = 'none';
loadingSection.scrollIntoView({ behavior: 'smooth' });
} else {
loadingSection.style.display = 'none';
}
}
function showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 10px;
color: white;
font-weight: 600;
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
max-width: 300px;
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
transform: translateX(100%);
transition: transform 0.3s ease;
`;
const colors = {
success: '#27ae60',
error: '#e74c3c',
warning: '#f39c12',
info: '#3498db'
};
const icons = {
success: 'checkmark-circle-outline',
error: 'close-circle-outline',
warning: 'warning-outline',
info: 'information-circle-outline'
};
notification.style.background = colors[type];
notification.innerHTML = `
<ion-icon name="${icons[type]}"></ion-icon>
<span>${message}</span>
`;
document.body.appendChild(notification);
// Slide in
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
// Remove after 3 seconds
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
function resetBooking() {
selectedBus = null;
selectedSeats = [];
allBuses = [];
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('seatSection').style.display = 'none';
document.getElementById('searchForm').reset();
document.getElementById('date').value = new Date().toISOString().split('T')[0];
// Reset filter
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector('.filter-btn[data-filter="all"]').classList.add('active');
currentFilter = 'all';
}
// Utility functions
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1).replace(/([A-Z])/g, ' $1');
}
function formatDate(dateString) {
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
return new Date(dateString).toLocaleDateString('en-US', options);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Add some initial animation
setTimeout(() => {
document.querySelector('.header').style.transform = 'translateY(0)';
document.querySelector('.search-section').style.transform = 'translateY(0)';
}, 200);
});
Conclusion
Congratulations! You’ve successfully built a basic online bus ticket booking project using HTML, CSS, and JavaScript. Remember that this is a simplified version and can be further enhanced with features like payment gateway integration and more sophisticated data management.







