diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..1248d61 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,148 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +:root { + color-scheme: light; + --bg: #f6f7f9; + --surface: #ffffff; + --border: #e3e6eb; + --text: #0f172a; + --muted: #6b7280; + --accent: #111827; + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 12px; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 15px; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.app-shell { + min-height: 100vh; + display: flex; + flex-direction: column; } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.app-header { + background: var(--surface); } -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; +.nav-link { + color: var(--muted); + font-weight: 500; } -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; +.nav-link:hover, +.nav-link:focus { + color: var(--text); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; +.hero-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 24px; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.content-card, +.metric-card, +.mini-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 20px; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); } -::-webkit-scrollbar-track { - background: transparent; +.metric-card h3 { + font-size: 1.6rem; + font-weight: 600; } -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; +.mini-card { + padding: 16px; } .table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; + margin-bottom: 0; } .table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); } .table td { - background: #fff; - padding: 1rem; - border: none; + vertical-align: middle; } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +.empty-state { + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + padding: 20px; + text-align: center; + color: var(--muted); + background: #fafafb; } -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; +.form-control, +.form-select { + border-radius: var(--radius-sm); + border-color: var(--border); } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.form-control:focus, +.form-select:focus { + border-color: #94a3b8; + box-shadow: 0 0 0 0.2rem rgba(148, 163, 184, 0.25); } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +.btn { + border-radius: var(--radius-sm); + font-weight: 600; } -.header-container { - display: flex; - justify-content: space-between; - align-items: center; +.btn-dark { + background: var(--accent); + border-color: var(--accent); } -.header-links { - display: flex; - gap: 1rem; +.btn-dark:hover { + background: #0b1120; + border-color: #0b1120; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +.btn-outline-dark { + border-color: var(--accent); + color: var(--accent); } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; - font-weight: 700; +.btn-outline-dark:hover { + background: var(--accent); + color: #fff; } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; +.toast-container { + z-index: 1055; } -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; +.badge { + border-radius: 999px; + font-weight: 500; } -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - width: 100%; - transition: all 0.3s ease; +footer { + background: var(--surface); + margin-top: auto; } -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; +@media (max-width: 768px) { + .hero-panel { + padding: 20px; + } } - -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.history-table { - width: 100%; -} - -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; -} - -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; -} - -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; -} - -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..3c13704 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,7 @@ document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); - - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; - - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; - - appendMessage(message, 'visitor'); - chatInput.value = ''; - - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); - } - }); + const toastEl = document.querySelector('.toast'); + if (toastEl && window.bootstrap) { + const toast = new bootstrap.Toast(toastEl, { delay: 3800 }); + toast.show(); + } }); diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..a456408 --- /dev/null +++ b/includes/app.php @@ -0,0 +1,129 @@ +exec( + "CREATE TABLE IF NOT EXISTS stores ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(150) NOT NULL, + slug VARCHAR(150) NOT NULL, + contact_email VARCHAR(190) DEFAULT NULL, + currency CHAR(3) NOT NULL DEFAULT 'USD', + created_at DATETIME NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); + + $pdo->exec( + "CREATE TABLE IF NOT EXISTS products ( + id INT AUTO_INCREMENT PRIMARY KEY, + store_id INT NOT NULL, + sku VARCHAR(64) NOT NULL, + name VARCHAR(180) NOT NULL, + price DECIMAL(10,2) NOT NULL DEFAULT 0.00, + stock INT NOT NULL DEFAULT 0, + status VARCHAR(24) NOT NULL DEFAULT 'active', + created_at DATETIME NOT NULL, + INDEX (store_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); +} + +function slugify(string $value): string +{ + $slug = strtolower(trim($value)); + $slug = preg_replace('/[^a-z0-9]+/', '-', $slug) ?? ''; + $slug = trim($slug, '-'); + return $slug !== '' ? $slug : 'store'; +} + +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; +} + +function get_stores(): array +{ + $stmt = db()->query( + "SELECT s.*, COUNT(p.id) AS product_count + FROM stores s + LEFT JOIN products p ON p.store_id = s.id + GROUP BY s.id + ORDER BY s.created_at DESC" + ); + return $stmt->fetchAll(); +} + +function get_store(int $id): ?array +{ + $stmt = db()->prepare("SELECT * FROM stores WHERE id = :id LIMIT 1"); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $store = $stmt->fetch(); + return $store ?: null; +} + +function get_products(?int $storeId = null, int $limit = 50): array +{ + if ($storeId) { + $stmt = db()->prepare( + "SELECT p.*, s.name AS store_name + FROM products p + JOIN stores s ON s.id = p.store_id + WHERE p.store_id = :store_id + ORDER BY p.created_at DESC + LIMIT :limit" + ); + $stmt->bindValue(':store_id', $storeId, PDO::PARAM_INT); + } else { + $stmt = db()->prepare( + "SELECT p.*, s.name AS store_name + FROM products p + JOIN stores s ON s.id = p.store_id + ORDER BY p.created_at DESC + LIMIT :limit" + ); + } + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(); +} + +function get_product(int $id): ?array +{ + $stmt = db()->prepare( + "SELECT p.*, s.name AS store_name, s.currency + FROM products p + JOIN stores s ON s.id = p.store_id + WHERE p.id = :id + LIMIT 1" + ); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); + $product = $stmt->fetch(); + return $product ?: null; +} + diff --git a/includes/layout.php b/includes/layout.php new file mode 100644 index 0000000..d70b8d1 --- /dev/null +++ b/includes/layout.php @@ -0,0 +1,82 @@ + + + + + + + <?= h($title) ?> + + + + + + + + + + + + + + +
+
+
+
+

Store Ops

+

+
+ +
+
+
+ +
+ +
+ +
+ +
+ + + + + count($stores), + 'products' => count($recentProducts), +]; + +render_header('Operations Overview', 'Multi-tenant store operations dashboard for catalog and order readiness.'); ?> - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+
+

