Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
9b7ccbdd3d versionone 2026-02-18 16:23:45 +00:00
14 changed files with 1213 additions and 144 deletions

65
admin/order.php Normal file
View File

@ -0,0 +1,65 @@
<?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 Normal file
View File

@ -0,0 +1,109 @@
<?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(); ?>

90
assets/css/custom.css Normal file
View File

@ -0,0 +1,90 @@
: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);
}

9
assets/js/main.js Normal file
View File

@ -0,0 +1,9 @@
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 Normal file
View File

@ -0,0 +1,100 @@
<?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 Normal file
View File

@ -0,0 +1,97 @@
<?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(); ?>

26
includes/bootstrap.php Normal file
View File

@ -0,0 +1,26 @@
<?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;
}

74
includes/layout.php Normal file
View File

@ -0,0 +1,74 @@
<?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";
}

349
includes/store.php Normal file
View File

@ -0,0 +1,349 @@
<?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]);
}

223
index.php
View File

@ -1,150 +1,87 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
@ini_set('display_errors', '1'); require_once __DIR__ . '/includes/bootstrap.php';
@error_reporting(E_ALL); require_once __DIR__ . '/includes/layout.php';
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; $categories = get_categories();
$now = date('Y-m-d H:i:s'); $featured = get_products(null, null, 'price_desc');
$featured = array_slice($featured, 0, 3);
render_header('E-SO9 Storefront', 'home');
?> ?>
<!doctype html> <main class="container my-5">
<html lang="en"> <section class="hero mb-5">
<head> <div class="row align-items-center g-4">
<meta charset="utf-8" /> <div class="col-lg-7">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <span class="badge badge-soft mb-3">Professional ecommerce MVP</span>
<title>New Style</title> <h1 class="display-6 fw-semibold mb-3">E-SO9 curated storefront with fast checkout and admin tracking.</h1>
<?php <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>
// Read project preview data from environment <div class="d-flex gap-3">
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <a href="/shop.php" class="btn btn-primary">Start shopping</a>
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <a href="/track.php" class="btn btn-outline-secondary">Track an order</a>
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<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>
<div class="col-lg-5">
<div class="stat-card">
<div class="row g-3">
<div class="col-6">
<div class="text-muted small">Categories</div>
<div class="h4 mb-0"><?= e((string) count($categories)) ?></div>
</div>
<div class="col-6">
<div class="text-muted small">Active products</div>
<div class="h4 mb-0">6</div>
</div>
<div class="col-12">
<div class="text-muted small">Checkout status</div>
<div class="fw-semibold">Demo payments enabled</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="mb-5">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Browse by category</h2>
<a href="/shop.php" class="link-secondary">See all products</a>
</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> </main>
<footer> <?php render_footer(); ?>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>

46
order.php Normal file
View File

@ -0,0 +1,46 @@
<?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 Normal file
View File

@ -0,0 +1,40 @@
<?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 Normal file
View File

@ -0,0 +1,63 @@
<?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 Normal file
View File

@ -0,0 +1,64 @@
<?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(); ?>