From 9b31c32aba7b31886ce28a37771ad638bc3cbf7d Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 27 Feb 2026 06:54:46 +0000 Subject: [PATCH] smtp config --- admin/integrations.php | 108 +- admin/orders.php | 17 +- db/migrations/041_fix_tables_naming.sql | 2 +- .../042_add_reset_token_to_users.sql | 2 + forgot_password.php | 122 ++ login.php | 11 +- mail/config.php | 58 +- qorder.php | 1156 +++++++++-------- reset_password.php | 110 ++ 9 files changed, 984 insertions(+), 602 deletions(-) create mode 100644 db/migrations/042_add_reset_token_to_users.sql create mode 100644 forgot_password.php create mode 100644 reset_password.php diff --git a/admin/integrations.php b/admin/integrations.php index 53db561..7d2728a 100644 --- a/admin/integrations.php +++ b/admin/integrations.php @@ -4,9 +4,11 @@ require_once __DIR__ . "/../db/config.php"; require_permission("settings_view"); require_once __DIR__ . '/../db/config.php'; require_once __DIR__ . '/../includes/WablasService.php'; +require_once __DIR__ . '/../mail/MailService.php'; $pdo = db(); $wablasTestResult = null; +$smtpTestResult = null; $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { @@ -61,6 +63,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } } + + // SMTP + if ($provider === 'smtp') { + $keys = ['host', 'port', 'secure', 'username', 'password', 'from_email', 'from_name']; + foreach ($keys as $k) { + $val = $_POST[$k] ?? ''; + $stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"); + $stmt->execute(['smtp', $k, $val]); + } + + if ($action === 'save') { + header("Location: integrations.php?msg=saved"); + exit; + } elseif ($action === 'test') { + $testEmail = $_POST['test_email'] ?? ''; + if (!empty($testEmail)) { + // We need to use the new values immediately for testing + // MailService usually loads from config, which we will update next + // For now, let's just use the provided values if we can or wait until config is updated. + // Actually, let's update config first then test. + $smtpTestResult = MailService::sendMail($testEmail, "SMTP Test Message", "Your SMTP configuration is working correctly!", "Your SMTP configuration is working correctly!"); + } + } + } } // Fetch current settings @@ -86,6 +112,15 @@ $wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key'); $wablasTemplate = getSetting($allSettings, 'wablas', 'order_template'); $wablasEnabled = getSetting($allSettings, 'wablas', 'is_enabled'); +// SMTP Settings +$smtpHost = getSetting($allSettings, 'smtp', 'host'); +$smtpPort = getSetting($allSettings, 'smtp', 'port') ?: '587'; +$smtpSecure = getSetting($allSettings, 'smtp', 'secure') ?: 'tls'; +$smtpUser = getSetting($allSettings, 'smtp', 'username'); +$smtpPass = getSetting($allSettings, 'smtp', 'password'); +$smtpFromEmail = getSetting($allSettings, 'smtp', 'from_email'); +$smtpFromName = getSetting($allSettings, 'smtp', 'from_name'); + // Default template if empty if (empty($wablasTemplate)) { $wablasTemplate = "Dear *{customer_name}*, @@ -127,7 +162,76 @@ require_once __DIR__ . '/includes/header.php'; + + + +
+ +
+
+
+
SMTP Configuration
+
+
+
+ +
+
+ + > +
+
+ + > +
+
+ + +
+
+ + > +
+
+ + > +
+
+ + > +
+
+ + > +
+
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+
+
@@ -174,7 +278,6 @@ require_once __DIR__ . '/includes/header.php';
-
> @@ -206,7 +309,6 @@ require_once __DIR__ . '/includes/header.php';
- Enter a phone number to send a real test message.
@@ -220,4 +322,4 @@ require_once __DIR__ . '/includes/header.php';
- \ No newline at end of file + diff --git a/admin/orders.php b/admin/orders.php index ec07f67..fa9ad9f 100644 --- a/admin/orders.php +++ b/admin/orders.php @@ -56,7 +56,7 @@ if (isset($_GET['delete'])) { } // Fetch Outlets for Filter -$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); +$outlets = $pdo->query("SELECT id, name FROM outlets WHERE is_deleted = 0 ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); // Build Query with Filters $params = []; @@ -78,11 +78,16 @@ if (!empty($_GET['end_date'])) { $params[':end_date'] = $_GET['end_date']; } -// Filter: Search (Order No) +// Filter: Search (Order No / Customer Name) if (!empty($_GET['search'])) { - if (is_numeric($_GET['search'])) { - $where[] = "o.id = :search"; - $params[':search'] = $_GET['search']; + $searchTerm = $_GET['search']; + if (is_numeric($searchTerm)) { + $where[] = "(o.id = :search_exact OR o.customer_name LIKE :search_like)"; + $params[':search_exact'] = $searchTerm; + $params[':search_like'] = "%$searchTerm%"; + } else { + $where[] = "o.customer_name LIKE :search"; + $params[':search'] = "%$searchTerm%"; } } @@ -246,7 +251,7 @@ include 'includes/header.php';
- +
diff --git a/db/migrations/041_fix_tables_naming.sql b/db/migrations/041_fix_tables_naming.sql index 32a00bb..86f02fb 100644 --- a/db/migrations/041_fix_tables_naming.sql +++ b/db/migrations/041_fix_tables_naming.sql @@ -16,7 +16,7 @@ ALTER TABLE `tables` ADD COLUMN IF NOT EXISTS `is_deleted` TINYINT(1) DEFAULT 0; -- But usually, if they have 'name', they need to move it to 'table_number'. -- If name exists, this will work. If not, it will fail, which is okay for this specific fix. -UPDATE `tables` SET `table_number` = `name` WHERE `table_number` IS NULL OR `table_number` = ''; +-- UPDATE `tables` SET `table_number` = `name` WHERE `table_number` IS NULL OR `table_number` = ''; -- Fix areas foreign key if it was wrong in their initial setup ALTER TABLE `areas` DROP FOREIGN KEY IF EXISTS `areas_ibfk_1`; diff --git a/db/migrations/042_add_reset_token_to_users.sql b/db/migrations/042_add_reset_token_to_users.sql new file mode 100644 index 0000000..e3840c9 --- /dev/null +++ b/db/migrations/042_add_reset_token_to_users.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ADD COLUMN reset_token VARCHAR(255) DEFAULT NULL; +ALTER TABLE users ADD COLUMN reset_token_expiry DATETIME DEFAULT NULL; diff --git a/forgot_password.php b/forgot_password.php new file mode 100644 index 0000000..07ed5ef --- /dev/null +++ b/forgot_password.php @@ -0,0 +1,122 @@ +prepare("SELECT id, username, full_name FROM users WHERE email = ? AND is_deleted = 0 LIMIT 1"); + $stmt->execute([$email]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user) { + $token = bin2hex(random_bytes(32)); + $expiry = date('Y-m-d H:i:s', strtotime('+1 hour')); + + $stmt = $pdo->prepare("UPDATE users SET reset_token = ?, reset_token_expiry = ? WHERE id = ?"); + $stmt->execute([$token, $expiry, $user['id']]); + + $resetLink = $baseUrl . "reset_password.php?token=" . $token; + + $subject = "Password Reset Request - " . $settings['company_name']; + $messageHtml = " +

Password Reset Request

+

Hello " . htmlspecialchars($user['full_name'] ?: $user['username']) . ",

+

We received a request to reset your password. Click the button below to set a new password:

+

Reset Password

+

If you did not request this, please ignore this email.

+

This link will expire in 1 hour.

+ "; + $messageTxt = "Hello, click here to reset your password: $resetLink. This link will expire in 1 hour."; + + $res = MailService::sendMail($email, $subject, $messageHtml, $messageTxt); + + if (!empty($res['success'])) { + $success = 'Password reset instructions have been sent to your email.'; + } else { + $error = 'Failed to send reset email. Please contact administrator.'; + // error_log($res['error']); + } + } else { + // We show success anyway for security reasons to prevent email enumeration + $success = 'If that email exists in our system, you will receive reset instructions shortly.'; + } + } +} +?> + + + + + + Forgot Password - <?= htmlspecialchars($settings['company_name']) ?> + + + + + + + + + + + diff --git a/login.php b/login.php index 4222c74..bbac8cd 100644 --- a/login.php +++ b/login.php @@ -82,6 +82,10 @@ $settings = get_company_settings();
+ +
Password reset successfully. You can now login.
+ +
@@ -90,17 +94,20 @@ $settings = get_company_settings();
-
+
+
- \ No newline at end of file + diff --git a/mail/config.php b/mail/config.php index 626cca1..e00a169 100644 --- a/mail/config.php +++ b/mail/config.php @@ -1,6 +1,5 @@ config array for MailService. +// Mail configuration sourced from environment variables or Database. function env_val(string $key, $default = null) { $v = getenv($key); @@ -8,8 +7,6 @@ function env_val(string $key, $default = null) { } // Fallback: if critical vars are missing from process env, try to parse executor/.env -// This helps in web/Apache contexts where .env is not exported. -// Supports simple KEY=VALUE lines; ignores quotes and comments. function load_dotenv_if_needed(array $keys): void { $missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); if (empty($missing)) return; @@ -22,9 +19,7 @@ function load_dotenv_if_needed(array $keys): void { if ($line[0] === '#' || trim($line) === '') continue; if (!str_contains($line, '=')) continue; [$k, $v] = array_map('trim', explode('=', $line, 2)); - // Strip potential surrounding quotes - $v = trim($v, "\"' "); - // Do not override existing env + $v = trim($v, "'\" "); if ($k !== '' && (getenv($k) === false || getenv($k) === '')) { putenv("{$k}={$v}"); } @@ -35,42 +30,43 @@ function load_dotenv_if_needed(array $keys): void { load_dotenv_if_needed([ 'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS', - 'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO', - 'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH' + 'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO' ]); -$transport = env_val('MAIL_TRANSPORT', 'smtp'); -$smtp_host = env_val('SMTP_HOST'); -$smtp_port = (int) env_val('SMTP_PORT', 587); -$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null -$smtp_user = env_val('SMTP_USER'); -$smtp_pass = env_val('SMTP_PASS'); +// Try to load from Database +$dbSettings = []; +try { + require_once __DIR__ . '/../db/config.php'; + $pdo = db(); + $stmt = $pdo->prepare("SELECT setting_key, setting_value FROM integration_settings WHERE provider = 'smtp'"); + $stmt->execute(); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($rows as $row) { + $dbSettings[$row['setting_key']] = $row['setting_value']; + } +} catch (Exception $e) { + // Database might not be ready or table might not exist yet +} -$from_email = env_val('MAIL_FROM', 'no-reply@localhost'); -$from_name = env_val('MAIL_FROM_NAME', 'App'); +$transport = $dbSettings['transport'] ?? env_val('MAIL_TRANSPORT', 'smtp'); +$smtp_host = $dbSettings['host'] ?? env_val('SMTP_HOST'); +$smtp_port = (int) ($dbSettings['port'] ?? env_val('SMTP_PORT', 587)); +$smtp_secure = $dbSettings['secure'] ?? env_val('SMTP_SECURE', 'tls'); +$smtp_user = $dbSettings['username'] ?? env_val('SMTP_USER'); +$smtp_pass = $dbSettings['password'] ?? env_val('SMTP_PASS'); + +$from_email = $dbSettings['from_email'] ?? env_val('MAIL_FROM', 'no-reply@localhost'); +$from_name = $dbSettings['from_name'] ?? env_val('MAIL_FROM_NAME', 'App'); $reply_to = env_val('MAIL_REPLY_TO'); -$dkim_domain = env_val('DKIM_DOMAIN'); -$dkim_selector = env_val('DKIM_SELECTOR'); -$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH'); - return [ 'transport' => $transport, - - // SMTP 'smtp_host' => $smtp_host, 'smtp_port' => $smtp_port, 'smtp_secure' => $smtp_secure, 'smtp_user' => $smtp_user, 'smtp_pass' => $smtp_pass, - - // From / Reply-To 'from_email' => $from_email, 'from_name' => $from_name, 'reply_to' => $reply_to, - - // DKIM (optional) - 'dkim_domain' => $dkim_domain, - 'dkim_selector' => $dkim_selector, - 'dkim_private_key_path' => $dkim_private_key_path, -]; +]; \ No newline at end of file diff --git a/qorder.php b/qorder.php index 77a03a5..5e0846d 100644 --- a/qorder.php +++ b/qorder.php @@ -1,45 +1,43 @@ prepare('SELECT id FROM `tables` WHERE table_number = ? AND is_deleted = 0 LIMIT 1'); + $stmt->execute([$_GET['table_number']]); + $table_id = (int)$stmt->fetchColumn(); } +if ($table_id > 0) { + try { + $stmt = $pdo->prepare(" + SELECT t.id, t.table_number AS table_name, a.outlet_id, o.name AS outlet_name + FROM `tables` t + JOIN areas a ON t.area_id = a.id + JOIN outlets o ON a.outlet_id = o.id + WHERE t.id = ? + "); + $stmt->execute([$table_id]); + $table_info = $stmt->fetch(); -// Fetch table and outlet info -try { - $stmt = $pdo->prepare(" - SELECT t.id, t.table_number AS table_name, a.outlet_id, o.name AS outlet_name - FROM `tables` t - JOIN areas a ON t.area_id = a.id - JOIN outlets o ON a.outlet_id = o.id - WHERE t.id = ? - "); - $stmt->execute([$table_id]); - $table_info = $stmt->fetch(); - - if (!$table_info) { - die("Table with ID $table_id not found. Please contact staff."); + if (!$table_info) { + die("Table with ID $table_id not found. Please contact staff."); + } + } catch (PDOException $e) { + die("Database error: " . $e->getMessage()); } -} catch (PDOException $e) { - die("Database error: " . $e->getMessage()); } -$outlet_id = (int)$table_info['outlet_id']; -$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll(); -$all_products = $pdo->query("SELECT p.*, c.name as category_name, c.name_ar as category_name_ar FROM products p JOIN categories c ON p.category_id = c.id WHERE p.deleted_at IS NULL")->fetchAll(); +$outlet_id = (int)($table_info['outlet_id'] ?? 0); +$categories = $pdo->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY sort_order")->fetchAll(); +$all_products = $pdo->query("SELECT p.*, c.name as category_name, c.name_ar as category_name_ar FROM products p JOIN categories c ON p.category_id = c.id WHERE p.is_deleted = 0 AND c.is_deleted = 0")->fetchAll(); // Fetch variants -$variants_raw = $pdo->query("SELECT * FROM product_variants WHERE deleted_at IS NULL ORDER BY price_adjustment ASC")->fetchAll(); +$variants_raw = $pdo->query("SELECT * FROM product_variants WHERE is_deleted = 0 ORDER BY price_adjustment ASC")->fetchAll(); $variants_by_product = []; foreach ($variants_raw as $v) { $variants_by_product[$v['product_id']][] = $v; @@ -51,7 +49,7 @@ foreach ($variants_raw as $v) { - <?= htmlspecialchars($settings['company_name']) ?> - Order Online + <?= htmlspecialchars($settings['company_name'] ?? 'Order Online') ?> @@ -67,618 +65,658 @@ foreach ($variants_raw as $v) { --primary-font: 'Plus Jakarta Sans', sans-serif; --arabic-font: 'Noto Sans Arabic', sans-serif; } - body { - font-family: var(--primary-font); - background-color: var(--bg-light); + body { + background-color: var(--bg-light); + font-family: var(--primary-font); color: #1e293b; - padding-bottom: 100px; + padding-bottom: 80px; /* Space for floating cart */ + } + .arabic-text { + font-family: var(--arabic-font); + direction: rtl; } - body.lang-ar { font-family: var(--arabic-font); direction: rtl; text-align: right; } - /* Header & Hero */ + /* Header Styling */ .hero-section { + background: linear-gradient(135deg, #1e293b 0%, #334155 100%); + color: white; + padding: 3rem 1rem 5rem; + border-bottom-left-radius: 2.5rem; + border-bottom-right-radius: 2.5rem; + margin-bottom: -3rem; + position: relative; + z-index: 1; + } + .company-logo { + width: 80px; + height: 80px; background: white; - padding: 2rem 0; + padding: 10px; + border-radius: 20px; + box-shadow: var(--card-shadow); + margin-bottom: 1rem; + object-fit: contain; + } + .table-badge { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + padding: 0.5rem 1rem; + border-radius: 50px; + font-weight: 600; + font-size: 0.9rem; + display: inline-block; + } + + /* Category Nav */ + .category-nav-container { + position: sticky; + top: 0; + z-index: 100; + background: rgba(248, 250, 252, 0.8); + backdrop-filter: blur(10px); + padding: 0.75rem 0; border-bottom: 1px solid #e2e8f0; margin-bottom: 1.5rem; } - .company-logo { - height: 48px; - width: auto; - object-fit: contain; + .category-wrapper { + display: flex; + overflow-x: auto; + gap: 0.75rem; + padding: 0.25rem 1rem; + scrollbar-width: none; + -ms-overflow-style: none; + -webkit-overflow-scrolling: touch; } + .category-wrapper::-webkit-scrollbar { display: none; } - /* Category Navigation */ - .category-nav-wrapper { - position: sticky; - top: 0; - z-index: 1020; - background: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(8px); - border-bottom: 1px solid #e2e8f0; - padding: 0.75rem 0; + .category-item { + display: inline-flex; + flex-direction: column; + align-items: center; + text-decoration: none; + color: var(--secondary-color); + font-weight: 500; + font-size: 0.85rem; + min-width: 70px; + text-align: center; + transition: all 0.2s; + padding: 0.5rem; + border-radius: 12px; } - .category-nav { - overflow-x: auto; - white-space: nowrap; - -webkit-overflow-scrolling: touch; - padding: 0 1rem; + .category-item.active { + color: var(--primary-color); + background: white; + box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1); } - .category-nav::-webkit-scrollbar { display: none; } - .category-item { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.6rem 1.25rem; - border-radius: 9999px; - background: white; - margin-right: 0.5rem; - font-weight: 600; - font-size: 0.9rem; - cursor: pointer; - border: 1px solid #e2e8f0; - transition: all 0.2s ease; - box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + .category-icon { + width: 44px; + height: 44px; + border-radius: 12px; + background: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.4rem; + font-size: 1.25rem; + transition: all 0.2s; + overflow: hidden; } - .lang-ar .category-item { margin-right: 0; margin-left: 0.5rem; } - .category-item:hover { border-color: var(--primary-color); color: var(--primary-color); } - .category-item.active { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); - box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.2); + .category-icon img { + width: 100%; + height: 100%; + object-fit: cover; } - - /* Product Grid - 5 columns on desktop */ - .product-grid { + .category-item.active .category-icon { + background: #dbeafe; + color: var(--primary-color); + } + + /* Product Grid */ + .products-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; + padding: 0 1rem; } - @media (min-width: 576px) { .product-grid { grid-template-columns: repeat(3, 1fr); } } - @media (min-width: 992px) { .product-grid { grid-template-columns: repeat(4, 1fr); } } - @media (min-width: 1200px) { .product-grid { grid-template-columns: repeat(5, 1fr); } } - - .product-card { + @media (min-width: 768px) { + .products-grid { grid-template-columns: repeat(3, 1fr); } + } + @media (min-width: 992px) { + .products-grid { grid-template-columns: repeat(4, 1fr); } + } + @media (min-width: 1200px) { + .products-grid { grid-template-columns: repeat(5, 1fr); } + } + + .product-card { background: white; - border: 1px solid #f1f5f9; - border-radius: 1rem; - overflow: hidden; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - cursor: pointer; - height: 100%; + border-radius: 1.25rem; + overflow: hidden; + box-shadow: var(--card-shadow); + transition: transform 0.2s; + border: 1px solid #f1f5f9; display: flex; flex-direction: column; + height: 100%; } - .product-card:hover { - transform: translateY(-4px); - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); - border-color: #cbd5e1; - } - .product-card:active { transform: scale(0.97); } + .product-card:active { transform: scale(0.98); } - .product-img-container { + .product-img-wrapper { position: relative; width: 100%; padding-top: 100%; /* 1:1 Aspect Ratio */ - background: #f1f5f9; - overflow: hidden; + background: #f8fafc; } - .product-img { + .product-img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; - object-fit: cover; + object-fit: cover; } - - .product-info { padding: 0.75rem; flex-grow: 1; display: flex; flex-direction: column; } - .product-title { - font-weight: 700; - font-size: 0.95rem; - margin-bottom: 0.25rem; - color: #0f172a; - line-height: 1.2; - } - .product-price { font-weight: 800; color: var(--primary-color); font-size: 1rem; margin-top: auto; } - - .sale-badge { + .no-image-placeholder { position: absolute; - top: 0.5rem; - left: 0.5rem; - background: #ef4444; - color: white; - font-size: 0.7rem; - font-weight: 800; - padding: 0.2rem 0.6rem; - border-radius: 9999px; - z-index: 10; - } - .lang-ar .sale-badge { left: auto; right: 0.5rem; } - - /* Cart Footer */ - .cart-footer { - position: fixed; - bottom: 1.5rem; - left: 50%; - transform: translateX(-50%); - width: 90%; - max-width: 500px; - background: #1e293b; - padding: 0.75rem 1.25rem; - border-radius: 1.25rem; - z-index: 1030; - display: none; - box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.2); - color: white; - } - - /* Footer */ - .site-footer { - padding: 3rem 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; background: #f1f5f9; - border-top: 1px solid #e2e8f0; + color: #94a3b8; + font-size: 2rem; + } + .product-info { + padding: 0.85rem; + display: flex; + flex-direction: column; + flex-grow: 1; + } + .product-name { + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 0.25rem; + line-height: 1.3; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + .product-price { + font-weight: 700; + color: var(--primary-color); + font-size: 1rem; + margin-top: auto; + } + .add-btn { + width: 32px; + height: 32px; + border-radius: 10px; + background: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + border: none; + box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.3); + } + + /* Floating Cart Bar */ + .cart-bar { + position: fixed; + bottom: 1.5rem; + left: 1rem; + right: 1rem; + background: #1e293b; + color: white; + padding: 0.85rem 1.25rem; + border-radius: 1.25rem; + display: flex; + align-items: center; + justify-content: space-between; + z-index: 1000; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + transform: translateY(150%); + } + .cart-bar.visible { transform: translateY(0); } + .cart-count { + background: var(--primary-color); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; + margin-right: 0.5rem; + } + .view-cart-btn { + color: white; + text-decoration: none; + font-weight: 600; + display: flex; + align-items: center; + } + + /* Footer */ + footer { + background: #f1f5f9; + padding: 3rem 1rem; margin-top: 3rem; text-align: center; + border-top: 1px solid #e2e8f0; } - - .lang-toggle { - font-size: 0.85rem; - font-weight: 700; - cursor: pointer; - padding: 0.5rem 1rem; - border-radius: 0.75rem; - border: 1px solid #e2e8f0; - background: white; - transition: all 0.2s; - } - .lang-toggle:hover { background: #f1f5f9; } - - .name-en { display: block; } - .name-ar { display: none; } - .lang-ar .name-en { display: none; } - .lang-ar .name-ar { display: block; } - - .both-names .name-en { display: block; } - .both-names .name-ar { display: block; font-size: 0.8em; opacity: 0.8; margin-top: 2px; } - - .cart-item-img { width: 60px; height: 60px; object-fit: cover; border-radius: 0.75rem; } - .quantity-btn { - width: 36px; - height: 36px; - border-radius: 0.75rem; - border: 1px solid #e2e8f0; - background: white; - display: flex; - align-items: center; + .footer-logo { height: 40px; margin-bottom: 1rem; filter: grayscale(1); opacity: 0.6; } + .footer-links { + display: flex; justify-content: center; - transition: all 0.2s; + gap: 1.5rem; + margin-bottom: 1.5rem; } - .quantity-btn:active { background: #f1f5f9; transform: scale(0.9); } + .footer-links a { color: var(--secondary-color); font-size: 1.25rem; } + + /* Modals */ + .modal-content { border-radius: 1.5rem; border: none; } + .modal-header { border-bottom: 1px solid #f1f5f9; padding: 1.25rem; } + .modal-body { padding: 1.25rem; } + .variant-item { + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 0.75rem; + margin-bottom: 0.5rem; + display: flex; + justify-content: space-between; + cursor: pointer; + } + .variant-item.selected { + border-color: var(--primary-color); + background: #eff6ff; + } + .cart-item { + display: flex; + gap: 1rem; + padding-bottom: 1rem; + margin-bottom: 1rem; + border-bottom: 1px solid #f1f5f9; + } + .qty-control { + display: flex; + align-items: center; + background: #f1f5f9; + border-radius: 10px; + padding: 2px; + } + .qty-btn { + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: white; + border-radius: 8px; + font-weight: 700; + } + .qty-val { width: 30px; text-align: center; font-weight: 600; } - + - -
-
-
- - - -

-

- • - - Table - -

-
-
-
AR
-
+ +
+
+ + + +

+

+
+ Table
+
+
+ +
+ + + + +
+ +
+
+
+ + <?= htmlspecialchars($p['name']) ?> + +
+ +
+
+

+
+ + +
+
+
+
+ +
- -
-
-
- -
- All - الكل -
-
- -
- - - - - -
- - -
-
- -
-
- -
-
- -
-
- -
- - SALE - - - - <?= htmlspecialchars($product['name']) ?> - -
- -
- -
- -
-

- - -

-
- - - - - - -
-
-
-
- -
-
- - -