Multi-store readiness

+

Run catalog operations across every store.

+

Create stores, keep SKU inventory accurate, and prepare the order lifecycle with clear ownership.

-
- - - +
+ Create a store + Add product +
+ + + +
+
+
+

Active stores

+

+
+
+
+
+

Recent products

+

+
+
+
+
+

Order pipeline

+

Enable next

+
+
+
+ +
+
+
+

Recent product activity

+

Latest SKUs added across the workspace.

+
+ View all +
+ + +
+

No products yet. Start by creating a store and adding its first SKU.

+ Create store +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
SKUProductStoreStatusStock
+
+ +
+ +
+
+
+

Next slice: Orders & payments

+

Track statuses from paid to delivered with payment reconciliation.

+
+ Coming next +
+
+
+
+

Order states

+

New → Paid → Packed → Shipped → Delivered

+
+
+
+
+

Payments

+

Record partials, refunds, and settlement notes.

+
+
+
+
+

Customer ledger

+

Centralize contact, address, and order history.

+
+
+
+
+ + diff --git a/product.php b/product.php new file mode 100644 index 0000000..0c247d8 --- /dev/null +++ b/product.php @@ -0,0 +1,104 @@ +prepare( + "UPDATE products SET status = :status, stock = :stock WHERE id = :id" + ); + $stmt->bindValue(':status', $status); + $stmt->bindValue(':stock', $stock, PDO::PARAM_INT); + $stmt->bindValue(':id', $productId, PDO::PARAM_INT); + $stmt->execute(); + flash_set('success', 'Product updated.'); + } else { + flash_set('error', 'Product not found.'); + } + + header('Location: /product.php?id=' . $productId); + exit; +} + +$product = $productId ? get_product($productId) : null; + +if (!$product) { + render_header('Product not found'); + ?> +
+

Product not found

+

The requested product does not exist.

+ Back to catalog +
+ + +
+
+
+

SKU

+

+

Store:

+ +
+
+
+

Price

+

+
+
+
+
+

Stock on hand

+

+
+
+
+ +
+

Status

+ +
+
+
+
+
+

Update inventory

+
+ +
+ + +
+
+ + +
+ +
+

Use this for quick stock corrections before orders are fulfilled.

+
+
+
+ + diff --git a/products.php b/products.php new file mode 100644 index 0000000..3ef9c1d --- /dev/null +++ b/products.php @@ -0,0 +1,156 @@ +prepare( + "INSERT INTO products (store_id, sku, name, price, stock, status, created_at) + VALUES (:store_id, :sku, :name, :price, :stock, :status, :created_at)" + ); + $stmt->bindValue(':store_id', $storeId, PDO::PARAM_INT); + $stmt->bindValue(':sku', $sku); + $stmt->bindValue(':name', $name); + $stmt->bindValue(':price', $price); + $stmt->bindValue(':stock', $stock, PDO::PARAM_INT); + $stmt->bindValue(':status', $status); + $stmt->bindValue(':created_at', date('Y-m-d H:i:s')); + $stmt->execute(); + flash_set('success', 'Product added to the catalog.'); + } + + header('Location: /products.php'); + exit; +} + +$filterStore = isset($_GET['store_id']) ? (int)$_GET['store_id'] : null; +$products = get_products($filterStore, 50); + +render_header('Product Catalog', 'Manage SKUs, pricing, and inventory for every store.'); +?> + +
+
+
+

