This commit is contained in:
Flatlogic Bot 2025-10-16 20:45:21 +00:00
parent 83696c725a
commit acff14d6dc
13 changed files with 413 additions and 639 deletions

View File

@ -0,0 +1,53 @@
<?php
session_start();
require_once '../db/config.php';
if (!isset($_GET['order_id'])) {
http_response_code(400);
echo json_encode(['error' => 'Order ID is required.']);
exit;
}
$order_id = $_GET['order_id'];
$user_id = $_SESSION['user_id'] ?? null;
// For guest users, we need a token
$token = $_GET['token'] ?? null;
$pdoconn = db();
// Verify the user or guest has permission to view this order
if ($user_id) {
$stmt = $pdoconn->prepare("SELECT id FROM orders WHERE id = :order_id AND user_id = :user_id");
$stmt->execute(['order_id' => $order_id, 'user_id' => $user_id]);
} else if ($token) {
$stmt = $pdoconn->prepare("SELECT id FROM orders WHERE id = :order_id AND token = :token");
$stmt->execute(['order_id' => $order_id, 'token' => $token]);
} else {
http_response_code(403);
echo json_encode(['error' => 'Authentication required.']);
exit;
}
if ($stmt->rowCount() == 0) {
http_response_code(404);
echo json_encode(['error' => 'Order not found or access denied.']);
exit;
}
// Fetch driver location
$stmt = $pdoconn->prepare("SELECT driver_lat, driver_lng FROM orders WHERE id = :order_id");
$stmt->execute(['order_id' => $order_id]);
$location = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$location || is_null($location['driver_lat']) || is_null($location['driver_lng'])) {
http_response_code(404);
echo json_encode(['error' => 'Driver location not available yet.']);
exit;
}
header('Content-Type: application/json');
echo json_encode([
'lat' => $location['driver_lat'],
'lng' => $location['driver_lng']
]);

View File

