Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,65 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/../includes/layout.php';
|
|
||||||
|
|
||||||
$orderId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
|
||||||
$order = null;
|
|
||||||
$items = [];
|
|
||||||
|
|
||||||
if ($orderId > 0) {
|
|
||||||
$stmt = db()->prepare('SELECT * FROM orders WHERE id = :id');
|
|
||||||
$stmt->execute([':id' => $orderId]);
|
|
||||||
$order = $stmt->fetch();
|
|
||||||
if ($order) {
|
|
||||||
$items = get_order_items((int) $order['id']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_header('Order Detail - E-SO9', 'admin');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<a href="/admin/orders.php" class="link-secondary">Back to orders</a>
|
|
||||||
<h1 class="h3 mb-3">Order detail</h1>
|
|
||||||
|
|
||||||
<?php if (!$order): ?>
|
|
||||||
<div class="alert alert-warning">Order not found.</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-5">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Order number</div>
|
|
||||||
<div class="h5 mb-2"><?= e($order['order_number']) ?></div>
|
|
||||||
<div class="text-muted small">Customer</div>
|
|
||||||
<div class="fw-semibold"><?= e($order['customer_name']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['customer_email']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['customer_address']) ?></div>
|
|
||||||
<div class="text-muted small mt-3">Status</div>
|
|
||||||
<div class="fw-semibold"><?= e($order['status']) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-7">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small mb-2">Items</div>
|
|
||||||
<?php if (!$items): ?>
|
|
||||||
<div class="text-muted">No items recorded.</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<ul class="list-unstyled">
|
|
||||||
<?php foreach ($items as $item): ?>
|
|
||||||
<li class="d-flex justify-content-between mb-2">
|
|
||||||
<span><?= e($item['name']) ?> x <?= e((string) $item['quantity']) ?></span>
|
|
||||||
<span><?= e(format_price((float) $item['price'])) ?></span>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="d-flex justify-content-between border-top pt-2">
|
|
||||||
<span class="fw-semibold">Total</span>
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) $order['total_price'])) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
109
admin/orders.php
109
admin/orders.php
@ -1,109 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/../includes/layout.php';
|
|
||||||
|
|
||||||
$statuses = ['Processing', 'Shipped', 'Delivered'];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$orderId = isset($_POST['order_id']) ? (int) $_POST['order_id'] : 0;
|
|
||||||
$status = isset($_POST['status']) ? (string) $_POST['status'] : '';
|
|
||||||
if ($orderId > 0 && in_array($status, $statuses, true)) {
|
|
||||||
update_order_status($orderId, $status);
|
|
||||||
flash_set('success', 'Order status updated.');
|
|
||||||
}
|
|
||||||
header('Location: /admin/orders.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$orders = get_orders();
|
|
||||||
|
|
||||||
render_header('Admin Orders - E-SO9', 'admin');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div>
|
|
||||||
<h1 class="h3 mb-1">Admin orders</h1>
|
|
||||||
<p class="text-muted mb-0">Update order statuses and monitor totals.</p>
|
|
||||||
</div>
|
|
||||||
<div class="admin-note">Demo admin area. No authentication yet.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Total orders</div>
|
|
||||||
<div class="h4 mb-0"><?= e((string) count($orders)) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Total revenue</div>
|
|
||||||
<div class="h4 mb-0">
|
|
||||||
<?php
|
|
||||||
$revenue = 0.0;
|
|
||||||
foreach ($orders as $order) {
|
|
||||||
$revenue += (float) $order['total_price'];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<?= e(format_price($revenue)) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Latest status</div>
|
|
||||||
<div class="h4 mb-0"><?= $orders ? e($orders[0]['status']) : 'N/A' ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!$orders): ?>
|
|
||||||
<div class="alert alert-light border">No orders yet. Place a demo order to see entries.</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Order</th>
|
|
||||||
<th>Customer</th>
|
|
||||||
<th>Total</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Update</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($orders as $order): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="fw-semibold"><?= e($order['order_number']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['created_at']) ?></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div><?= e($order['customer_name']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['customer_email']) ?></div>
|
|
||||||
</td>
|
|
||||||
<td><?= e(format_price((float) $order['total_price'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<span class="badge text-bg-dark"><?= e($order['status']) ?></span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/admin/orders.php" class="d-flex gap-2">
|
|
||||||
<input type="hidden" name="order_id" value="<?= e((string) $order['id']) ?>" />
|
|
||||||
<select name="status" class="form-select form-select-sm">
|
|
||||||
<?php foreach ($statuses as $status): ?>
|
|
||||||
<option value="<?= e($status) ?>" <?= $order['status'] === $status ? 'selected' : '' ?>><?= e($status) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Save</button>
|
|
||||||
<a class="btn btn-link" href="/admin/order.php?id=<?= e((string) $order['id']) ?>">View</a>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg: #f6f7f9;
|
|
||||||
--surface: #ffffff;
|
|
||||||
--surface-muted: #f1f3f6;
|
|
||||||
--border: #e2e5ea;
|
|
||||||
--text: #111827;
|
|
||||||
--muted: #6b7280;
|
|
||||||
--accent: #0f766e;
|
|
||||||
--accent-strong: #0b5f59;
|
|
||||||
--radius-sm: 6px;
|
|
||||||
--radius-md: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 2.5rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-soft {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
color: var(--muted);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--surface);
|
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card img {
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
border-top-left-radius: var(--radius-md);
|
|
||||||
border-top-right-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--accent);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover,
|
|
||||||
.btn-primary:focus {
|
|
||||||
background: var(--accent-strong);
|
|
||||||
border-color: var(--accent-strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control,
|
|
||||||
.form-select {
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border-color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border-color: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-note {
|
|
||||||
background: var(--surface-muted);
|
|
||||||
border: 1px dashed var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const toastEls = document.querySelectorAll('[data-toast="auto"]');
|
|
||||||
toastEls.forEach((el) => {
|
|
||||||
if (window.bootstrap && bootstrap.Toast) {
|
|
||||||
const toast = new bootstrap.Toast(el, { delay: 3500 });
|
|
||||||
toast.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
100
cart.php
100
cart.php
@ -1,100 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$productId = isset($_POST['product_id']) ? (int) $_POST['product_id'] : 0;
|
|
||||||
$quantity = isset($_POST['quantity']) ? (int) $_POST['quantity'] : 1;
|
|
||||||
$action = isset($_POST['action']) ? (string) $_POST['action'] : 'add';
|
|
||||||
|
|
||||||
if ($productId > 0) {
|
|
||||||
if ($action === 'update') {
|
|
||||||
cart_update($productId, $quantity);
|
|
||||||
flash_set('success', 'Cart updated.');
|
|
||||||
} else {
|
|
||||||
cart_add($productId, $quantity);
|
|
||||||
flash_set('success', 'Item added to cart.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header('Location: /cart.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = cart_items();
|
|
||||||
$total = cart_total();
|
|
||||||
|
|
||||||
render_header('Cart - E-SO9', 'cart');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<h1 class="h3 mb-3">Your cart</h1>
|
|
||||||
|
|
||||||
<?php if (!$items): ?>
|
|
||||||
<div class="alert alert-light border">Your cart is empty. Browse products to get started.</div>
|
|
||||||
<a href="/shop.php" class="btn btn-outline-secondary">Go to shop</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table align-middle">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Product</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th>Qty</th>
|
|
||||||
<th>Total</th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($items as $item): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="fw-semibold"><?= e($item['product']['name']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($item['product']['category_name'] ?? 'General') ?></div>
|
|
||||||
</td>
|
|
||||||
<td><?= e(format_price((float) $item['product']['price'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/cart.php" class="d-flex gap-2">
|
|
||||||
<input type="hidden" name="action" value="update" />
|
|
||||||
<input type="hidden" name="product_id" value="<?= e((string) $item['product']['id']) ?>" />
|
|
||||||
<input type="number" name="quantity" class="form-control form-control-sm" min="1" value="<?= e((string) $item['quantity']) ?>" />
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Update</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td><?= e(format_price((float) $item['line_total'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="post" action="/cart.php">
|
|
||||||
<input type="hidden" name="action" value="update" />
|
|
||||||
<input type="hidden" name="product_id" value="<?= e((string) $item['product']['id']) ?>" />
|
|
||||||
<input type="hidden" name="quantity" value="0" />
|
|
||||||
<button class="btn btn-link text-danger" type="submit">Remove</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span class="text-muted">Subtotal</span>
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) $total)) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between mb-2">
|
|
||||||
<span class="text-muted">Shipping</span>
|
|
||||||
<span class="fw-semibold">$0.00</span>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between border-top pt-2">
|
|
||||||
<span class="fw-semibold">Total</span>
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) $total)) ?></span>
|
|
||||||
</div>
|
|
||||||
<a href="/checkout.php" class="btn btn-primary w-100 mt-3">Proceed to checkout</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
97
checkout.php
97
checkout.php
@ -1,97 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
$items = cart_items();
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$name = trim((string) ($_POST['name'] ?? ''));
|
|
||||||
$email = trim((string) ($_POST['email'] ?? ''));
|
|
||||||
$address = trim((string) ($_POST['address'] ?? ''));
|
|
||||||
|
|
||||||
if ($name === '') {
|
|
||||||
$errors[] = 'Name is required.';
|
|
||||||
}
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
$errors[] = 'Valid email is required.';
|
|
||||||
}
|
|
||||||
if ($address === '') {
|
|
||||||
$errors[] = 'Address is required.';
|
|
||||||
}
|
|
||||||
if (!$items) {
|
|
||||||
$errors[] = 'Your cart is empty.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$errors) {
|
|
||||||
$orderNumber = create_order([
|
|
||||||
'name' => $name,
|
|
||||||
'email' => $email,
|
|
||||||
'address' => $address
|
|
||||||
], $items);
|
|
||||||
|
|
||||||
if ($orderNumber) {
|
|
||||||
cart_clear();
|
|
||||||
flash_set('success', 'Order placed successfully.');
|
|
||||||
header('Location: /order.php?order=' . urlencode($orderNumber));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$errors[] = 'Unable to place the order. Try again.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_header('Checkout - E-SO9', 'cart');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<h1 class="h3 mb-3">Checkout</h1>
|
|
||||||
|
|
||||||
<?php foreach ($errors as $error): ?>
|
|
||||||
<div class="alert alert-warning"><?= e($error) ?></div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-7">
|
|
||||||
<div class="stat-card">
|
|
||||||
<h2 class="h6 mb-3">Customer details</h2>
|
|
||||||
<form method="post" action="/checkout.php">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Full name</label>
|
|
||||||
<input type="text" name="name" class="form-control" value="<?= e((string) ($_POST['name'] ?? '')) ?>" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" name="email" class="form-control" value="<?= e((string) ($_POST['email'] ?? '')) ?>" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Shipping address</label>
|
|
||||||
<textarea name="address" class="form-control" rows="3" required><?= e((string) ($_POST['address'] ?? '')) ?></textarea>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" type="submit">Place demo order</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-5">
|
|
||||||
<div class="stat-card">
|
|
||||||
<h2 class="h6 mb-3">Order summary</h2>
|
|
||||||
<?php if (!$items): ?>
|
|
||||||
<p class="text-muted">Add items to cart to continue.</p>
|
|
||||||
<?php else: ?>
|
|
||||||
<ul class="list-unstyled mb-3">
|
|
||||||
<?php foreach ($items as $item): ?>
|
|
||||||
<li class="d-flex justify-content-between mb-2">
|
|
||||||
<span><?= e($item['product']['name']) ?> x <?= e((string) $item['quantity']) ?></span>
|
|
||||||
<span><?= e(format_price((float) $item['line_total'])) ?></span>
|
|
||||||
</li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<div class="d-flex justify-content-between border-top pt-2">
|
|
||||||
<span class="fw-semibold">Total</span>
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) cart_total())) ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
require_once __DIR__ . '/store.php';
|
|
||||||
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
init_store();
|
|
||||||
|
|
||||||
function e(string $value): string {
|
|
||||||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
function flash_set(string $type, string $message): void {
|
|
||||||
$_SESSION['flash'] = ['type' => $type, 'message' => $message];
|
|
||||||
}
|
|
||||||
|
|
||||||
function flash_get(): ?array {
|
|
||||||
if (empty($_SESSION['flash'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$flash = $_SESSION['flash'];
|
|
||||||
unset($_SESSION['flash']);
|
|
||||||
return $flash;
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
function render_header(string $title, string $active = ''): void {
|
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
||||||
$cartCount = cart_count();
|
|
||||||
$isActive = fn(string $key) => $active === $key ? 'active' : '';
|
|
||||||
echo "<!doctype html>\n";
|
|
||||||
echo "<html lang=\"en\">\n";
|
|
||||||
echo "<head>\n";
|
|
||||||
echo " <meta charset=\"utf-8\" />\n";
|
|
||||||
echo " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n";
|
|
||||||
echo " <title>" . e($title) . "</title>\n";
|
|
||||||
if ($projectDescription) {
|
|
||||||
echo " <meta name=\"description\" content=\"" . e($projectDescription) . "\" />\n";
|
|
||||||
echo " <meta property=\"og:description\" content=\"" . e($projectDescription) . "\" />\n";
|
|
||||||
echo " <meta property=\"twitter:description\" content=\"" . e($projectDescription) . "\" />\n";
|
|
||||||
}
|
|
||||||
if ($projectImageUrl) {
|
|
||||||
echo " <meta property=\"og:image\" content=\"" . e($projectImageUrl) . "\" />\n";
|
|
||||||
echo " <meta property=\"twitter:image\" content=\"" . e($projectImageUrl) . "\" />\n";
|
|
||||||
}
|
|
||||||
echo " <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n";
|
|
||||||
echo " <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n";
|
|
||||||
echo " <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n";
|
|
||||||
echo " <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\">\n";
|
|
||||||
echo " <link href=\"/assets/css/custom.css?v=" . time() . "\" rel=\"stylesheet\">\n";
|
|
||||||
echo "</head>\n";
|
|
||||||
echo "<body>\n";
|
|
||||||
echo "<nav class=\"navbar navbar-expand-lg sticky-top border-bottom bg-white\">\n";
|
|
||||||
echo " <div class=\"container\">\n";
|
|
||||||
echo " <a class=\"navbar-brand fw-semibold\" href=\"/index.php\">E-SO9</a>\n";
|
|
||||||
echo " <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#siteNav\">\n";
|
|
||||||
echo " <span class=\"navbar-toggler-icon\"></span>\n";
|
|
||||||
echo " </button>\n";
|
|
||||||
echo " <div class=\"collapse navbar-collapse\" id=\"siteNav\">\n";
|
|
||||||
echo " <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n";
|
|
||||||
echo " <li class=\"nav-item\"><a class=\"nav-link " . $isActive('home') . "\" href=\"/index.php\">Home</a></li>\n";
|
|
||||||
echo " <li class=\"nav-item\"><a class=\"nav-link " . $isActive('shop') . "\" href=\"/shop.php\">Shop</a></li>\n";
|
|
||||||
echo " <li class=\"nav-item\"><a class=\"nav-link " . $isActive('track') . "\" href=\"/track.php\">Track Order</a></li>\n";
|
|
||||||
echo " <li class=\"nav-item\"><a class=\"nav-link " . $isActive('admin') . "\" href=\"/admin/orders.php\">Admin</a></li>\n";
|
|
||||||
echo " </ul>\n";
|
|
||||||
echo " <a class=\"btn btn-outline-secondary btn-sm\" href=\"/cart.php\">Cart <span class=\"badge text-bg-dark ms-1\">" . $cartCount . "</span></a>\n";
|
|
||||||
echo " </div>\n";
|
|
||||||
echo " </div>\n";
|
|
||||||
echo "</nav>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function render_footer(): void {
|
|
||||||
$flash = flash_get();
|
|
||||||
echo "<footer class=\"border-top py-4 mt-5\">\n";
|
|
||||||
echo " <div class=\"container d-flex flex-column flex-md-row justify-content-between gap-2\">\n";
|
|
||||||
echo " <div class=\"text-muted small\">E-SO9 demo storefront and admin panel.</div>\n";
|
|
||||||
echo " <div class=\"text-muted small\">Payments run in demo mode.</div>\n";
|
|
||||||
echo " </div>\n";
|
|
||||||
echo "</footer>\n";
|
|
||||||
echo "<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js\"></script>\n";
|
|
||||||
echo "<script src=\"/assets/js/main.js?v=" . time() . "\"></script>\n";
|
|
||||||
|
|
||||||
if ($flash) {
|
|
||||||
$type = $flash['type'] === 'success' ? 'success' : 'warning';
|
|
||||||
echo "<div class=\"toast-container position-fixed bottom-0 end-0 p-3\">\n";
|
|
||||||
echo " <div class=\"toast align-items-center text-bg-" . $type . " border-0\" role=\"alert\" data-toast=\"auto\">\n";
|
|
||||||
echo " <div class=\"d-flex\">\n";
|
|
||||||
echo " <div class=\"toast-body\">" . e($flash['message']) . "</div>\n";
|
|
||||||
echo " <button type=\"button\" class=\"btn-close btn-close-white me-2 m-auto\" data-bs-dismiss=\"toast\"></button>\n";
|
|
||||||
echo " </div>\n";
|
|
||||||
echo " </div>\n";
|
|
||||||
echo "</div>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "</body>\n</html>\n";
|
|
||||||
}
|
|
||||||
@ -1,349 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
function init_store(): void {
|
|
||||||
ensure_schema();
|
|
||||||
seed_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensure_schema(): void {
|
|
||||||
$pdo = db();
|
|
||||||
$pdo->exec(
|
|
||||||
'CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
||||||
);
|
|
||||||
|
|
||||||
$pdo->exec(
|
|
||||||
'CREATE TABLE IF NOT EXISTS products (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(160) NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
price DECIMAL(10,2) NOT NULL,
|
|
||||||
stock INT NOT NULL DEFAULT 0,
|
|
||||||
rating DECIMAL(3,2) NOT NULL DEFAULT 0.00,
|
|
||||||
category_id INT NULL,
|
|
||||||
image_url VARCHAR(255) NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX (category_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
||||||
);
|
|
||||||
|
|
||||||
$pdo->exec(
|
|
||||||
'CREATE TABLE IF NOT EXISTS orders (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
order_number VARCHAR(20) NOT NULL,
|
|
||||||
customer_name VARCHAR(120) NOT NULL,
|
|
||||||
customer_email VARCHAR(160) NOT NULL,
|
|
||||||
customer_address VARCHAR(255) NOT NULL,
|
|
||||||
status VARCHAR(30) NOT NULL,
|
|
||||||
total_price DECIMAL(10,2) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY (order_number)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
||||||
);
|
|
||||||
|
|
||||||
$pdo->exec(
|
|
||||||
'CREATE TABLE IF NOT EXISTS order_items (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
order_id INT NOT NULL,
|
|
||||||
product_id INT NOT NULL,
|
|
||||||
quantity INT NOT NULL,
|
|
||||||
price DECIMAL(10,2) NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX (order_id),
|
|
||||||
INDEX (product_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function seed_data(): void {
|
|
||||||
$pdo = db();
|
|
||||||
$count = (int) $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn();
|
|
||||||
if ($count > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$categories = [
|
|
||||||
'Apparel',
|
|
||||||
'Electronics',
|
|
||||||
'Moroccan Goods'
|
|
||||||
];
|
|
||||||
|
|
||||||
$categoryStmt = $pdo->prepare('INSERT INTO categories (name) VALUES (:name)');
|
|
||||||
$categoryIds = [];
|
|
||||||
foreach ($categories as $name) {
|
|
||||||
$categoryStmt->execute([':name' => $name]);
|
|
||||||
$categoryIds[$name] = (int) $pdo->lastInsertId();
|
|
||||||
}
|
|
||||||
|
|
||||||
$products = [
|
|
||||||
[
|
|
||||||
'name' => 'Atlas Hoodie',
|
|
||||||
'description' => 'Midweight cotton hoodie with brushed interior and clean stitch detailing.',
|
|
||||||
'price' => 79.00,
|
|
||||||
'stock' => 24,
|
|
||||||
'rating' => 4.6,
|
|
||||||
'category' => 'Apparel'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Casablanca Sneakers',
|
|
||||||
'description' => 'Everyday sneaker with cushioned sole and minimalist profile.',
|
|
||||||
'price' => 98.00,
|
|
||||||
'stock' => 18,
|
|
||||||
'rating' => 4.4,
|
|
||||||
'category' => 'Apparel'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Rabat Smartwatch',
|
|
||||||
'description' => 'Slim fitness watch with sleep tracking and a 7-day battery.',
|
|
||||||
'price' => 149.00,
|
|
||||||
'stock' => 12,
|
|
||||||
'rating' => 4.2,
|
|
||||||
'category' => 'Electronics'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Sahara Bluetooth Speaker',
|
|
||||||
'description' => 'Portable speaker with clean bass and long-lasting battery.',
|
|
||||||
'price' => 129.00,
|
|
||||||
'stock' => 10,
|
|
||||||
'rating' => 4.3,
|
|
||||||
'category' => 'Electronics'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Fez Copper Lamp',
|
|
||||||
'description' => 'Hand-finished lamp with soft perforated light pattern.',
|
|
||||||
'price' => 119.00,
|
|
||||||
'stock' => 7,
|
|
||||||
'rating' => 4.8,
|
|
||||||
'category' => 'Moroccan Goods'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Agadir Ceramic Tagine',
|
|
||||||
'description' => 'Glazed ceramic tagine for slow cooking and elegant serving.',
|
|
||||||
'price' => 84.00,
|
|
||||||
'stock' => 9,
|
|
||||||
'rating' => 4.7,
|
|
||||||
'category' => 'Moroccan Goods'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$productStmt = $pdo->prepare(
|
|
||||||
'INSERT INTO products (name, description, price, stock, rating, category_id) VALUES (:name, :description, :price, :stock, :rating, :category_id)'
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($products as $product) {
|
|
||||||
$productStmt->execute([
|
|
||||||
':name' => $product['name'],
|
|
||||||
':description' => $product['description'],
|
|
||||||
':price' => $product['price'],
|
|
||||||
':stock' => $product['stock'],
|
|
||||||
':rating' => $product['rating'],
|
|
||||||
':category_id' => $categoryIds[$product['category']] ?? null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_categories(): array {
|
|
||||||
$stmt = db()->query('SELECT id, name FROM categories ORDER BY name ASC');
|
|
||||||
return $stmt->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_products(?string $search = null, ?int $categoryId = null, ?string $sort = null): array {
|
|
||||||
$sql = 'SELECT p.*, c.name AS category_name FROM products p LEFT JOIN categories c ON c.id = p.category_id WHERE 1=1';
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$sql .= ' AND (p.name LIKE :search OR p.description LIKE :search)';
|
|
||||||
$params[':search'] = '%' . $search . '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($categoryId) {
|
|
||||||
$sql .= ' AND p.category_id = :category_id';
|
|
||||||
$params[':category_id'] = $categoryId;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($sort) {
|
|
||||||
case 'price_asc':
|
|
||||||
$sql .= ' ORDER BY p.price ASC';
|
|
||||||
break;
|
|
||||||
case 'price_desc':
|
|
||||||
$sql .= ' ORDER BY p.price DESC';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$sql .= ' ORDER BY p.created_at DESC';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = db()->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
return $stmt->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_product(int $id): ?array {
|
|
||||||
$stmt = db()->prepare('SELECT p.*, c.name AS category_name FROM products p LEFT JOIN categories c ON c.id = p.category_id WHERE p.id = :id');
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
$product = $stmt->fetch();
|
|
||||||
return $product ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function format_price(float $price): string {
|
|
||||||
return '$' . number_format($price, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function product_image_data(string $label): string {
|
|
||||||
$clean = preg_replace('/[^A-Za-z0-9]/', '', $label);
|
|
||||||
$text = strtoupper(substr($clean, 0, 2));
|
|
||||||
if ($text === '') {
|
|
||||||
$text = 'ES';
|
|
||||||
}
|
|
||||||
$svg = "<svg xmlns='http://www.w3.org/2000/svg' width='600' height='400' viewBox='0 0 600 400'><rect width='600' height='400' fill='#f1f3f6'/><text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-family='Inter, Arial, sans-serif' font-size='96' fill='#111827'>" . $text . "</text></svg>";
|
|
||||||
return 'data:image/svg+xml;utf8,' . rawurlencode($svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_count(): int {
|
|
||||||
$cart = $_SESSION['cart'] ?? [];
|
|
||||||
return array_sum($cart);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_items(): array {
|
|
||||||
$cart = $_SESSION['cart'] ?? [];
|
|
||||||
if (!$cart) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ids = array_keys($cart);
|
|
||||||
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
||||||
$stmt = db()->prepare(
|
|
||||||
"SELECT p.*, c.name AS category_name FROM products p LEFT JOIN categories c ON c.id = p.category_id WHERE p.id IN ($placeholders)"
|
|
||||||
);
|
|
||||||
$stmt->execute($ids);
|
|
||||||
$products = $stmt->fetchAll();
|
|
||||||
$indexed = [];
|
|
||||||
foreach ($products as $product) {
|
|
||||||
$indexed[(int) $product['id']] = $product;
|
|
||||||
}
|
|
||||||
|
|
||||||
$items = [];
|
|
||||||
foreach ($cart as $productId => $qty) {
|
|
||||||
$product = $indexed[(int) $productId] ?? null;
|
|
||||||
if (!$product) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$lineTotal = ((float) $product['price']) * $qty;
|
|
||||||
$items[] = [
|
|
||||||
'product' => $product,
|
|
||||||
'quantity' => $qty,
|
|
||||||
'line_total' => $lineTotal
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return $items;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_total(): float {
|
|
||||||
$total = 0.0;
|
|
||||||
foreach (cart_items() as $item) {
|
|
||||||
$total += $item['line_total'];
|
|
||||||
}
|
|
||||||
return $total;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_add(int $productId, int $quantity): void {
|
|
||||||
if ($quantity < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$cart = $_SESSION['cart'] ?? [];
|
|
||||||
$cart[$productId] = ($cart[$productId] ?? 0) + $quantity;
|
|
||||||
$_SESSION['cart'] = $cart;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_update(int $productId, int $quantity): void {
|
|
||||||
$cart = $_SESSION['cart'] ?? [];
|
|
||||||
if ($quantity < 1) {
|
|
||||||
unset($cart[$productId]);
|
|
||||||
} else {
|
|
||||||
$cart[$productId] = $quantity;
|
|
||||||
}
|
|
||||||
$_SESSION['cart'] = $cart;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cart_clear(): void {
|
|
||||||
unset($_SESSION['cart']);
|
|
||||||
}
|
|
||||||
|
|
||||||
function create_order(array $customer, array $items): ?string {
|
|
||||||
if (!$items) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$orderNumber = 'ES9-' . strtoupper(bin2hex(random_bytes(3)));
|
|
||||||
$total = 0.0;
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$total += $item['line_total'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderStmt = $pdo->prepare(
|
|
||||||
'INSERT INTO orders (order_number, customer_name, customer_email, customer_address, status, total_price) VALUES (:order_number, :customer_name, :customer_email, :customer_address, :status, :total_price)'
|
|
||||||
);
|
|
||||||
$orderStmt->execute([
|
|
||||||
':order_number' => $orderNumber,
|
|
||||||
':customer_name' => $customer['name'],
|
|
||||||
':customer_email' => $customer['email'],
|
|
||||||
':customer_address' => $customer['address'],
|
|
||||||
':status' => 'Processing',
|
|
||||||
':total_price' => $total
|
|
||||||
]);
|
|
||||||
|
|
||||||
$orderId = (int) $pdo->lastInsertId();
|
|
||||||
|
|
||||||
$itemStmt = $pdo->prepare(
|
|
||||||
'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (:order_id, :product_id, :quantity, :price)'
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$itemStmt->execute([
|
|
||||||
':order_id' => $orderId,
|
|
||||||
':product_id' => $item['product']['id'],
|
|
||||||
':quantity' => $item['quantity'],
|
|
||||||
':price' => $item['product']['price']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
return $orderNumber;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_order_by_number(string $orderNumber): ?array {
|
|
||||||
$stmt = db()->prepare('SELECT * FROM orders WHERE order_number = :order_number');
|
|
||||||
$stmt->execute([':order_number' => $orderNumber]);
|
|
||||||
$order = $stmt->fetch();
|
|
||||||
return $order ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_order_items(int $orderId): array {
|
|
||||||
$stmt = db()->prepare(
|
|
||||||
'SELECT oi.*, p.name, p.description FROM order_items oi LEFT JOIN products p ON p.id = oi.product_id WHERE oi.order_id = :order_id'
|
|
||||||
);
|
|
||||||
$stmt->execute([':order_id' => $orderId]);
|
|
||||||
return $stmt->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_orders(): array {
|
|
||||||
$stmt = db()->query('SELECT * FROM orders ORDER BY created_at DESC');
|
|
||||||
return $stmt->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_order_status(int $orderId, string $status): void {
|
|
||||||
$stmt = db()->prepare('UPDATE orders SET status = :status WHERE id = :id');
|
|
||||||
$stmt->execute([':status' => $status, ':id' => $orderId]);
|
|
||||||
}
|
|
||||||
225
index.php
225
index.php
@ -1,87 +1,150 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
@ini_set('display_errors', '1');
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
@error_reporting(E_ALL);
|
||||||
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
$categories = get_categories();
|
$phpVersion = PHP_VERSION;
|
||||||
$featured = get_products(null, null, 'price_desc');
|
$now = date('Y-m-d H:i:s');
|
||||||
$featured = array_slice($featured, 0, 3);
|
|
||||||
|
|
||||||
render_header('E-SO9 Storefront', 'home');
|
|
||||||
?>
|
?>
|
||||||
<main class="container my-5">
|
<!doctype html>
|
||||||
<section class="hero mb-5">
|
<html lang="en">
|
||||||
<div class="row align-items-center g-4">
|
<head>
|
||||||
<div class="col-lg-7">
|
<meta charset="utf-8" />
|
||||||
<span class="badge badge-soft mb-3">Professional ecommerce MVP</span>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<h1 class="display-6 fw-semibold mb-3">E-SO9 curated storefront with fast checkout and admin tracking.</h1>
|
<title>New Style</title>
|
||||||
<p class="text-muted mb-4">Browse products, add to cart, place a demo order, and track status. Admins can review orders and update fulfillment.</p>
|
<?php
|
||||||
<div class="d-flex gap-3">
|
// Read project preview data from environment
|
||||||
<a href="/shop.php" class="btn btn-primary">Start shopping</a>
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||||
<a href="/track.php" class="btn btn-outline-secondary">Track an order</a>
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||||
</div>
|
?>
|
||||||
</div>
|
<?php if ($projectDescription): ?>
|
||||||
<div class="col-lg-5">
|
<!-- Meta description -->
|
||||||
<div class="stat-card">
|
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||||
<div class="row g-3">
|
<!-- Open Graph meta tags -->
|
||||||
<div class="col-6">
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
<div class="text-muted small">Categories</div>
|
<!-- Twitter meta tags -->
|
||||||
<div class="h4 mb-0"><?= e((string) count($categories)) ?></div>
|
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<div class="col-6">
|
<?php if ($projectImageUrl): ?>
|
||||||
<div class="text-muted small">Active products</div>
|
<!-- Open Graph image -->
|
||||||
<div class="h4 mb-0">6</div>
|
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
</div>
|
<!-- Twitter image -->
|
||||||
<div class="col-12">
|
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||||
<div class="text-muted small">Checkout status</div>
|
<?php endif; ?>
|
||||||
<div class="fw-semibold">Demo payments enabled</div>
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
</div>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
</div>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
</div>
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color-start: #6a11cb;
|
||||||
|
--bg-color-end: #2575fc;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||||
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
@keyframes bg-pan {
|
||||||
|
0% { background-position: 0% 0%; }
|
||||||
|
100% { background-position: 100% 100%; }
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-color);
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
margin: 1.25rem auto 1.25rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px; height: 1px;
|
||||||
|
padding: 0; margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap; border: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Analyzing your requirements and generating your website…</h1>
|
||||||
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
|
<span class="sr-only">Loading…</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||||
|
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||||
|
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</main>
|
||||||
|
<footer>
|
||||||
<section class="mb-5">
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
</footer>
|
||||||
<h2 class="h5 mb-0">Browse by category</h2>
|
</body>
|
||||||
<a href="/shop.php" class="link-secondary">See all products</a>
|
</html>
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<?php foreach ($categories as $category): ?>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="stat-card h-100">
|
|
||||||
<div class="text-muted small">Category</div>
|
|
||||||
<div class="h5 mb-2"><?= e($category['name']) ?></div>
|
|
||||||
<a href="/shop.php?category=<?= e((string) $category['id']) ?>" class="btn btn-outline-secondary btn-sm">Explore</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h2 class="h5 mb-0">Featured products</h2>
|
|
||||||
<span class="text-muted small">Hand-picked for the launch</span>
|
|
||||||
</div>
|
|
||||||
<div class="row g-4">
|
|
||||||
<?php foreach ($featured as $product): ?>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="product-card h-100">
|
|
||||||
<img src="<?= e($product['image_url'] ?: product_image_data($product['name'])) ?>" class="img-fluid" alt="<?= e($product['name']) ?>" width="600" height="400" />
|
|
||||||
<div class="p-3">
|
|
||||||
<div class="text-muted small mb-1"><?= e($product['category_name'] ?? 'General') ?></div>
|
|
||||||
<h3 class="h6 mb-2"><?= e($product['name']) ?></h3>
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) $product['price'])) ?></span>
|
|
||||||
<a href="/product.php?id=<?= e((string) $product['id']) ?>" class="btn btn-outline-secondary btn-sm">View</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
|
|||||||
46
order.php
46
order.php
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
$orderNumber = isset($_GET['order']) ? (string) $_GET['order'] : '';
|
|
||||||
$order = $orderNumber ? get_order_by_number($orderNumber) : null;
|
|
||||||
|
|
||||||
render_header('Order - E-SO9', 'track');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<h1 class="h3 mb-3">Order details</h1>
|
|
||||||
|
|
||||||
<?php if (!$order): ?>
|
|
||||||
<div class="alert alert-warning">Order not found. Check the number and try again.</div>
|
|
||||||
<a href="/track.php" class="btn btn-outline-secondary">Track another order</a>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-7">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<div class="text-muted small">Order number</div>
|
|
||||||
<div class="h5 mb-1"><?= e($order['order_number']) ?></div>
|
|
||||||
</div>
|
|
||||||
<span class="badge text-bg-dark align-self-start"><?= e($order['status']) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="text-muted small">Customer</div>
|
|
||||||
<div class="fw-semibold"><?= e($order['customer_name']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['customer_email']) ?></div>
|
|
||||||
<div class="text-muted small"><?= e($order['customer_address']) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-5">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Total paid</div>
|
|
||||||
<div class="h4 mb-0"><?= e(format_price((float) $order['total_price'])) ?></div>
|
|
||||||
<div class="text-muted small mt-2">Placed <?= e($order['created_at']) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
40
product.php
40
product.php
@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
|
||||||
$product = $id ? get_product($id) : null;
|
|
||||||
|
|
||||||
if (!$product) {
|
|
||||||
flash_set('warning', 'Product not found.');
|
|
||||||
header('Location: /shop.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_header($product['name'] . ' - E-SO9', 'shop');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<img src="<?= e($product['image_url'] ?: product_image_data($product['name'])) ?>" class="img-fluid rounded" alt="<?= e($product['name']) ?>" width="800" height="600" />
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="text-muted small mb-2"><?= e($product['category_name'] ?? 'General') ?></div>
|
|
||||||
<h1 class="h3 mb-3"><?= e($product['name']) ?></h1>
|
|
||||||
<p class="text-muted mb-3"><?= e($product['description']) ?></p>
|
|
||||||
<div class="d-flex align-items-center gap-3 mb-4">
|
|
||||||
<span class="h4 mb-0"><?= e(format_price((float) $product['price'])) ?></span>
|
|
||||||
<span class="badge badge-soft">Rating <?= e((string) $product['rating']) ?></span>
|
|
||||||
<span class="text-muted small">Stock <?= e((string) $product['stock']) ?></span>
|
|
||||||
</div>
|
|
||||||
<form method="post" action="/cart.php" class="d-flex gap-2">
|
|
||||||
<input type="hidden" name="product_id" value="<?= e((string) $product['id']) ?>" />
|
|
||||||
<input type="number" name="quantity" class="form-control" min="1" max="<?= e((string) $product['stock']) ?>" value="1" />
|
|
||||||
<button class="btn btn-primary" type="submit">Add to cart</button>
|
|
||||||
</form>
|
|
||||||
<div class="admin-note mt-4">Demo checkout only. Orders are stored locally for the admin view.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
63
shop.php
63
shop.php
@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
$search = isset($_GET['search']) ? trim((string) $_GET['search']) : null;
|
|
||||||
$categoryId = isset($_GET['category']) ? (int) $_GET['category'] : null;
|
|
||||||
$sort = isset($_GET['sort']) ? (string) $_GET['sort'] : null;
|
|
||||||
|
|
||||||
$categories = get_categories();
|
|
||||||
$products = get_products($search, $categoryId, $sort);
|
|
||||||
|
|
||||||
render_header('Shop - E-SO9', 'shop');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-start align-items-lg-center gap-3 mb-4">
|
|
||||||
<div>
|
|
||||||
<h1 class="h3 mb-1">Shop all products</h1>
|
|
||||||
<p class="text-muted mb-0">Search, filter, and add items to your cart.</p>
|
|
||||||
</div>
|
|
||||||
<form class="d-flex gap-2" method="get" action="/shop.php">
|
|
||||||
<input type="search" name="search" class="form-control" placeholder="Search products" value="<?= e($search ?? '') ?>" />
|
|
||||||
<select name="category" class="form-select">
|
|
||||||
<option value="">All categories</option>
|
|
||||||
<?php foreach ($categories as $category): ?>
|
|
||||||
<option value="<?= e((string) $category['id']) ?>" <?= $categoryId === (int) $category['id'] ? 'selected' : '' ?>>
|
|
||||||
<?= e($category['name']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
<select name="sort" class="form-select">
|
|
||||||
<option value="">Newest</option>
|
|
||||||
<option value="price_asc" <?= $sort === 'price_asc' ? 'selected' : '' ?>>Price low-high</option>
|
|
||||||
<option value="price_desc" <?= $sort === 'price_desc' ? 'selected' : '' ?>>Price high-low</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-outline-secondary" type="submit">Apply</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!$products): ?>
|
|
||||||
<div class="alert alert-light border">No products matched your filters.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="row g-4">
|
|
||||||
<?php foreach ($products as $product): ?>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="product-card h-100">
|
|
||||||
<img src="<?= e($product['image_url'] ?: product_image_data($product['name'])) ?>" class="img-fluid" alt="<?= e($product['name']) ?>" width="600" height="400" />
|
|
||||||
<div class="p-3 d-flex flex-column h-100">
|
|
||||||
<div class="text-muted small mb-1"><?= e($product['category_name'] ?? 'General') ?></div>
|
|
||||||
<h2 class="h6 mb-2"><?= e($product['name']) ?></h2>
|
|
||||||
<p class="text-muted small flex-grow-1"><?= e($product['description']) ?></p>
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<span class="fw-semibold"><?= e(format_price((float) $product['price'])) ?></span>
|
|
||||||
<a href="/product.php?id=<?= e((string) $product['id']) ?>" class="btn btn-outline-secondary btn-sm">View</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
64
track.php
64
track.php
@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/includes/bootstrap.php';
|
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
|
||||||
|
|
||||||
$orderNumber = '';
|
|
||||||
$email = '';
|
|
||||||
$order = null;
|
|
||||||
$error = '';
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$orderNumber = trim((string) ($_POST['order_number'] ?? ''));
|
|
||||||
$email = trim((string) ($_POST['email'] ?? ''));
|
|
||||||
|
|
||||||
if ($orderNumber === '' || $email === '') {
|
|
||||||
$error = 'Both fields are required.';
|
|
||||||
} else {
|
|
||||||
$order = get_order_by_number($orderNumber);
|
|
||||||
if (!$order || strtolower((string) $order['customer_email']) !== strtolower($email)) {
|
|
||||||
$error = 'No matching order found.';
|
|
||||||
$order = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_header('Track Order - E-SO9', 'track');
|
|
||||||
?>
|
|
||||||
<main class="container my-5">
|
|
||||||
<h1 class="h3 mb-3">Track your order</h1>
|
|
||||||
<div class="row g-4">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="stat-card">
|
|
||||||
<form method="post" action="/track.php">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Order number</label>
|
|
||||||
<input type="text" name="order_number" class="form-control" value="<?= e($orderNumber) ?>" required />
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Email</label>
|
|
||||||
<input type="email" name="email" class="form-control" value="<?= e($email) ?>" required />
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary" type="submit">Check status</button>
|
|
||||||
</form>
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div class="alert alert-warning mt-3"><?= e($error) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<?php if ($order): ?>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="text-muted small">Status</div>
|
|
||||||
<div class="h4 mb-2"><?= e($order['status']) ?></div>
|
|
||||||
<div class="text-muted small">Order total</div>
|
|
||||||
<div class="fw-semibold mb-2"><?= e(format_price((float) $order['total_price'])) ?></div>
|
|
||||||
<a href="/order.php?order=<?= e($order['order_number']) ?>" class="btn btn-outline-secondary btn-sm">View full order</a>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="admin-note">Place a demo order to see tracking updates here.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<?php render_footer(); ?>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user