Add product

+ +
Create a store first to add products.
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +

Assign every SKU to a store to keep inventory separated.

+
+
+
+
+
+
+

Catalog list

+

Filter by store to narrow inventory.

+
+
+ + +
+
+ + +
+

No products match this filter.

+

Add a new SKU to get started.

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
SKUProductStoreStatusPriceStock
$
+
+ +
+
+
+ + diff --git a/store.php b/store.php new file mode 100644 index 0000000..30cef9f --- /dev/null +++ b/store.php @@ -0,0 +1,83 @@ + +
+

Store not found

+

The store you requested does not exist. Return to the directory to pick another.

+ Back to stores +
+ + +
+
+
+

Store profile

+

+

Slug: · Currency:

+
+ +
+
+ +
+
+
+

Recent products

+

Latest inventory updates for this store.

+
+ items +
+ + +
+

No products created for this store yet.

+ Add the first SKU +
+ +
+ + + + + + + + + + + + + + + + + + + +
SKUProductStatusStock
+
+ +
+ + diff --git a/stores.php b/stores.php new file mode 100644 index 0000000..4d9b491 --- /dev/null +++ b/stores.php @@ -0,0 +1,115 @@ +prepare( + "INSERT INTO stores (name, slug, contact_email, currency, created_at) + VALUES (:name, :slug, :email, :currency, :created_at)" + ); + $stmt->bindValue(':name', $name); + $stmt->bindValue(':slug', $slug); + $stmt->bindValue(':email', $email !== '' ? $email : null); + $stmt->bindValue(':currency', $currency); + $stmt->bindValue(':created_at', date('Y-m-d H:i:s')); + $stmt->execute(); + flash_set('success', 'Store created successfully.'); + } + + header('Location: /stores.php'); + exit; +} + +$stores = get_stores(); + +render_header('Store Directory', 'Create and manage tenant stores with isolated catalogs.'); +?> + +
+
+
+

Create a store

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

Each store has its own catalog, inventory, and future order pipeline.

+
+
+
+
+
+
+

Stores

+

Multi-tenant roster with catalog counts.

+
+ total +
+ + +
+

No stores added yet.

+

Create the first store to unlock product management.

+
+ +
+ + + + + + + + + + + + + + + + + + + +
StoreSlugContactProducts
+ + + +
+
+ +
+
+
+ +