@ -23,10 +23,7 @@ MajuroEats Theme
/* --- Global Styles --- */
body {
font-family: var(--font-family);
background-image: url('../assets/pasted-20251016-192041-2abf91d9.jpg');
background-size: cover;
background-attachment: fixed;
background-position: center;
background-color: var(--coconut-white);
color: var(--text-dark);
margin: 0;
padding-top: 80px; /* Space for fixed header */
@ -188,225 +185,174 @@ main {
/* --- Hero Section --- */
.hero-section {
background-image: url('../assets/pasted-20251016-192041-2abf91d9.jpg');
background-size: cover;
background-attachment: fixed;
background-position: center;
position: relative;
padding: 120px 0;
background-color: #f8f9fa;
padding: 100px 0;
text-align: center;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.hero-content-container {
position: relative;
z-index: 2;
}
.hero-content h1 {
font-size: 3.8rem;
.hero-title {
font-size: 3.5rem;
font-weight: 800;
margin: 0 0 15px;
text-shadow: 0 3px 10px rgba(0,0,0,0.3);
margin-bottom: 1rem;
}
.hero-content p {
font-size: 1.3rem;
margin: 0 auto 40px;
opacity: 0.95;
max-width: 600px;
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-light);
margin-bottom: 2.5rem;
}
.hero-search-form {
display: flex;
justify-content: center;
margin: 30px auto;
box-shadow: var(--shadow-medium);
.hero-section .btn-primary {
background-color: #ff5a5f;
border-color: #ff5a5f;
font-size: 1.25rem;
padding: 15px 40px;
border-radius: 50px;
overflow: hidden;
max-width: 600px;
background-color: var(--coconut-white);
border: 3px solid var(--coconut-white);
}
#address-input {
flex-grow: 1;
border: none;
padding: 20px;
font-size: 1.1rem;
font-family: var(--font-family);
color: var(--text-dark);
background: transparent;
}
#address-input:focus {
outline: none;
}
#find-food-btn {
background-color: var(--coral);
color: var(--coconut-white);
border: none;
padding: 0 40px;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: var(--transition);
border-radius: 50px;
}
#find-food-btn:hover {
background-color: var(--orange);
.hero-section .btn-primary:hover {
background-color: #e04f54;
border-color: #e04f54;
}
.delivery-note {
font-size: 0.9rem;
opacity: 0.8;
.hero-section .text-muted {
color: #6c757d !important;
text-decoration: underline;
}
/* --- Promo Section --- */
.promo-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
margin-bottom: 60px;
/* --- How It Works Section --- */
#how-it-works {
padding: 60px 0;
background-color: #fff;
}
.promo-card {
background: var(--coconut-white);
padding: 30px;
border-radius: var(--border-radius);
text-align: center;
box-shadow: var(--shadow-soft);
transition: var(--transition);
.step {
padding: 20px;
}
.promo-card:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-medium);
.step-icon {
width: 80px;
height: 80px;
margin-bottom: 1.5rem;
}
.promo-card h3 {
.step h3 {
font-size: 1.5rem;
font-weight: 800;
color: var(--ocean-blue);
margin: 0 0 10px;
font-weight: 700;
margin-bottom: 0.5rem;
}
.promo-card p {
font-size: 1rem;
.step p {
color: var(--text-light);
margin: 0;
}
/* --- Featured Restaurants --- */
.featured-restaurants {
margin-bottom: 60px;
}
.restaurant-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.restaurant-card {
background: var(--coconut-white);
/* --- Restaurant & Cuisine Cards --- */
.restaurant-card, .cuisine-card {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-soft);
overflow: hidden;
transition: var(--transition);
color: var(--text-dark);
border: 2px solid transparent;
background-color: #fff;
}
.restaurant-card:hover {
transform: translateY(-10px);
.restaurant-card:hover, .cuisine-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-medium);
border-color: var(--coral);
}
.card-image {
.restaurant-card .card-img-top {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
height: 200px;
background-size: cover;
background-position: center;
object-fit: cover;
}
.card-content {
padding: 20px;
.restaurant-card .card-body {
padding: 1.5rem;
}
.card-content h3 {
font-size: 1.4rem;
font-weight: 800;
margin: 0 0 5px;
}
.cuisine-tags {
font-size: 0.9rem;
color: var(--text-light);
margin: 0 0 15px;
}
.restaurant-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1rem;
.restaurant-card .card-title a {
color: var(--text-dark);
font-weight: 700;
}
.rating-display {
display: flex;
align-items: center;
gap: 5px;
}
.rating-display .star {
color: #FFC700;
font-size: 1.2rem;
}
.delivery-info {
color: var(--text-light);
}
.see-all-container {
text-align: center;
margin-top: 40px;
}
.see-all-btn {
background-color: var(--coral);
color: var(--coconut-white);
padding: 15px 35px;
border-radius: 50px;
font-size: 1.1rem;
font-weight: 700;
border: 2px solid var(--coral);
transition: var(--transition);
}
.see-all-btn:hover {
background-color: transparent;
.restaurant-card .card-title a:hover {
color: var(--coral);
}
/* --- Footer --- */
.main-footer {
background-color: var(--text-dark);
color: var(--sand);
padding: 40px 0;
.cuisine-card {
display: block;
padding: 1rem;
text-align: center;
}
.cuisine-card img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 1rem;
}
.cuisine-card h5 {
color: var(--text-dark);
font-weight: 700;
}
/* --- Modal Styles --- */
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 600px;
border-radius: var(--border-radius);
position: relative;
}
.close-btn {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
}
.close-btn:hover,
.close-btn:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
#map {
height: 400px;
width: 100%;
margin-top: 20px;
margin-bottom: 20px;
border-radius: var(--border-radius);
}
#confirm-location-btn {
width: 100%;
padding: 15px;
font-size: 1.1rem;
}
/* --- Responsive Design --- */
@media (max-width: 992px) {
.nav-links {
@ -456,7 +402,7 @@ main {
display: block;
}
.hero-content h1 {
.hero-title {
font-size: 2.8rem;
}
}
@ -471,265 +417,10 @@ main {
.nav-links {
top: 70px;
}
.hero-content h1 {
.hero-title {
font-size: 2.2rem;
}
.hero-content p {
.hero-subtitle {
font-size: 1.1rem;
}
.location-actions {
flex-direction: column;
gap: 15px;
}
}
/* --- Empty State --- */
.empty-state {
padding: 80px 40px;
background-color: var(--sand);
border-radius: var(--border-radius);
margin-top: 40px;
}
.empty-state h3 {
font-size: 1.8rem;
font-weight: 800;
color: var(--text-dark);
}
.empty-state p {
font-size: 1.1rem;
color: var(--text-light);
max-width: 400px;
margin: 10px auto 0;
}
.btn {
padding: 15px 35px;
border-radius: 50px;
font-weight: 700;
font-size: 1.1rem;
border: 2px solid transparent;
transition: var(--transition);
cursor: pointer;
text-align: center;
display: inline-block;
}
.btn-primary {
background-color: var(--coral);
color: var(--coconut-white);
box-shadow: var(--shadow-soft);
}
.btn-primary:hover {
background-color: var(--orange);
transform: translateY(-2px);
box-shadow: var(--shadow-medium);
}
.btn-secondary {
background-color: rgba(255, 255, 255, 0.2);
color: var(--coconut-white);
border-color: var(--coconut-white);
}
.btn-secondary:hover {
background-color: var(--coconut-white);
color: var(--text-dark);
}
.location-actions {
display: flex;
gap: 20px;
justify-content: center;
}
.w-100 {
width: 100%;
}
.modal {
display: none;
position: fixed;
z-index: 1001;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 600px;
border-radius: var(--border-radius);
position: relative;
}
.close-button {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
}
.close-button:hover,
.close-button:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
#map {
height: 400px;
width: 100%;
margin-top: 20px;
margin-bottom: 20px;
border-radius: var(--border-radius);
}
#confirm-location {
width: 100%;
padding: 15px;
font-size: 1.1rem;
}
/* --- Back to Home Button --- */
.back-to-home-btn {
display: inline-block;
margin-top: 10px;
margin-bottom: -10px; /* to reduce space */
padding: 8px 15px;
background-color: #f8f9fa;
color: var(--text-light);
border-radius: 50px;
font-size: 0.9rem;
font-weight: 700;
border: 1px solid var(--border-color);
transition: var(--transition);
}
.back-to-home-btn:hover {
background-color: #e9ecef;
color: var(--text-dark);
border-color: #ccc;
}
/* --- Featured Cuisines --- */
.featured-cuisines {
margin-bottom: 60px;
}
.cuisine-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
}
.cuisine-card {
position: relative;
border-radius: var(--border-radius);
overflow: hidden;
transition: var(--transition);
color: var(--coconut-white);
display: block;
}
.cuisine-card:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-medium);
}
.cuisine-card .card-image {
height: 180px;
background-size: cover;
background-position: center;
transition: var(--transition);
}
.cuisine-card:hover .card-image {
transform: scale(1.05);
}
.cuisine-card-content {
position: absolute;
bottom: 0;
left: 0;
padding: 20px;
width: 100%;
box-sizing: border-box;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
}
.cuisine-card-content h3 {
font-size: 1.5rem;
font-weight: 800;
margin: 0;
text-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
/* --- Restaurants Page --- */
.filter-bar {
background-color: var(--sand);
padding: 20px;
border-radius: var(--border-radius);
margin-bottom: 40px;
box-shadow: var(--shadow-soft);
}
.filter-bar .form-control, .filter-bar .form-select {
border-radius: 50px;
padding: 10px 20px;
border: 1px solid var(--border-color);
}
.filter-bar .btn-primary {
background-color: var(--coral);
border-color: var(--coral);
border-radius: 50px;
padding: 10px 20px;
font-weight: 700;
width: 100%;
}
.restaurant-card .card-img-top {
height: 200px;
object-fit: cover;
}
.restaurant-card .card-body {
padding: 20px;
}
.restaurant-card .card-title {
font-size: 1.3rem;
font-weight: 800;
margin-bottom: 10px;
}
.restaurant-card .card-text {
color: var(--text-light);
margin-bottom: 15px;
}
.restaurant-card .btn-outline-primary {
border-color: var(--coral);
color: var(--coral);
border-radius: 50px;
font-weight: 700;
}
.restaurant-card .btn-outline-primary:hover {
background-color: var(--coral);
color: var(--coconut-white);
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#007bff" width="38px" height="38px"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#dc3545" width="38px" height="38px"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>

After

Width:  |  Height:  |  Size: 274 B

1
assets/images/step1.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#ff5a5f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>

After

Width:  |  Height:  |  Size: 281 B

1
assets/images/step2.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#ff5a5f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>

After

Width:  |  Height:  |  Size: 332 B

1
assets/images/step3.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#ff5a5f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 17.5V14H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v1.5"></path><path d="M18 8h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-1"></path><path d="M22 12l-3 3-3-3"></path><path d="M19 15V3"></path></svg>

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

105
hero.php
View File

@ -1,25 +1,96 @@
<section class="hero-section">
<div class="hero-overlay"></div>
<div class="container hero-content-container">
<div class="hero-content">
<h1>Your favorite local food, delivered.</h1>
<p>Set your location to find restaurants near you.</p>
<div class="location-actions">
<button id="pin-location-btn" class="btn btn-primary">Pin a Location</button>
<button id="use-location-btn" class="btn btn-secondary">Use My Location</button>
</div>
<a href="menu.php" class="find-restaurants-btn" style="display: none;"></a>
<p class="delivery-note">MajuroEats delivers only within the main island zone (RitaLaura).</p>
<!-- hero.php -->
<div class="hero-section">
<div class="container text-center">
<h1 class="hero-title">Your Favorite Food, Delivered</h1>
<p class="hero-subtitle">Enter your location to see which restaurants deliver to you.</p>
<a href="#" class="btn btn-primary btn-lg" id="pin-location-btn">Pin a Location on Map</a>
<div class="mt-3">
<a href="#" class="text-muted" id="use-location-btn">or use my current location</a>
</div>
</div>
</section>
</div>
<!-- Location Modal -->
<div id="location-modal" class="modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<h2>Pin Your Location</h2>
<p>Click on the map to set your delivery address.</p>
<h2>Pin Your Delivery Location</h2>
<div id="map" style="height: 400px; width: 100%;"></div>
<button id="confirm-location-btn" class="btn btn-primary w-100">Confirm Location</button>
<button id="confirm-location-btn" class="btn btn-primary mt-3" disabled>Confirm Location</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('location-modal');
const pinBtn = document.getElementById('pin-location-btn');
const useLocationBtn = document.getElementById('use-location-btn');
const closeBtn = document.querySelector('.close-btn');
const confirmBtn = document.getElementById('confirm-location-btn');
let map;
let marker;
let selectedLocation = null;
const majuroCenter = [7.11, 171.38];
function initMap(center, zoom) {
if (map) {
map.off();
map.remove();
}
map = L.map('map').setView(center, zoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
map.on('click', function(e) {
if (marker) {
map.removeLayer(marker);
}
marker = L.marker(e.latlng).addTo(map);
selectedLocation = e.latlng;
confirmBtn.disabled = false;
});
}
pinBtn.onclick = function(e) {
e.preventDefault();
modal.style.display = 'block';
setTimeout(() => initMap(majuroCenter, 13), 100);
};
useLocationBtn.onclick = function(e) {
e.preventDefault();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
window.location.href = `restaurants.php?lat=${userLocation.lat}&lng=${userLocation.lng}`;
}, function() {
alert('Could not get your location. Please pin it on the map.');
});
} else {
alert('Geolocation is not supported by this browser.');
}
};
closeBtn.onclick = function() {
modal.style.display = 'none';
};
confirmBtn.onclick = function() {
if (selectedLocation) {
window.location.href = `restaurants.php?lat=${selectedLocation.lat}&lng=${selectedLocation.lng}`;
}
modal.style.display = 'none';
};
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = 'none';
}
};
});
</script>

