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 @@ + + + +
+ + +Store Ops
+= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-This page will update automatically as the plan is implemented.
-Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
Multi-store readiness
+Create stores, keep SKU inventory accurate, and prepare the order lifecycle with clear ownership.
Active stores
+Recent products
+Order pipeline
+Enable next
+Latest SKUs added across the workspace.
+No products yet. Start by creating a store and adding its first SKU.
+ Create store +| SKU | +Product | +Store | +Status | +Stock | +
|---|---|---|---|---|
| = h($product['sku']) ?> | += h($product['name']) ?> | += h($product['store_name']) ?> | += h(ucfirst($product['status'])) ?> | += h((string)$product['stock']) ?> | +
Track statuses from paid to delivered with payment reconciliation.
+Order states
+New → Paid → Packed → Shipped → Delivered
+Payments
+Record partials, refunds, and settlement notes.
+Customer ledger
+Centralize contact, address, and order history.
+SKU = h($product['sku']) ?>
+Store: = h($product['store_name']) ?>
+ +Price
+= h($product['currency']) ?> = h(number_format((float)$product['price'], 2)) ?>
+Stock on hand
+= h((string)$product['stock']) ?>
+Status
+ = h(ucfirst($product['status'])) ?> +Use this for quick stock corrections before orders are fulfilled.
+Assign every SKU to a store to keep inventory separated.
+Filter by store to narrow inventory.
+No products match this filter.
+Add a new SKU to get started.
+| SKU | +Product | +Store | +Status | +Price | +Stock | +
|---|---|---|---|---|---|
| = h($product['sku']) ?> | += h($product['name']) ?> | += h($product['store_name']) ?> | += h(ucfirst($product['status'])) ?> | +$= h(number_format((float)$product['price'], 2)) ?> | += h((string)$product['stock']) ?> | +
The store you requested does not exist. Return to the directory to pick another.
+ Back to stores +Store profile
+Slug: = h($store['slug']) ?> · Currency: = h($store['currency']) ?>
+Latest inventory updates for this store.
+No products created for this store yet.
+ Add the first SKU +| SKU | +Product | +Status | +Stock | +
|---|---|---|---|
| = h($product['sku']) ?> | += h($product['name']) ?> | += h(ucfirst($product['status'])) ?> | += h((string)$product['stock']) ?> | +
Each store has its own catalog, inventory, and future order pipeline.
+Multi-tenant roster with catalog counts.
+No stores added yet.
+Create the first store to unlock product management.
+| Store | +Slug | +Contact | +Products | +
|---|---|---|---|
| + + = h($store['name']) ?> + + | += h($store['slug']) ?> | += h($store['contact_email'] ?? '—') ?> | += h((string)$store['product_count']) ?> | +