190
index.php
View File

@ -1,132 +1,96 @@
<?php
// Handle AJAX request for nearby restaurants
if (isset($_GET['action']) && $_GET['action'] == 'get_restaurants' && isset($_GET['lat']) && isset($_GET['lng'])) {
require_once 'db/config.php';
header('Content-Type: application/json');
require_once 'header.php';
require_once 'hero.php';
require_once 'db/config.php';
$lat = (float)$_GET['lat'];
$lng = (float)$_GET['lng'];
$radius = 10; // Search radius in kilometers
// Fetch top-rated restaurants
$stmt = $pdo->query("
SELECT r.*, AVG(ra.rating) as avg_rating
FROM restaurants r
LEFT JOIN ratings ra ON r.id = ra.restaurant_id
GROUP BY r.id
ORDER BY avg_rating DESC
LIMIT 6
");
$top_restaurants = $stmt->fetchAll();
$db = db();
// Haversine formula to calculate distance
$stmt = $db->prepare("
SELECT r.id, r.name, r.image_url, AVG(rt.rating) as average_rating,
(6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) AS distance
FROM restaurants r
LEFT JOIN ratings rt ON r.id = rt.restaurant_id
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
GROUP BY r.id
HAVING distance < ?
ORDER BY distance
LIMIT 12
");
$stmt->execute([$lat, $lng, $lat, $radius]);
$restaurants = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get cuisines for each restaurant
$cuisine_sql = "SELECT c.name FROM cuisines c JOIN restaurant_cuisines rc ON c.id = rc.cuisine_id WHERE rc.restaurant_id = ?";
$cuisine_stmt = $db->prepare($cuisine_sql);
foreach ($restaurants as &$restaurant) {
$cuisine_stmt->execute([$restaurant['id']]);
$restaurant['cuisines'] = $cuisine_stmt->fetchAll(PDO::FETCH_COLUMN);
}
echo json_encode($restaurants);
exit;
}
// Fetch featured cuisines
$stmt = $pdo->query("SELECT * FROM cuisines ORDER BY name ASC LIMIT 6");
$cuisines = $stmt->fetchAll();
?>
<?php include 'header.php'; ?>
<?php include 'hero.php'; ?>
<div class="container page-content">
<section class="promo-section">
<div class="promo-card">
<h3>$0 Delivery Fee</h3>
<p>On your first order</p>
</div>
<div class="promo-card">
<h3>Earn Rewards</h3>
<p>With every meal</p>
</div>
<div class="promo-card">
<h3>Support Local</h3>
<p>Majuro Restaurants</p>
<div class="container mt-5 mb-5">
<section id="how-it-works" class="text-center">
<h2 class="section-title">How It Works</h2>
<div class="row">
<div class="col-md-4">
<div class="step">
<img src="assets/images/step1.svg" alt="Step 1: Choose a restaurant" class="step-icon">
<h3>Choose A Restaurant</h3>
<p>Browse from our extensive list of local restaurants.</p>
</div>
</div>
<div class="col-md-4">
<div class="step">
<img src="assets/images/step2.svg" alt="Step 2: Pick your meal" class="step-icon">
<h3>Pick Your Meal</h3>
<p>Select your favorite dishes and add them to your cart.</p>
</div>
</div>
<div class="col-md-4">
<div class="step">
<img src="assets/images/step3.svg" alt="Step 3: Fast delivery" class="step-icon">
<h3>Fast Delivery</h3>
<p>Get your food delivered right to your doorstep, fast!</p>
</div>
</div>
</div>
</section>
<?php
// Fetch Top-Rated Restaurants
require_once 'db/config.php';
$db = db();
$top_restaurants_stmt = $db->query("
SELECT r.id, r.name, r.image_url, GROUP_CONCAT(c.name SEPARATOR ', ') as cuisines, AVG(rt.rating) as average_rating
FROM restaurants r
LEFT JOIN restaurant_cuisines rc ON r.id = rc.restaurant_id
LEFT JOIN cuisines c ON rc.cuisine_id = c.id
LEFT JOIN ratings rt ON r.id = rt.restaurant_id
GROUP BY r.id
ORDER BY average_rating DESC
LIMIT 4
");
$top_restaurants = $top_restaurants_stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch Featured Cuisines
$featured_cuisines_stmt = $db->query("
SELECT c.id, c.name, c.image_url
FROM cuisines c
JOIN (
SELECT cuisine_id, COUNT(*) as restaurant_count
FROM restaurant_cuisines
GROUP BY cuisine_id
ORDER BY restaurant_count DESC
LIMIT 4
) as popular_cuisines ON c.id = popular_cuisines.cuisine_id
");
$featured_cuisines = $featured_cuisines_stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<section class="top-rated-restaurants">
<h2 class="section-title">Top-rated restaurants</h2>
<div class="restaurant-grid">
<?php foreach ($top_restaurants as $restaurant): ?>
<a href="menu.php?restaurant_id=<?php echo $restaurant['id']; ?>" class="restaurant-card">
<div class="card-image" style="background-image: url('<?php echo htmlspecialchars($restaurant['image_url'] ?: 'assets/images/hero.jpg'); ?>')"></div>
<div class="card-content">
<h3><?php echo htmlspecialchars($restaurant['name']); ?></h3>
<p class="cuisine-tags"><?php echo htmlspecialchars($restaurant['cuisines']); ?></p>
<div class="restaurant-info">
<div class="rating-display">
<?php if ($restaurant['average_rating']): ?>
<span class="star"></span> <?php echo number_format($restaurant['average_rating'], 1); ?>
<?php else: ?>
New
<?php endif; ?>
<section id="restaurants" class="mt-5">
<h2 class="section-title text-center">Top-Rated Restaurants</h2>
<div class="row">
<?php if (count($top_restaurants) > 0): ?>
<?php foreach ($top_restaurants as $restaurant): ?>
<div class="col-md-4 mb-4">
<div class="card restaurant-card">
<a href="menu.php?restaurant_id=<?php echo $restaurant['id']; ?>">
<img src="<?php echo htmlspecialchars($restaurant['image_url']); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($restaurant['name']); ?>">
</a>
<div class="card-body">
<h5 class="card-title"><a href="menu.php?restaurant_id=<?php echo $restaurant['id']; ?>"><?php echo htmlspecialchars($restaurant['name']); ?></a></h5>
<p class="card-text text-muted"><?php echo htmlspecialchars(substr($restaurant['description'], 0, 80)); ?>...</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-success"><?php echo number_format($restaurant['avg_rating'], 1); ?> ★</span>
<a href="menu.php?restaurant_id=<?php echo $restaurant['id']; ?>" class="btn btn-sm btn-outline-primary">View Menu</a>
</div>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
<?php endforeach; ?>
<?php else: ?>
<p class="text-center">No restaurants found.</p>
<?php endif; ?>
</div>
</section>
<section class="featured-cuisines">
<h2 class="section-title">Featured Cuisines</h2>
<div class="cuisine-grid">
<?php foreach ($featured_cuisines as $cuisine): ?>
<a href="index.php?cuisine=<?php echo $cuisine['id']; ?>" class="cuisine-card">
<div class="card-image" style="background-image: url('<?php echo htmlspecialchars($cuisine['image_url'] ?: 'https://via.placeholder.com/300x200'); ?>')"></div>
<div class="cuisine-card-content">
<h3><?php echo htmlspecialchars($cuisine['name']); ?></h3>
<section id="cuisines" class="mt-5">
<h2 class="section-title text-center">Featured Cuisines</h2>
<div class="row justify-content-center">
<?php if (count($cuisines) > 0): ?>
<?php foreach ($cuisines as $cuisine): ?>
<div class="col-md-2 col-sm-4 col-6 text-center mb-4">
<a href="restaurants.php?cuisine_id=<?php echo $cuisine['id']; ?>" class="cuisine-card">
<img src="<?php echo htmlspecialchars($cuisine['image_url']); ?>" alt="<?php echo htmlspecialchars($cuisine['name']); ?>" class="img-fluid">
<h5 class="mt-2"><?php echo htmlspecialchars($cuisine['name']); ?></h5>
</a>
</div>
</a>
<?php endforeach; ?>
<?php endforeach; ?>
<?php else: ?>
<p class="text-center">No cuisines found.</p>
<?php endif; ?>
</div>
</section>
</div>
<?php include 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

@ -15,18 +15,29 @@ if (!$order_id) {
$pdo = db();
$order = null;
// Fetch order details along with restaurant location
$sql = "SELECT o.*, r.name as restaurant_name, r.lat as restaurant_lat, r.lng as restaurant_lng FROM orders o JOIN restaurants r ON o.restaurant_id = r.id WHERE o.id = ?";
$params = [$order_id];
if ($user_id) {
// User is logged in, verify order belongs to them
$stmt = $pdo->prepare("SELECT o.*, r.name as restaurant_name FROM orders o JOIN restaurants r ON o.restaurant_id = r.id WHERE o.id = ? AND o.user_id = ?");
$stmt->execute([$order_id, $user_id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
$sql .= " AND o.user_id = ?";
$params[] = $user_id;
} elseif ($token) {
// Guest access, verify token
$stmt = $pdo->prepare("SELECT o.*, r.name as restaurant_name FROM orders o JOIN restaurants r ON o.restaurant_id = r.id WHERE o.id = ? AND o.guest_token = ?");
$stmt->execute([$order_id, $token]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
$sql .= " AND o.token = ?";
$params[] = $token;
} else {
// No user and no token, deny access
include 'header.php';
echo "<div class='container mt-5'><div class='alert alert-danger'>Authentication required to view this order.</div></div>";
include 'footer.php';
exit();
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
include 'header.php';
echo "<div class='container mt-5'><div class='alert alert-danger'>Order not found or you do not have permission to view it.</div></div>";
@ -165,115 +176,94 @@ document.addEventListener('DOMContentLoaded', function() {
renderTimeline(data.status);
} else if(data.error) {
console.error('Error fetching status:', data.error);
timelineContainer.innerHTML = '<div class="alert alert-warning">Could not retrieve order status. Please try again later.</div>';
}
})
.catch(error => {
console.error('Fetch error:', error);
timelineContainer.innerHTML = '<div class="alert alert-danger">An error occurred while trying to update the order status.</div>';
});
}
renderTimeline(currentStatus);
setInterval(fetchStatus, 10000); // Poll every 10 seconds
// --- Map Logic ---
// --- New Map Logic ---
let map = null;
let driverMarker = null;
let restaurantMarker = null;
let homeMarker = null;
const restaurantLocation = {
lat: <?php echo $order['restaurant_lat'] ?? 'null'; ?>,
lng: <?php echo $order['restaurant_lng'] ?? 'null'; ?>
};
// Define custom icons
const driverIcon = L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png', // A different color or style
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
iconUrl: 'assets/images/driver-icon.svg', // Custom driver icon
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -38]
});
const restaurantIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
iconUrl: 'assets/images/restaurant-icon.svg', // Custom restaurant icon
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -38]
});
const homeIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
function initializeMap() {
if (map || !restaurantLocation.lat) return;
map = L.map('map').setView([restaurantLocation.lat, restaurantLocation.lng], 14);
function initializeMap(restaurantLocation) {
if (map) return;
map = L.map('map').setView([restaurantLocation.lat, restaurantLocation.lng], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
attribution: '© OpenStreetMap contributors'
}).addTo(map);
restaurantMarker = L.marker([restaurantLocation.lat, restaurantLocation.lng], {icon: restaurantIcon}).addTo(map)
.bindPopup(`<b>${restaurantLocation.name}</b>`).openPopup();
L.marker([restaurantLocation.lat, restaurantLocation.lng], { icon: restaurantIcon })
.addTo(map)
.bindPopup('<b><?php echo htmlspecialchars($order['restaurant_name']); ?></b>');
}
function updateMap(data) {
if (!data.restaurant_location || !data.restaurant_location.lat) return;
if (!map) {
initializeMap(data.restaurant_location);
function fetchDriverLocation() {
// Only start fetching if the order is 'Out For Delivery'
if (currentStatus.toLowerCase() !== 'out for delivery') {
// You might want to hide the map or show a message until the driver is on the way
// For now, we just won't fetch the location.
return;
}
if (data.driver_location && data.driver_location.lat) {
const { lat, lng } = data.driver_location;
if (driverMarker) {
driverMarker.setLatLng([lat, lng]);
} else {
driverMarker = L.marker([lat, lng], {icon: driverIcon}).addTo(map)
.bindPopup('<b>Your Driver</b>');
}
// Center map between driver and restaurant
if (restaurantMarker) {
const bounds = L.latLngBounds(driverMarker.getLatLng(), restaurantMarker.getLatLng());
map.fitBounds(bounds.pad(0.3));
}
}
// Geocode delivery address to show home marker
// This requires a geocoding service. For now, we will skip this.
// Example using a hypothetical geocoding service:
// if (!homeMarker && data.delivery_address) {
// geocode(data.delivery_address).then(coords => {
// homeMarker = L.marker(coords, {icon: homeIcon}).addTo(map).bindPopup('<b>Your Address</b>');
// });
// }
}
function fetchMapData() {
let url = `api/get_order_status.php?order_id=${orderId}`;
let url = `api/get_driver_location.php?order_id=${orderId}`;
if (token) {
url += `&token=${token}`;
}
fetch(url)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Error fetching map data:', data.error);
if (data.lat && data.lng) {
const driverLatLng = [data.lat, data.lng];
if (!driverMarker) {
driverMarker = L.marker(driverLatLng, { icon: driverIcon }).addTo(map)
.bindPopup('<b>Your Driver</b>');
map.setView(driverLatLng, 15);
} else {
driverMarker.setLatLng(driverLatLng);
}
// Optional: Adjust map bounds to show both restaurant and driver
if (driverMarker) {
const bounds = L.latLngBounds([restaurantLocation.lat, restaurantLocation.lng], driverMarker.getLatLng());
map.fitBounds(bounds.pad(0.3));
}
} else {
updateMap(data);
console.warn('Driver location not available yet.');
}
})
.catch(error => console.error('Fetch error for map:', error));
.catch(error => console.error('Error fetching driver location:', error));
}
fetchMapData();
setInterval(fetchMapData, 5000); // Poll every 5 seconds for map updates
initializeMap();
// Start polling for driver location immediately and repeat every 5 seconds
fetchDriverLocation();
setInterval(fetchDriverLocation, 5000);
});
</script>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<?php include 'footer.php'; ?>