Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c225ebebc1 | ||
|
|
8a6a18d0c2 | ||
|
|
f28a0493a0 | ||
|
|
41e2f42e41 | ||
|
|
f7f8238fee | ||
|
|
7da45b4e24 | ||
|
|
6c608b6ba5 | ||
|
|
3f48850ff5 | ||
|
|
20dd5c8f61 | ||
|
|
f745cb0f27 | ||
|
|
a1d3188d59 | ||
|
|
a8dff7dfb0 |
1
.gemini/tmp/sales_chart_cache.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"labels":["1404-09"],"data":[2940000]}
|
||||||
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*/node_modules/
|
*/node_modules/
|
||||||
*/build/
|
*/build/
|
||||||
|
|
||||||
|
# Ignore environment files
|
||||||
|
.env
|
||||||
|
|||||||
60
about.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'درباره ما';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container py-5 my-5">
|
||||||
|
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||||
|
<h1>داستان آتیمه</h1>
|
||||||
|
<p class="fs-5 text-muted">تلفیق هنر سنتی و طراحی مدرن</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="about-card p-4 p-lg-5 mb-5" data-aos="fade-up">
|
||||||
|
<div class="row g-0 align-items-center">
|
||||||
|
<div class="col-lg-6" data-aos="fade-right" data-aos-delay="100">
|
||||||
|
<img src="assets/images/pexels/about-us-34942790.jpg" class="img-fluid about-image" alt="هنر چرمدوزی">
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6" data-aos="fade-left" data-aos-delay="200">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h2 class="fw-bold mb-4">باور ما</h2>
|
||||||
|
<p class="lh-lg">ما در آتیمه، به قدرت دستها و اصالت مواد اولیه باور داریم. داستان ما از یک کارگاه کوچک و عشقی عمیق به هنر چرمدوزی آغاز شد. هدف ما خلق آثاری است که نه تنها یک وسیله کاربردی، بلکه بخشی از داستان و استایل روزمره شما باشند؛ آثاری که با گذر زمان، زیباتر و شخصیتر میشوند.</p>
|
||||||
|
<p class="lh-lg mt-3">هر محصول، حاصل ساعتها کار دست هنرمندان ماهر و استفاده از بهترین و باکیفیتترین چرمهای طبیعی است. ما به جزئیات اهمیت میدهیم، از انتخاب نخ گرفته تا طراحی هر برش و دوخت. این تعهد به کیفیت، تضمین میکند که هر ساخته دست ما، اثری ماندگار و بیهمتا باشد.</p>
|
||||||
|
<a href="shop.php" class="btn btn-primary mt-4">مجموعه ما را ببینید</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Team Section or Values -->
|
||||||
|
<section class="py-5">
|
||||||
|
<div class="text-center mb-5" data-aos="fade-down">
|
||||||
|
<h2 class="fw-bold">ارزشهای ما</h2>
|
||||||
|
</div>
|
||||||
|
<ul class="about-us-list">
|
||||||
|
<li class="about-us-item" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<div class="inner">
|
||||||
|
<i class="ri-award-line ri-2x mb-3"></i>
|
||||||
|
<h4 class="fw-bold">تعهد به کیفیت</h4>
|
||||||
|
<p class="text-muted px-3">استفاده از بهترین مواد اولیه و کنترل کیفی دقیق در تمام مراحل تولید.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="about-us-item" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<div class="inner">
|
||||||
|
<i class="ri-hand-heart-line ri-2x mb-3"></i>
|
||||||
|
<h4 class="fw-bold">هنر دست</h4>
|
||||||
|
<p class="text-muted px-3">تمام محصولات ما با عشق و دقت توسط هنرمندان ماهر ساخته میشوند.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="about-us-item" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<div class="inner">
|
||||||
|
<i class="ri-leaf-line ri-2x mb-3"></i>
|
||||||
|
<h4 class="fw-bold">طراحی ماندگار</h4>
|
||||||
|
<p class="text-muted px-3">خلق آثاری مدرن و در عین حال کلاسیک که هیچگاه از مد نمیافتند.</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
1
about_us_image.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"id":34942790,"local_path":"assets\/images\/pexels\/about-us-34942790.jpg","photographer":"Blanca Isela","photographer_url":"https:\/\/www.pexels.com\/@blanca-isela-2156722885","original_url":"https:\/\/images.pexels.com\/photos\/34942790\/pexels-photo-34942790.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"}
|
||||||
90
admin/add_product.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
|
if ($flash_message) {
|
||||||
|
unset($_SESSION['flash_message']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1>افزودن محصول جدید</h1>
|
||||||
|
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">نام محصول</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">توضیحات</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="5" required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||||
|
<input type="number" class="form-control" id="price" name="price" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock" class="form-label">موجودی</label>
|
||||||
|
<input type="number" class="form-control" id="stock" name="stock" required value="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="image" class="form-label">تصویر محصول</label>
|
||||||
|
<input type="file" class="form-control" id="image" name="image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
||||||
|
<input type="text" class="form-control" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
|
||||||
|
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input type="checkbox" id="is_featured" name="is_featured" value="1" style="width: 20px; height: 20px;">
|
||||||
|
<span>این یک محصول ویژه است</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: left; margin-top: 2rem;">
|
||||||
|
<button type="submit" class="btn btn-primary">افزودن محصول</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
<?php if ($flash_message): ?>
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
|
||||||
|
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||||
|
icon: '<?php echo $flash_message["type"]; ?>',
|
||||||
|
confirmButtonText: 'باشه',
|
||||||
|
background: style.getPropertyValue('--admin-surface'),
|
||||||
|
color: style.getPropertyValue('--admin-text')
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
251
admin/api.php
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/auth_handler.php';
|
||||||
|
|
||||||
|
// Start the session to check for admin status
|
||||||
|
if (!is_admin()) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: Close the session immediately after use to prevent locking.
|
||||||
|
// This allows other concurrent requests from the same user to be processed.
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
if ($action === 'get_sales_data') {
|
||||||
|
require_once __DIR__ . '/../includes/jdf.php';
|
||||||
|
|
||||||
|
$cache_file = __DIR__ . '/cache/sales_chart.json';
|
||||||
|
$cache_lifetime = 3600; // 1 hour
|
||||||
|
|
||||||
|
// Clear PHP's stat cache to ensure we get the most up-to-date file status
|
||||||
|
clearstatcache();
|
||||||
|
|
||||||
|
if (file_exists($cache_file) && is_readable($cache_file) && (time() - filemtime($cache_file) < $cache_lifetime)) {
|
||||||
|
$cached_data = file_get_contents($cache_file);
|
||||||
|
// Verify that the cache content is a valid JSON
|
||||||
|
if ($cached_data && json_decode($cached_data) !== null) {
|
||||||
|
header('X-Cache: HIT');
|
||||||
|
echo $cached_data;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CACHE MISS: Regenerate the data
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
YEAR(created_at) as year,
|
||||||
|
MONTH(created_at) as month,
|
||||||
|
SUM(total_amount) as total_sales
|
||||||
|
FROM orders
|
||||||
|
WHERE status = 'Delivered'
|
||||||
|
GROUP BY year, month
|
||||||
|
ORDER BY year ASC, month ASC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$sales_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
$data = [];
|
||||||
|
foreach ($sales_data as $row) {
|
||||||
|
$jalali_date = gregorian_to_jalali($row['year'], $row['month'], 1);
|
||||||
|
$labels[] = $jalali_date[0] . '-' . str_pad($jalali_date[1], 2, '0', STR_PAD_LEFT);
|
||||||
|
$data[] = (float)$row['total_sales'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_data = json_encode(['labels' => $labels, 'data' => $data]);
|
||||||
|
|
||||||
|
// Atomic Write Operation
|
||||||
|
$cache_dir = dirname($cache_file);
|
||||||
|
if (!is_dir($cache_dir)) {
|
||||||
|
mkdir($cache_dir, 0755, true);
|
||||||
|
}
|
||||||
|
$temp_file = $cache_file . '.' . uniqid() . '.tmp';
|
||||||
|
if (file_put_contents($temp_file, $response_data) !== false) {
|
||||||
|
// If rename fails, the old (possibly stale) cache will be used, which is acceptable.
|
||||||
|
// The temp file will be cleaned up on subsequent runs or by a cron job.
|
||||||
|
rename($temp_file, $cache_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('X-Cache: MISS');
|
||||||
|
echo $response_data;
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log("FATAL: DB Exception during sales data generation: " . $e->getMessage());
|
||||||
|
echo json_encode(['error' => 'Database error while fetching sales data.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'get_stats') {
|
||||||
|
try {
|
||||||
|
// Optimized: Fetch all stats in a single query
|
||||||
|
$query = "
|
||||||
|
SELECT
|
||||||
|
(SELECT SUM(total_amount) FROM orders WHERE status = 'Delivered') as total_sales,
|
||||||
|
(SELECT COUNT(*) FROM orders WHERE status = 'Shipped') as shipped_orders,
|
||||||
|
(SELECT COUNT(*) FROM orders WHERE status = 'Cancelled') as cancelled_orders,
|
||||||
|
(SELECT COUNT(*) FROM orders WHERE status = 'Processing') as processing_orders,
|
||||||
|
(SELECT COUNT(*) FROM users) as total_users,
|
||||||
|
(SELECT COUNT(*) FROM page_views) as total_views,
|
||||||
|
(SELECT COUNT(*) FROM page_views WHERE YEAR(view_timestamp) = YEAR(CURDATE()) AND MONTH(view_timestamp) = MONTH(CURDATE())) as this_month_views,
|
||||||
|
(SELECT COUNT(*) FROM page_views WHERE YEAR(view_timestamp) = YEAR(CURDATE() - INTERVAL 1 MONTH) AND MONTH(view_timestamp) = MONTH(CURDATE() - INTERVAL 1 MONTH)) as last_month_views
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $pdo->query($query);
|
||||||
|
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$this_month_views = (int)($stats['this_month_views'] ?? 0);
|
||||||
|
$last_month_views = (int)($stats['last_month_views'] ?? 0);
|
||||||
|
|
||||||
|
$percentage_change = 0;
|
||||||
|
if ($last_month_views > 0) {
|
||||||
|
$percentage_change = (($this_month_views - $last_month_views) / $last_month_views) * 100;
|
||||||
|
} elseif ($this_month_views > 0) {
|
||||||
|
$percentage_change = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'total_sales' => (float)($stats['total_sales'] ?? 0),
|
||||||
|
'shipped_orders' => (int)($stats['shipped_orders'] ?? 0),
|
||||||
|
'cancelled_orders' => (int)($stats['cancelled_orders'] ?? 0),
|
||||||
|
'processing_orders' => (int)($stats['processing_orders'] ?? 0),
|
||||||
|
'total_users' => (int)($stats['total_users'] ?? 0),
|
||||||
|
'total_page_views' => [
|
||||||
|
'count' => (int)($stats['total_views'] ?? 0),
|
||||||
|
'percentage_change' => round($percentage_change, 2)
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log("API Error (get_stats): " . $e->getMessage());
|
||||||
|
echo json_encode(['error' => 'Database error while fetching stats.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'get_reports_data') {
|
||||||
|
try {
|
||||||
|
// 1. General Stats
|
||||||
|
$stats_query = "
|
||||||
|
SELECT
|
||||||
|
(SELECT SUM(total_amount) FROM orders WHERE status = 'Delivered') as total_revenue,
|
||||||
|
(SELECT COUNT(*) FROM orders) as total_orders,
|
||||||
|
(SELECT COUNT(*) FROM users WHERE is_admin = 0) as total_users,
|
||||||
|
(SELECT COUNT(*) FROM products) as total_products
|
||||||
|
";
|
||||||
|
$stats_stmt = $pdo->query($stats_query);
|
||||||
|
$stats = $stats_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 2. Recent Orders
|
||||||
|
$recent_orders_query = "
|
||||||
|
SELECT o.id, o.total_amount, o.status, COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_display_name
|
||||||
|
FROM orders o
|
||||||
|
LEFT JOIN users u ON o.user_id = u.id
|
||||||
|
ORDER BY o.created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
";
|
||||||
|
$recent_orders_stmt = $pdo->query($recent_orders_query);
|
||||||
|
$recent_orders = $recent_orders_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 3. Top Selling Products (Calculated in PHP)
|
||||||
|
$orders_for_products_query = "SELECT items_json FROM orders WHERE status = 'Delivered'";
|
||||||
|
$orders_for_products_stmt = $pdo->query($orders_for_products_query);
|
||||||
|
$all_orders_items = $orders_for_products_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$product_sales = [];
|
||||||
|
foreach ($all_orders_items as $order_items) {
|
||||||
|
$items = json_decode($order_items['items_json'], true);
|
||||||
|
if (is_array($items)) {
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (isset($item['name']) && isset($item['quantity'])) {
|
||||||
|
$product_name = $item['name'];
|
||||||
|
$quantity = (int)$item['quantity'];
|
||||||
|
if (!isset($product_sales[$product_name])) {
|
||||||
|
$product_sales[$product_name] = 0;
|
||||||
|
}
|
||||||
|
$product_sales[$product_name] += $quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($product_sales);
|
||||||
|
$top_products = [];
|
||||||
|
$count = 0;
|
||||||
|
foreach ($product_sales as $name => $total_sold) {
|
||||||
|
$top_products[] = ['name' => $name, 'total_sold' => $total_sold];
|
||||||
|
$count++;
|
||||||
|
if ($count >= 5) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'stats' => [
|
||||||
|
'total_revenue' => (float)($stats['total_revenue'] ?? 0),
|
||||||
|
'total_orders' => (int)($stats['total_orders'] ?? 0),
|
||||||
|
'total_users' => (int)($stats['total_users'] ?? 0),
|
||||||
|
'total_products' => (int)($stats['total_products'] ?? 0),
|
||||||
|
],
|
||||||
|
'recent_orders' => $recent_orders,
|
||||||
|
'top_products' => $top_products
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log("API Error (get_reports_data): " . $e->getMessage());
|
||||||
|
echo json_encode(['error' => 'Database error while fetching report data.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'get_monthly_sales') {
|
||||||
|
require_once __DIR__ . '/../includes/jdf.php';
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
YEAR(created_at) as year,
|
||||||
|
MONTH(created_at) as month,
|
||||||
|
SUM(total_amount) as total_sales
|
||||||
|
FROM orders
|
||||||
|
WHERE status = 'Delivered'
|
||||||
|
GROUP BY year, month
|
||||||
|
ORDER BY year ASC, month ASC
|
||||||
|
");
|
||||||
|
$stmt->execute();
|
||||||
|
$sales_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$labels = [];
|
||||||
|
$values = [];
|
||||||
|
$jalali_months = [
|
||||||
|
1 => 'فروردین', 2 => 'اردیبهشت', 3 => 'خرداد',
|
||||||
|
4 => 'تیر', 5 => 'مرداد', 6 => 'شهریور',
|
||||||
|
7 => 'مهر', 8 => 'آبان', 9 => 'آذر',
|
||||||
|
10 => 'دی', 11 => 'بهمن', 12 => 'اسفند'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($sales_data as $row) {
|
||||||
|
$jalali_date = gregorian_to_jalali($row['year'], $row['month'], 1);
|
||||||
|
$labels[] = $jalali_months[(int)$jalali_date[1]] . ' ' . $jalali_date[0];
|
||||||
|
$values[] = (float)$row['total_sales'];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['labels' => $labels, 'values' => $values]);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
error_log("API Error (get_monthly_sales): " . $e->getMessage());
|
||||||
|
echo json_encode(['error' => 'Database error while fetching monthly sales.']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
331
admin/assets/css/admin_style.css
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
/*
|
||||||
|
* Admin Panel Luxury Redesign
|
||||||
|
* This file centralizes all styles for the admin panel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* --- Variable Imports & Overrides ---
|
||||||
|
We can re-use variables from the main theme.css. Let's define some admin-specific ones.
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
|
--admin-bg: #111111; /* Deep Dark */
|
||||||
|
--admin-surface: #1a1a1a; /* Slightly lighter surface */
|
||||||
|
--admin-card-bg: #242424; /* Card background */
|
||||||
|
--admin-border: #333333;
|
||||||
|
--admin-text: #E0E0E0;
|
||||||
|
--admin-text-muted: #888;
|
||||||
|
--admin-gold: #e5b56e;
|
||||||
|
--admin-blue: #4a90e2;
|
||||||
|
--admin-success: #50e3c2;
|
||||||
|
--admin-danger: #e35050;
|
||||||
|
--admin-warning: #f5a623;
|
||||||
|
--admin-info: #4a90e2;
|
||||||
|
--sidebar-width: 260px;
|
||||||
|
--sidebar-width-collapsed: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.admin-body {
|
||||||
|
background-color: var(--admin-bg);
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-family: 'Vazirmatn', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Main Layout --- */
|
||||||
|
.admin-wrapper {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background-color: var(--admin-surface);
|
||||||
|
border-left: 1px solid var(--admin-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-right: var(--sidebar-width);
|
||||||
|
transition: margin-right 0.3s ease;
|
||||||
|
background-color: var(--admin-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Header */
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
.sidebar-header h2 a {
|
||||||
|
color: var(--admin-gold);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
.sidebar-header h2 span {
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Navigation */
|
||||||
|
.admin-nav {
|
||||||
|
padding: 1rem 0;
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-weight: 500;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-link i {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-link:hover {
|
||||||
|
background-color: var(--admin-bg);
|
||||||
|
color: var(--admin-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-nav-link.active {
|
||||||
|
color: var(--admin-gold);
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: var(--admin-bg);
|
||||||
|
border-right-color: var(--admin-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Footer */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
.sidebar-footer a {
|
||||||
|
display: block;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
.sidebar-footer a:hover {
|
||||||
|
color: var(--admin-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Header Bar --- */
|
||||||
|
.admin-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background-color: var(--admin-surface);
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 999;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-toggle {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--admin-text);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header-title h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Main Content Styling --- */
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
background-color: var(--admin-card-bg);
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
border-bottom: 1px solid var(--admin-border);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat Cards on Dashboard */
|
||||||
|
.stat-cards-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: var(--admin-card-bg);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 1rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.stat-card .icon.bg-primary { background-color: var(--admin-blue); }
|
||||||
|
.stat-card .icon.bg-warning { background-color: var(--admin-warning); }
|
||||||
|
.stat-card .icon.bg-success { background-color: var(--admin-success); }
|
||||||
|
.stat-card .icon.bg-danger { background-color: var(--admin-danger); }
|
||||||
|
|
||||||
|
|
||||||
|
.stat-card .stat-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--admin-text-muted);
|
||||||
|
}
|
||||||
|
.stat-card .stat-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table {
|
||||||
|
border-color: var(--admin-border);
|
||||||
|
}
|
||||||
|
.table th {
|
||||||
|
color: var(--admin-gold);
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-color: var(--admin-border) !important;
|
||||||
|
}
|
||||||
|
.table td {
|
||||||
|
color: var(--admin-text);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.table-hover tbody tr:hover {
|
||||||
|
background-color: var(--admin-surface);
|
||||||
|
color: var(--admin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
.status-processing, .status-badge.bg-info { background-color: var(--admin-info); }
|
||||||
|
.status-shipped, .status-badge.bg-warning { background-color: var(--admin-warning); }
|
||||||
|
.status-completed, .status-badge.bg-success { background-color: var(--admin-success); }
|
||||||
|
.status-cancelled, .status-badge.bg-danger { background-color: var(--admin-danger); }
|
||||||
|
.status-pending, .status-badge.bg-secondary { background-color: var(--admin-text-muted); color: #fff; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-control, .form-select {
|
||||||
|
background-color: var(--admin-surface);
|
||||||
|
border-color: var(--admin-border);
|
||||||
|
color: var(--admin-text);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.form-control:focus, .form-select:focus {
|
||||||
|
background-color: var(--admin-surface);
|
||||||
|
border-color: var(--admin-gold);
|
||||||
|
color: var(--admin-text);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(229, 181, 110, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--admin-gold);
|
||||||
|
border-color: var(--admin-gold);
|
||||||
|
color: #111;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #d4a55a;
|
||||||
|
border-color: #d4a55a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Responsive & Collapsed State --- */
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.admin-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -100%;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 1050; /* Above bootstrap backdrop */
|
||||||
|
transition: right 0.4s ease;
|
||||||
|
}
|
||||||
|
.admin-sidebar.open {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.admin-main-content {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
.sidebar-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 1040;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.sidebar-backdrop.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 993px) {
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar {
|
||||||
|
width: var(--sidebar-width-collapsed);
|
||||||
|
}
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-main-content {
|
||||||
|
margin-right: var(--sidebar-width-collapsed);
|
||||||
|
}
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar .sidebar-header h2 a {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar .sidebar-header h2 span,
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar .admin-nav-link span,
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar .sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.admin-wrapper.sidebar-collapsed .admin-sidebar .admin-nav-link {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
admin/assets/css/dashboard_style.css
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
:root {
|
||||||
|
--dark-bg: #1a1a2e;
|
||||||
|
--dark-surface: #16213e;
|
||||||
|
--dark-primary: #0f3460;
|
||||||
|
--dark-secondary: #e94560;
|
||||||
|
--dark-text-primary: #ffffff;
|
||||||
|
--dark-text-secondary: #c5c5c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-theme {
|
||||||
|
background-color: var(--dark-bg);
|
||||||
|
color: var(--dark-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-v2 {
|
||||||
|
background-color: var(--dark-surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
border: 1px solid var(--dark-primary);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-v2:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-v2 .icon-container {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--dark-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-v2 .stat-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--dark-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-v2 .stat-info h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background-color: var(--dark-surface);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--dark-primary);
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container h5 {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
8
admin/auth_check.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth_handler.php';
|
||||||
|
|
||||||
|
// Check if the user is logged in. If not, redirect to the login page.
|
||||||
|
if (!is_admin()) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
8
admin/auth_handler.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_admin() {
|
||||||
|
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||||
|
}
|
||||||
1
admin/cache/sales_chart.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"labels":[],"data":[]}
|
||||||
119
admin/dashboard.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'داشبورد';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1><?php echo $page_title; ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab-links">
|
||||||
|
<a href="#reports" class="tab-link active">گزارشات</a>
|
||||||
|
<a href="#settings" class="tab-link">تنظیمات</a>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div id="reports" class="tab-pane active">
|
||||||
|
<h3>گزارشات فروش</h3>
|
||||||
|
<div class="stat-cards-grid-reports">
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>مجموع فروش (تکمیل شده)</p>
|
||||||
|
<h3 id="total-sales">...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>مجموع کاربران</p>
|
||||||
|
<h3 id="total-users">...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>سفارشات در حال پردازش</p>
|
||||||
|
<h3 id="processing-orders">...</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-top: 2rem;">
|
||||||
|
<h5>نمودار فروش ماهانه (سفارشات تحویل شده)</h5>
|
||||||
|
<div style="height: 350px;">
|
||||||
|
<canvas id="salesChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="settings" class="tab-pane">
|
||||||
|
<h3>تنظیمات</h3>
|
||||||
|
<p>این بخش برای تنظیمات آینده در نظر گرفته شده است.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Tab functionality
|
||||||
|
const tabLinks = document.querySelectorAll('.tab-link');
|
||||||
|
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||||
|
|
||||||
|
tabLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = this.getAttribute('href');
|
||||||
|
|
||||||
|
tabLinks.forEach(l => l.classList.remove('active'));
|
||||||
|
tabPanes.forEach(p => p.classList.remove('active'));
|
||||||
|
|
||||||
|
this.classList.add('active');
|
||||||
|
document.querySelector(targetId).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch data for stats and chart
|
||||||
|
Promise.all([
|
||||||
|
fetch('api.php?action=get_stats').then(res => res.ok ? res.json() : Promise.reject('Failed to load stats')),
|
||||||
|
fetch('api.php?action=get_sales_data').then(res => res.ok ? res.json() : Promise.reject('Failed to load sales data'))
|
||||||
|
]).then(([statsData, salesData]) => {
|
||||||
|
if (statsData.error) throw new Error(statsData.error);
|
||||||
|
document.getElementById('total-sales').textContent = new Intl.NumberFormat('fa-IR').format(statsData.total_sales) + ' تومان';
|
||||||
|
document.getElementById('total-users').textContent = statsData.total_users;
|
||||||
|
document.getElementById('processing-orders').textContent = statsData.processing_orders;
|
||||||
|
|
||||||
|
if (salesData.error) throw new Error(salesData.error);
|
||||||
|
renderSalesChart(salesData.labels, salesData.data);
|
||||||
|
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Dashboard Error:', error);
|
||||||
|
const reportsTab = document.getElementById('reports');
|
||||||
|
reportsTab.innerHTML = `<div style="color: #F44336; padding: 2rem; text-align: center;">خطا در بارگذاری دادههای داشبورد. لطفاً بعداً تلاش کنید.</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderSalesChart(labels, data) {
|
||||||
|
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||||
|
const primaryColor = getComputedStyle(document.body).getPropertyValue('--admin-primary').trim();
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'میزان فروش',
|
||||||
|
data: data,
|
||||||
|
backgroundColor: `${primaryColor}33`, // 20% opacity
|
||||||
|
borderColor: primaryColor,
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/footer.php';
|
||||||
|
?>
|
||||||
127
admin/edit_product.php
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||||
|
if (!$product_id) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'شناسه محصول نامعتبر است.'];
|
||||||
|
header('Location: products.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
||||||
|
$stmt->execute([$product_id]);
|
||||||
|
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$product) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'محصول مورد نظر یافت نشد.'];
|
||||||
|
header('Location: products.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطا در اتصال به پایگاه داده.'];
|
||||||
|
header('Location: products.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.image-preview-container {
|
||||||
|
background-color: var(--admin-bg);
|
||||||
|
border: 1px dashed var(--admin-border);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.image-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1>ویرایش محصول: <?php echo htmlspecialchars($product['name']); ?></h1>
|
||||||
|
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="handler.php?action=edit" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||||
|
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name" class="form-label">نام محصول</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description" class="form-label">توضیحات</label>
|
||||||
|
<textarea class="form-control" id="description" name="description" rows="5" required><?php echo htmlspecialchars($product['description']); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||||
|
<input type="number" class="form-control" id="price" name="price" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock" class="form-label">موجودی</label>
|
||||||
|
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
||||||
|
<input type="text" class="form-control" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>" placeholder="مثال: #8B4513, #2C2C2C">
|
||||||
|
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input type="checkbox" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?> style="width: 20px; height: 20px;">
|
||||||
|
<span>این یک محصول ویژه است</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">تصویر محصول</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="image-preview-container">
|
||||||
|
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" id="image-preview" class="image-preview">
|
||||||
|
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(event)">
|
||||||
|
<small style="color: var(--admin-text-muted); margin-top: 0.5rem; display: block;">برای تغییر، تصویر جدیدی انتخاب کنید.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: left; margin-top: 2rem;">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> ذخیره تغییرات</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function previewImage(event) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => document.getElementById('image-preview').src = reader.result;
|
||||||
|
if (event.target.files[0]) reader.readAsDataURL(event.target.files[0]);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
35
admin/footer.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
</main> <!-- .admin-main-content's inner main -->
|
||||||
|
</div> <!-- .admin-main-content -->
|
||||||
|
</div> <!-- .admin-wrapper -->
|
||||||
|
|
||||||
|
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const sidebar = document.querySelector('.admin-sidebar');
|
||||||
|
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||||
|
const adminWrapper = document.querySelector('.admin-wrapper');
|
||||||
|
const backdrop = document.getElementById('sidebar-backdrop');
|
||||||
|
|
||||||
|
if (sidebarToggle) {
|
||||||
|
sidebarToggle.addEventListener('click', function() {
|
||||||
|
if (window.innerWidth <= 992) {
|
||||||
|
sidebar.classList.toggle('open');
|
||||||
|
backdrop.classList.toggle('show');
|
||||||
|
} else {
|
||||||
|
adminWrapper.classList.toggle('sidebar-collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.addEventListener('click', function() {
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
this.classList.remove('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
77
admin/handler.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'update_order_status') {
|
||||||
|
$order_id = filter_input(INPUT_POST, 'order_id', FILTER_VALIDATE_INT);
|
||||||
|
$status = filter_input(INPUT_POST, 'status', FILTER_SANITIZE_STRING);
|
||||||
|
|
||||||
|
$allowed_statuses = ['Processing', 'Shipped', 'Delivered', 'Cancelled'];
|
||||||
|
|
||||||
|
if ($order_id && $status && in_array($status, $allowed_statuses)) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$status, $order_id]);
|
||||||
|
|
||||||
|
$_SESSION['success_message'] = "وضعیت سفارش #{$order_id} با موفقیت به '{$status}' تغییر یافت.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Order status update failed: " . $e->getMessage());
|
||||||
|
$_SESSION['error_message'] = "خطایی در بهروزرسانی وضعیت سفارش رخ داد.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$_SESSION['error_message'] = "اطلاعات نامعتبر برای بهروزرسانی وضعیت.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'add_user') {
|
||||||
|
$first_name = filter_input(INPUT_POST, 'first_name', FILTER_SANITIZE_STRING);
|
||||||
|
$last_name = filter_input(INPUT_POST, 'last_name', FILTER_SANITIZE_STRING);
|
||||||
|
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
|
||||||
|
$phone = filter_input(INPUT_POST, 'phone', FILTER_SANITIZE_STRING);
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$is_admin = filter_input(INPUT_POST, 'is_admin', FILTER_VALIDATE_INT) ? 1 : 0;
|
||||||
|
|
||||||
|
if ($first_name && $last_name && $email && !empty($password)) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
if ($stmt->fetch()) {
|
||||||
|
$_SESSION['error_message'] = "کاربری با این ایمیل از قبل وجود دارد.";
|
||||||
|
header('Location: users.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
// Insert user
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO users (first_name, last_name, email, phone, password, is_admin, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())");
|
||||||
|
$stmt->execute([$first_name, $last_name, $email, $phone, $hashed_password, $is_admin]);
|
||||||
|
|
||||||
|
$_SESSION['success_message'] = "کاربر جدید با موفقیت اضافه شد.";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Add user failed: " . $e->getMessage());
|
||||||
|
$_SESSION['error_message'] = "خطایی در افزودن کاربر جدید رخ داد.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$_SESSION['error_message'] = "اطلاعات وارد شده نامعتبر است. لطفاً تمام فیلدهای ستارهدار را پر کنید.";
|
||||||
|
}
|
||||||
|
header('Location: users.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: orders.php');
|
||||||
|
exit;
|
||||||
|
?>
|
||||||
38
admin/header.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo isset($page_title) ? $page_title . ' - ' : ''; ?>پنل مدیریت آتیمه</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<!-- IRANSans Font -->
|
||||||
|
<link rel="stylesheet" href="https://font-ir.s3.ir-thr-at1.arvanstorage.com/IRANSans/css/IRANSans.css">
|
||||||
|
|
||||||
|
<!-- Main Theme CSS -->
|
||||||
|
<link rel="stylesheet" href="../assets/css/theme.css?v=<?php echo time(); ?>">
|
||||||
|
|
||||||
|
<!-- Font Awesome for admin icons -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="assets/css/admin_style.css?v=<?php echo time(); ?>">
|
||||||
|
|
||||||
|
<!-- SweetAlert2 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<!-- Chart.js -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="admin-body">
|
||||||
|
|
||||||
|
<div class="admin-wrapper">
|
||||||
|
<?php require_once 'nav.php'; ?>
|
||||||
|
<div class="admin-main-content">
|
||||||
|
<header class="admin-header-bar">
|
||||||
|
<button id="sidebar-toggle" class="btn">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<div class="admin-header-title">
|
||||||
|
<h1><?php echo isset($page_title) ? $page_title : 'داشبورد'; ?></h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
125
admin/index.php
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
$page_title = 'داشبورد';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
$dashboard_error = null;
|
||||||
|
$total_products = 0;
|
||||||
|
$total_orders = 0;
|
||||||
|
$recent_orders = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$total_products = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
|
||||||
|
$total_orders = $pdo->query("SELECT COUNT(*) FROM orders")->fetchColumn();
|
||||||
|
|
||||||
|
$recent_orders_query = "
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_name,
|
||||||
|
o.total_amount,
|
||||||
|
o.status,
|
||||||
|
o.created_at
|
||||||
|
FROM orders AS o
|
||||||
|
LEFT JOIN users AS u ON o.user_id = u.id
|
||||||
|
ORDER BY o.created_at DESC
|
||||||
|
LIMIT 5
|
||||||
|
";
|
||||||
|
$recent_orders = $pdo->query($recent_orders_query)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$dashboard_error = "<strong>خطا در بارگذاری اطلاعات:</strong> " . $e->getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
|
if ($flash_message) {
|
||||||
|
unset($_SESSION['flash_message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_status_badge_class($status) {
|
||||||
|
switch (strtolower($status)) {
|
||||||
|
case 'processing': return 'status-processing';
|
||||||
|
case 'shipped': return 'status-shipped';
|
||||||
|
case 'delivered': return 'status-delivered';
|
||||||
|
case 'cancelled': return 'status-cancelled';
|
||||||
|
default: return 'status-pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<?php if ($flash_message): ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||||
|
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||||
|
icon: '<?php echo $flash_message["type"]; ?>',
|
||||||
|
confirmButtonText: 'باشه',
|
||||||
|
background: 'var(--admin-surface)',
|
||||||
|
color: 'var(--admin-text)'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($dashboard_error): ?>
|
||||||
|
<div class="card"><div class="card-body" style="color: var(--admin-danger);"><?php echo $dashboard_error; ?></div></div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<div class="stat-cards-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon bg-primary"><i class="fas fa-box"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<p>کل محصولات</p>
|
||||||
|
<h3><?php echo htmlspecialchars($total_products); ?></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="icon bg-warning"><i class="fas fa-receipt"></i></div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<p>کل سفارشات</p>
|
||||||
|
<h3><?php echo htmlspecialchars($total_orders); ?></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">آخرین سفارشات</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شماره سفارش</th>
|
||||||
|
<th>نام مشتری</th>
|
||||||
|
<th>مبلغ کل</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($recent_orders)): ?>
|
||||||
|
<tr><td colspan="5" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($recent_orders as $order): ?>
|
||||||
|
<tr>
|
||||||
|
<td>#<?php echo htmlspecialchars($order['id']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
|
||||||
|
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||||
|
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||||
|
<td><?php echo date('Y-m-d', strtotime($order['created_at'])); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
69
admin/login.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$hardcoded_password = 'admin123';
|
||||||
|
|
||||||
|
if (isset($_POST['password']) && $_POST['password'] === $hardcoded_password) {
|
||||||
|
$_SESSION['is_admin'] = true;
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = 'رمز عبور وارد شده اشتباه است.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$page_title = 'ورود به پنل مدیریت';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= $page_title; ?></title>
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<link rel="stylesheet" href="assets/css/admin_main.css?v=<?= time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body class="admin-theme">
|
||||||
|
|
||||||
|
<div class="admin-login-wrapper">
|
||||||
|
<div class="admin-login-box">
|
||||||
|
<h2>پنل مدیریت آتیمه</h2>
|
||||||
|
<p>برای دسترسی به پنل، لطفاً وارد شوید.</p>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger mb-3"><?= $error; ?></div>
|
||||||
|
<p class="text-center text-muted mb-4">رمز عبور پیشفرض: <code>admin123</code></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="login.php">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="form-label">رمز عبور</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">ورود <i class="ri-arrow-left-line"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.alert-danger {
|
||||||
|
background-color: var(--admin-danger-bg, #fef2f2);
|
||||||
|
border: 1px solid var(--admin-danger-border, #fecaca);
|
||||||
|
color: var(--admin-danger-text, #991b1b);
|
||||||
|
padding: 0.8rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.w-100 { width: 100%; }
|
||||||
|
</style>
|
||||||
22
admin/logout.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Unset all of the session variables.
|
||||||
|
$_SESSION = array();
|
||||||
|
|
||||||
|
// If it's desired to kill the session, also delete the session cookie.
|
||||||
|
// Note: This will destroy the session, and not just the session data!
|
||||||
|
if (ini_get("session.use_cookies")) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
setcookie(session_name(), '', time() - 42000,
|
||||||
|
$params["path"], $params["domain"],
|
||||||
|
$params["secure"], $params["httponly"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, destroy the session.
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
// Redirect to the login page.
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
45
admin/nav.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php $current_page = basename($_SERVER['PHP_SELF']); ?>
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2><a href="index.php">آتیمه<span>.</span></a></h2>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul class="admin-nav">
|
||||||
|
<li class="admin-nav-item">
|
||||||
|
<a class="admin-nav-link <?php echo ($current_page == 'index.php' || $current_page == 'dashboard.php') ? 'active' : ''; ?>" href="index.php">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>داشبورد</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="admin-nav-item">
|
||||||
|
<a class="admin-nav-link <?php echo in_array($current_page, ['products.php', 'add_product.php', 'edit_product.php']) ? 'active' : ''; ?>" href="products.php">
|
||||||
|
<i class="fas fa-box"></i>
|
||||||
|
<span>محصولات</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="admin-nav-item">
|
||||||
|
<a class="admin-nav-link <?php echo ($current_page == 'orders.php') ? 'active' : ''; ?>" href="orders.php">
|
||||||
|
<i class="fas fa-clipboard-list"></i>
|
||||||
|
<span>سفارشات</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="admin-nav-item">
|
||||||
|
<a class="admin-nav-link <?php echo ($current_page == 'reports.php') ? 'active' : ''; ?>" href="reports.php">
|
||||||
|
<i class="fas fa-chart-bar"></i>
|
||||||
|
<span>گزارشات</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="admin-nav-item">
|
||||||
|
<a class="admin-nav-link <?php echo ($current_page == 'users.php') ? 'active' : ''; ?>" href="users.php">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
<span>کاربران</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<a href="../index.php" target="_blank"><i class="fas fa-external-link-alt"></i> مشاهده سایت</a>
|
||||||
|
<hr style="border-color: var(--admin-border-light); margin: 1rem 0;">
|
||||||
|
<a href="logout.php"><i class="fas fa-sign-out-alt"></i> خروج</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
193
admin/orders.php
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$query = "SELECT o.*, COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_display_name FROM orders o LEFT JOIN users u ON o.user_id = u.id ORDER BY o.created_at DESC";
|
||||||
|
$stmt = $pdo->query($query);
|
||||||
|
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$error_message = "خطا در دریافت اطلاعات سفارشات: " . $e->getMessage();
|
||||||
|
$orders = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_status_badge_class($status) {
|
||||||
|
switch (strtolower($status)) {
|
||||||
|
case 'processing': return 'status-processing';
|
||||||
|
case 'shipped': return 'status-shipped';
|
||||||
|
case 'delivered': return 'status-delivered';
|
||||||
|
case 'cancelled': return 'status-cancelled';
|
||||||
|
default: return 'status-pending';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$statuses = ['Processing', 'Shipped', 'Delivered', 'Cancelled'];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Same status badges from index.php */
|
||||||
|
.status-badge { padding: 0.3em 0.6em; border-radius: 6px; font-size: 0.8rem; font-weight: 600; color: #fff; }
|
||||||
|
.status-processing { background-color: var(--admin-info); }
|
||||||
|
.status-shipped { background-color: var(--admin-warning); }
|
||||||
|
.status-delivered { background-color: var(--admin-success); }
|
||||||
|
.status-cancelled { background-color: var(--admin-danger); }
|
||||||
|
.status-pending { background-color: var(--admin-text-muted); }
|
||||||
|
|
||||||
|
/* Custom Modal Styles */
|
||||||
|
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; }
|
||||||
|
.modal-container { background: var(--admin-surface); border: 1px solid var(--admin-border); border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; display: flex; flex-direction: column; }
|
||||||
|
.modal-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--admin-border); display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.modal-body { padding: 1.5rem; overflow-y: auto; }
|
||||||
|
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--admin-border); text-align: left; }
|
||||||
|
.modal-close { background: none; border: none; font-size: 1.5rem; color: var(--admin-text-muted); cursor: pointer; }
|
||||||
|
.modal-overlay.active { display: flex; }
|
||||||
|
.items-list img { width: 50px; height: 50px; object-fit: cover; border-radius: 6px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1>مدیریت سفارشات</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['flash_message'])): ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?php echo $_SESSION["flash_message"]["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||||
|
html: '<?php echo addslashes($_SESSION["flash_message"]["message"]); ?>',
|
||||||
|
icon: '<?php echo $_SESSION["flash_message"]["type"]; ?>',
|
||||||
|
confirmButtonText: 'باشه',
|
||||||
|
background: style.getPropertyValue('--admin-surface'),
|
||||||
|
color: style.getPropertyValue('--admin-text')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php unset($_SESSION['flash_message']); endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>شماره</th><th>نام مشتری</th><th>مبلغ کل</th><th>وضعیت</th><th>تاریخ</th><th style="text-align: left;">عملیات</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<tr><td colspan="6" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<tr>
|
||||||
|
<td>#<?php echo $order['id']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($order['customer_display_name']); ?></td>
|
||||||
|
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||||
|
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||||
|
<td><?php echo date("Y-m-d", strtotime($order['created_at'])); ?></td>
|
||||||
|
<td style="text-align: left;">
|
||||||
|
<button class="btn btn-sm view-order-btn" data-order-id="<?php echo $order['id']; ?>" style="background-color: var(--admin-info); color: white;"><i class="fas fa-eye"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<div id="modal-<?php echo $order['id']; ?>" class="modal-overlay">
|
||||||
|
<div class="modal-container">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5>جزئیات سفارش #<?php echo $order['id']; ?></h5>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="m-0">اطلاعات مشتری</h6>
|
||||||
|
<span class="text-muted small">کد پیگیری: <strong><?php echo htmlspecialchars($order['tracking_id']); ?></strong></span>
|
||||||
|
</div>
|
||||||
|
<p><strong>نام:</strong> <?php echo htmlspecialchars($order['customer_display_name']); ?><br>
|
||||||
|
<strong>آدرس:</strong> <?php echo htmlspecialchars($order['billing_address'] . ", " . $order['billing_city'] . ", " . $order['billing_province']); ?><br>
|
||||||
|
<strong>تلفن:</strong> <?php echo htmlspecialchars($order['billing_phone']); ?></p>
|
||||||
|
<hr style="border-color: var(--admin-border);">
|
||||||
|
<h6>محصولات</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table items-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">محصول</th>
|
||||||
|
<th>رنگ</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th class="text-start">قیمت واحد</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php $items = json_decode($order['items_json'], true); ?>
|
||||||
|
<?php foreach($items as $item): ?>
|
||||||
|
<tr style="vertical-align: middle;">
|
||||||
|
<td style="width: 60px;"><img src="../<?php echo htmlspecialchars($item['image_url']); ?>" style="width: 50px; height: 50px; object-fit: cover; border-radius: 6px;"></td>
|
||||||
|
<td><?php echo htmlspecialchars($item['name']); ?></td>
|
||||||
|
<td style="width: 60px;">
|
||||||
|
<?php if (!empty($item['color'])): ?>
|
||||||
|
<span style="display: inline-block; width: 22px; height: 22px; border-radius: 50%; background-color: <?php echo htmlspecialchars($item['color']); ?>; border: 1px solid var(--admin-border); box-shadow: 0 1px 3px rgba(0,0,0,0.1);" title="<?php echo htmlspecialchars($item['color']); ?>"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td style="width: 80px;"><?php echo $item['quantity']; ?> عدد</td>
|
||||||
|
<td style="width: 120px;" class="text-start"><?php echo number_format($item['price']); ?> تومان</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr style="border-color: var(--admin-border);">
|
||||||
|
<h5 style="text-align: left;">مبلغ نهایی: <?php echo number_format($order['total_amount']); ?> تومان</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<form action="handler.php" method="POST" style="display: flex; width: 100%; justify-content: space-between; align-items: center;">
|
||||||
|
<input type="hidden" name="order_id" value="<?php echo $order['id']; ?>">
|
||||||
|
<input type="hidden" name="action" value="update_order_status">
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 1rem;">
|
||||||
|
<label for="status_<?php echo $order['id']; ?>" class="form-label">تغییر وضعیت:</label>
|
||||||
|
<select class="form-control" name="status" id="status_<?php echo $order['id']; ?>">
|
||||||
|
<?php foreach ($statuses as $status): ?>
|
||||||
|
<option value="<?php echo $status; ?>" <?php echo ($order['status'] === $status) ? 'selected' : ''; ?>><?php echo $status; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">بهروزرسانی</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const viewButtons = document.querySelectorAll('.view-order-btn');
|
||||||
|
viewButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const orderId = this.getAttribute('data-order-id');
|
||||||
|
document.getElementById('modal-' + orderId).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeButtons = document.querySelectorAll('.modal-close');
|
||||||
|
closeButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
this.closest('.modal-overlay').classList.remove('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||||
|
overlay.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
this.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
100
admin/products.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT id, name, price FROM products ORDER BY created_at DESC");
|
||||||
|
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Error fetching products: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
|
if ($flash_message) {
|
||||||
|
unset($_SESSION['flash_message']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1>مدیریت محصولات</h1>
|
||||||
|
<a href="add_product.php" class="btn btn-primary"><i class="fas fa-plus"></i> افزودن محصول</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>نام محصول</th>
|
||||||
|
<th>قیمت</th>
|
||||||
|
<th style="text-align: left;">عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($products)): ?>
|
||||||
|
<tr><td colspan="4" style="text-align: center; padding: 2rem;">هیچ محصولی یافت نشد.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($products as $product): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($product['id']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||||
|
<td><?php echo number_format($product['price']); ?> تومان</td>
|
||||||
|
<td style="text-align: left;">
|
||||||
|
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn" style="background-color: var(--admin-info); color: white;"><i class="fas fa-edit"></i></a>
|
||||||
|
<a href="handler.php?action=delete&id=<?php echo $product['id']; ?>" class="btn btn-danger delete-btn"><i class="fas fa-trash"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
|
|
||||||
|
<?php if ($flash_message): ?>
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
|
||||||
|
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||||
|
icon: '<?php echo $flash_message["type"]; ?>',
|
||||||
|
confirmButtonText: 'باشه',
|
||||||
|
background: style.getPropertyValue('--admin-surface'),
|
||||||
|
color: style.getPropertyValue('--admin-text')
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const href = this.getAttribute('href');
|
||||||
|
Swal.fire({
|
||||||
|
title: 'آیا مطمئن هستید؟',
|
||||||
|
text: "این عمل غیرقابل بازگشت است!",
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: style.getPropertyValue('--admin-danger'),
|
||||||
|
cancelButtonColor: style.getPropertyValue('--admin-info'),
|
||||||
|
confirmButtonText: 'بله، حذف کن!',
|
||||||
|
cancelButtonText: 'انصراف',
|
||||||
|
background: style.getPropertyValue('--admin-surface'),
|
||||||
|
color: style.getPropertyValue('--admin-text')
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
175
admin/reports.php
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'گزارشات';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1><?php echo $page_title; ?></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stat Cards -->
|
||||||
|
<div class="stat-cards-grid-reports" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>مجموع درآمد</p>
|
||||||
|
<h3 id="total-revenue">...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>تعداد سفارشات</p>
|
||||||
|
<h3 id="total-orders">...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>تعداد کاربران</p>
|
||||||
|
<h3 id="total-users">...</h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>تعداد محصولات</p>
|
||||||
|
<h3 id="total-products">...</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sales Chart -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">نمودار فروش ماهانه</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="salesChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="display: flex; gap: 2rem; margin-top: 2rem;">
|
||||||
|
<!-- Recent Orders -->
|
||||||
|
<div class="card" style="flex: 1;">
|
||||||
|
<div class="card-header">آخرین سفارشات</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>شماره سفارش</th>
|
||||||
|
<th>مشتری</th>
|
||||||
|
<th>مبلغ</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="recent-orders-body">
|
||||||
|
<!-- Data will be loaded via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Selling Products -->
|
||||||
|
<div class="card" style="flex: 1;">
|
||||||
|
<div class="card-header">محصولات پرفروش</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>محصول</th>
|
||||||
|
<th>تعداد فروش</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="top-products-body">
|
||||||
|
<!-- Data will be loaded via JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Fetch general reports data
|
||||||
|
fetch('api.php?action=get_reports_data')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('total-revenue').textContent = new Intl.NumberFormat('fa-IR').format(data.stats.total_revenue) + ' تومان';
|
||||||
|
document.getElementById('total-orders').textContent = data.stats.total_orders;
|
||||||
|
document.getElementById('total-users').textContent = data.stats.total_users;
|
||||||
|
document.getElementById('total-products').textContent = data.stats.total_products;
|
||||||
|
|
||||||
|
const recentOrdersBody = document.getElementById('recent-orders-body');
|
||||||
|
if(data.recent_orders.length > 0) {
|
||||||
|
data.recent_orders.forEach(order => {
|
||||||
|
let row = `<tr>
|
||||||
|
<td>#${order.id}</td>
|
||||||
|
<td>${order.customer_display_name}</td>
|
||||||
|
<td>${new Intl.NumberFormat('fa-IR').format(order.total_amount)} تومان</td>
|
||||||
|
<td><span class="status-badge status-${order.status.toLowerCase()}">${order.status}</span></td>
|
||||||
|
</tr>`;
|
||||||
|
recentOrdersBody.innerHTML += row;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recentOrdersBody.innerHTML = '<tr><td colspan="4" class="text-center">سفارشی یافت نشد.</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const topProductsBody = document.getElementById('top-products-body');
|
||||||
|
if(data.top_products.length > 0) {
|
||||||
|
data.top_products.forEach(product => {
|
||||||
|
let row = `<tr>
|
||||||
|
<td>${product.name}</td>
|
||||||
|
<td>${product.total_sold} عدد</td>
|
||||||
|
</tr>`;
|
||||||
|
topProductsBody.innerHTML += row;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
topProductsBody.innerHTML = '<tr><td colspan="2" class="text-center">محصولی یافت نشد.</td></tr>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching reports:', error));
|
||||||
|
|
||||||
|
// Fetch monthly sales data for the chart
|
||||||
|
fetch('api.php?action=get_monthly_sales')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
console.error('Error fetching chart data:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = document.getElementById('salesChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: data.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'فروش ماهانه',
|
||||||
|
data: data.values,
|
||||||
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value, index, values) {
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(value) + ' تومان';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching chart data:', error));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/footer.php';
|
||||||
|
?>
|
||||||
1
admin/test.php
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?php phpinfo(); ?>
|
||||||
136
admin/users.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'مدیریت کاربران';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT id, first_name, last_name, email, phone, created_at FROM users WHERE is_admin = 0 ORDER BY created_at DESC");
|
||||||
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$user_count = count($users);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Error fetching users: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1><?php echo $page_title; ?></h1>
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||||
|
<button id="add-user-btn" class="btn btn-primary">افزودن کاربر جدید</button>
|
||||||
|
<span>تعداد کل کاربران:</span>
|
||||||
|
<span class="badge bg-primary" style="font-size: 1rem; background-color: var(--admin-primary) !important; color: #000 !important; padding: 0.5rem 1rem; border-radius: 8px;"><?php echo $user_count; ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['success_message'])): ?>
|
||||||
|
<div class="alert alert-success" style="background-color: var(--admin-success); color: #fff; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;"><?php echo $_SESSION['success_message']; unset($_SESSION['success_message']); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (isset($_SESSION['error_message'])): ?>
|
||||||
|
<div class="alert alert-danger" style="background-color: var(--admin-danger); color: #fff; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;"><?php echo $_SESSION['error_message']; unset($_SESSION['error_message']); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="add-user-form-container" class="card" style="display: none; margin-bottom: 2rem;">
|
||||||
|
<div class="card-header">فرم افزودن کاربر جدید</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="handler.php" method="POST">
|
||||||
|
<input type="hidden" name="action" value="add_user">
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="first_name" class="form-label">نام</label>
|
||||||
|
<input type="text" id="first_name" name="first_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="last_name" class="form-label">نام خانوادگی</label>
|
||||||
|
<input type="text" id="last_name" name="last_name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="form-label">ایمیل</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone" class="form-label">شماره تلفن</label>
|
||||||
|
<input type="text" id="phone" name="phone" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="form-label">رمز عبور</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="align-self: center;">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_admin" id="is_admin" value="1">
|
||||||
|
<label class="form-check-label" for="is_admin">
|
||||||
|
ادمین باشد؟
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<button type="submit" class="btn btn-primary">ذخیره کاربر</button>
|
||||||
|
<button type="button" id="cancel-add-user" class="btn btn-secondary">انصراف</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const addUserBtn = document.getElementById('add-user-btn');
|
||||||
|
const addUserForm = document.getElementById('add-user-form-container');
|
||||||
|
const cancelBtn = document.getElementById('cancel-add-user');
|
||||||
|
|
||||||
|
if(addUserBtn) {
|
||||||
|
addUserBtn.addEventListener('click', () => {
|
||||||
|
addUserForm.style.display = 'block';
|
||||||
|
addUserBtn.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
addUserForm.style.display = 'none';
|
||||||
|
addUserBtn.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>نام</th>
|
||||||
|
<th>ایمیل</th>
|
||||||
|
<th>شماره تلفن</th>
|
||||||
|
<th>تاریخ عضویت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($users)):
|
||||||
|
?>
|
||||||
|
<tr><td colspan="5" style="text-align: center; padding: 2rem;">هیچ کاربری یافت نشد.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($users as $user):
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($user['id']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars(trim($user['first_name'] . ' ' . $user['last_name'])); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($user['email']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($user['phone'] ?? 'ثبت نشده'); ?></td>
|
||||||
|
<td><?php echo date("Y-m-d", strtotime($user['created_at'])); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/footer.php';
|
||||||
|
?>
|
||||||
143
api/get_order_details.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', '/var/log/apache2/flatlogic_error.log');
|
||||||
|
|
||||||
|
require_once '../db/config.php';
|
||||||
|
require_once '../includes/jdf.php';
|
||||||
|
|
||||||
|
// Function to send JSON error response
|
||||||
|
function send_error($message) {
|
||||||
|
echo json_encode(['error' => $message]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
send_error('Invalid request method.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$input_data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!isset($input_data['tracking_id']) || empty($input_data['tracking_id'])) {
|
||||||
|
send_error('شناسه رهگیری مشخص نشده است.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tracking_id = $input_data['tracking_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// 1. Fetch the order by tracking_id
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
"SELECT id, billing_name, billing_email, billing_address, billing_city, billing_province, billing_postal_code, total_amount, items_json, created_at, status
|
||||||
|
FROM orders
|
||||||
|
WHERE tracking_id = :tracking_id"
|
||||||
|
);
|
||||||
|
$stmt->bindParam(':tracking_id', $tracking_id, PDO::PARAM_STR);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
send_error('سفارشی با این کد رهگیری یافت نشد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Decode items JSON and fetch product details
|
||||||
|
$items_from_db = json_decode($order['items_json'], true);
|
||||||
|
$products_response = [];
|
||||||
|
$product_ids = [];
|
||||||
|
|
||||||
|
if (is_array($items_from_db)) {
|
||||||
|
foreach ($items_from_db as $item) {
|
||||||
|
if (isset($item['product_id'])) {
|
||||||
|
$product_ids[] = $item['product_id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($product_ids)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($product_ids), '?'));
|
||||||
|
// Price is taken from items_json, not the products table, which is correct.
|
||||||
|
// The selected color is also in items_json.
|
||||||
|
$stmt_products = $db->prepare("SELECT id, name, image_url FROM products WHERE id IN ($placeholders)");
|
||||||
|
$stmt_products->execute($product_ids);
|
||||||
|
$products_data = $stmt_products->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$products_by_id = [];
|
||||||
|
foreach ($products_data as $product) {
|
||||||
|
$products_by_id[$product['id']] = $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items_from_db as $item) {
|
||||||
|
$product_id = $item['product_id'];
|
||||||
|
if (isset($products_by_id[$product_id])) {
|
||||||
|
$product = $products_by_id[$product_id];
|
||||||
|
$products_response[] = [
|
||||||
|
'id' => $product['id'],
|
||||||
|
'name' => $product['name'],
|
||||||
|
'price' => number_format($item['price']) . ' تومان',
|
||||||
|
'image_url' => $product['image_url'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'color' => $item['color'] ?? null // Add the selected color from the order
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. Format the response
|
||||||
|
$status_map = [
|
||||||
|
'pending' => 'در انتظار پرداخت',
|
||||||
|
'processing' => 'در حال پردازش',
|
||||||
|
'shipped' => 'ارسال شده',
|
||||||
|
'completed' => 'تکمیل شده',
|
||||||
|
'delivered' => 'تحویل شده', // Add mapping for Delivered
|
||||||
|
'cancelled' => 'لغو شده',
|
||||||
|
'refunded' => 'مسترد شده'
|
||||||
|
];
|
||||||
|
$status_persian = $status_map[strtolower($order['status'])] ?? $order['status'];
|
||||||
|
|
||||||
|
// Robust date formatting to prevent errors
|
||||||
|
try {
|
||||||
|
// Create DateTime object to reliably parse the date from DB
|
||||||
|
$date = new DateTime($order['created_at']);
|
||||||
|
$timestamp = $date->getTimestamp();
|
||||||
|
// Format the timestamp into Jalali date
|
||||||
|
$order_date_jalali = jdate('Y/m/d ساعت H:i', $timestamp);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If parsing fails, log the error and return a safe value
|
||||||
|
error_log("Jalali date conversion failed for order ID {$order['id']}: " . $e->getMessage());
|
||||||
|
$order_date_jalali = 'تاریخ نامعتبر';
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_response = [
|
||||||
|
'id' => $order['id'],
|
||||||
|
'order_date' => $order_date_jalali,
|
||||||
|
'total_amount' => number_format($order['total_amount']) . ' تومان',
|
||||||
|
'discount_amount' => '0 تومان',
|
||||||
|
'status' => $order['status'], // Pass original status to JS for logic
|
||||||
|
'status_persian' => $status_persian, // Pass Persian status for display
|
||||||
|
'shipping_name' => $order['billing_name'],
|
||||||
|
'shipping_address' => trim(implode(', ', array_filter([$order['billing_province'], $order['billing_city'], $order['billing_address']]))),
|
||||||
|
'shipping_postal_code' => $order['billing_postal_code']
|
||||||
|
];
|
||||||
|
|
||||||
|
// Final JSON structure
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'order' => $order_response,
|
||||||
|
'products' => $products_response
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode($response, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("API Error in get_order_details.php: " . $e->getMessage());
|
||||||
|
send_error('خطای سرور: مشکل در ارتباط با پایگاه داده.');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("API Error in get_order_details.php: " . $e->getMessage());
|
||||||
|
send_error('خطای سرور: یک مشکل پیش بینی نشده رخ داد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
78
api/get_pexels_image.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__.'/../includes/pexels.php';
|
||||||
|
|
||||||
|
$query = isset($_GET['query']) ? $_GET['query'] : 'leather craftsmanship';
|
||||||
|
$type = isset($_GET['type']) ? $_GET['type'] : 'photo'; // 'photo' or 'video'
|
||||||
|
$orientation = isset($_GET['orientation']) ? $_GET['orientation'] : 'landscape';
|
||||||
|
|
||||||
|
if ($type === 'video') {
|
||||||
|
$url = 'https://api.pexels.com/videos/search?query=' . urlencode($query) . '&orientation=' . urlencode($orientation) . '&per_page=1&page=1';
|
||||||
|
$data = pexels_get($url);
|
||||||
|
if (!$data || empty($data['videos'])) {
|
||||||
|
echo json_encode(['error'=>'Failed to fetch video from Pexels.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$video = $data['videos'][0];
|
||||||
|
$src = '';
|
||||||
|
// Find the best quality mp4 link
|
||||||
|
foreach($video['video_files'] as $file) {
|
||||||
|
if ($file['file_type'] === 'video/mp4' && (strpos($file['link'], 'external') !== false)) {
|
||||||
|
$src = $file['link'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (empty($src)) {
|
||||||
|
echo json_encode(['error'=>'No suitable video file found.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target_dir = __DIR__ . '/../assets/videos/';
|
||||||
|
$target_filename = $video['id'] . '.mp4';
|
||||||
|
$target_path = $target_dir . $target_filename;
|
||||||
|
|
||||||
|
if (!is_dir($target_dir)) {
|
||||||
|
mkdir($target_dir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download_to($src, $target_path)) {
|
||||||
|
echo json_encode([
|
||||||
|
'id' => $video['id'],
|
||||||
|
'local_path' => 'assets/videos/' . $target_filename,
|
||||||
|
'original_url' => $src
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error'=>'Failed to download and save video.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // It's a photo
|
||||||
|
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($query) . '&orientation=' . urlencode($orientation) . '&per_page=1&page=1';
|
||||||
|
$data = pexels_get($url);
|
||||||
|
|
||||||
|
if (!$data || empty($data['photos'])) {
|
||||||
|
echo json_encode(['error'=>'Failed to fetch image from Pexels.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$photo = $data['photos'][0];
|
||||||
|
$src = $photo['src']['large2x'] ?? ($photo['src']['large'] ?? $photo['src']['original']);
|
||||||
|
$target_dir = __DIR__ . '/../assets/images/pexels/';
|
||||||
|
$target_filename = 'about-us-' . $photo['id'] . '.jpg';
|
||||||
|
$target_path = $target_dir . $target_filename;
|
||||||
|
|
||||||
|
if (!is_dir($target_dir)) {
|
||||||
|
mkdir($target_dir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (download_to($src, $target_path)) {
|
||||||
|
echo json_encode([
|
||||||
|
'id' => $photo['id'],
|
||||||
|
'local_path' => 'assets/images/pexels/' . $target_filename,
|
||||||
|
'photographer' => $photo['photographer'] ?? null,
|
||||||
|
'photographer_url' => $photo['photographer_url'] ?? null,
|
||||||
|
'original_url' => $src
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error'=>'Failed to download and save image.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
579
assets/css/custom.css
Normal file
@ -0,0 +1,579 @@
|
|||||||
|
:root {
|
||||||
|
--status-default: #444;
|
||||||
|
--status-default-dark: #6c757d;
|
||||||
|
--status-processing: #ffc107;
|
||||||
|
--status-shipped: #0d6efd;
|
||||||
|
--status-completed: #198754;
|
||||||
|
--status-cancelled: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-us-list {
|
||||||
|
width: 80vw;
|
||||||
|
display: grid;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
justify-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-us-item {
|
||||||
|
width: 20vw;
|
||||||
|
min-width: 200px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
position: relative;
|
||||||
|
inset: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(-65deg, #0000 40%, #fff7 50%, #0000 70%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation: thing 1.5s ease infinite;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes thing {
|
||||||
|
0% {
|
||||||
|
background-position: 130%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: -166%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.about-us-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.about-us-item {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-modal-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1050;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-modal-container.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
-webkit-backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
color: #f0f0f0;
|
||||||
|
border-radius: 15px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-modal-container.visible .modal-content {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-order-id {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary,
|
||||||
|
.shipping-details,
|
||||||
|
.products-list,
|
||||||
|
.status-details {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item strong {
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tracker {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tracker::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: 0;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
margin: 0 20px;
|
||||||
|
height: 4px;
|
||||||
|
background-color: var(--status-default);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-progress {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: 20px;
|
||||||
|
height: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
transition: width 0.5s ease, background-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
text-align: center;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step .dot {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--status-default);
|
||||||
|
border: 3px solid #2c2c2c;
|
||||||
|
margin: 0 auto;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
transition: background-color 0.5s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step .label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #a0a0a0;
|
||||||
|
transition: color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step.completed .label,
|
||||||
|
.status-step.active .label {
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-step.active .dot {
|
||||||
|
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tracker.is-cancelled .status-step {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tracker.is-cancelled .label {
|
||||||
|
color: var(--status-cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#modal-products-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-item img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-quantity {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #a0a0a0;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-color-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-color-dot {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .row > * {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .row {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center social icons and contact list items on mobile */
|
||||||
|
footer .social-icons,
|
||||||
|
footer .list-unstyled .d-flex {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Modern Login Page --- */
|
||||||
|
.login-page-modern {
|
||||||
|
background-color: var(--color-dark-bg);
|
||||||
|
background-image: url('../images/pexels/about-us-35056828.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-page-modern::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(17, 17, 17, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
padding: 2.5rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header .logo-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header .logo-title {
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header .tagline {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper .form-title {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control {
|
||||||
|
background-color: var(--color-dark-bg);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > .form-control:focus {
|
||||||
|
background-color: var(--color-dark-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-floating > label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-wrapper .btn-primary {
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a:hover {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.login-container {
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) and (max-width: 991px) {
|
||||||
|
.login-container {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.login-form-wrapper {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
.login-header .logo-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Login Toggle Switch --- */
|
||||||
|
.login-toggle-container .btn-group {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--bs-border-radius-lg); /* Match form control radius */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toggle-container .btn-outline-primary {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
border-color: transparent; /* Remove individual button borders */
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toggle-container .btn-outline-primary:hover {
|
||||||
|
background-color: rgba(var(--bs-primary-rgb), 0.1);
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toggle-container .btn-check:checked + .btn-outline-primary {
|
||||||
|
background-color: var(--color-gold);
|
||||||
|
color: var(--color-dark-bg);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--bs-primary-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toggle-container .btn-check:focus + .btn-outline-primary {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Divider --- */
|
||||||
|
.divider-with-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text::before,
|
||||||
|
.divider-with-text::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text:not(:empty)::before {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-with-text:not(:empty)::after {
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Google Sign-in Button --- */
|
||||||
|
.btn-google {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #424242;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-color: #c5c5c5;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google .ri-google-fill {
|
||||||
|
color: #DB4437;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-google span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Hero Video Section --- */
|
||||||
|
.hero-video-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: -1; /* Changed from 1 to -1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-video-background video {
|
||||||
|
min-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-video-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section .container {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
325
assets/css/theme.css
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* Dark & Luxury Theme
|
||||||
|
* Palette: Black, Gray, Custom Blue
|
||||||
|
* Font: Vazirmatn
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Color Palette */
|
||||||
|
--color-dark-bg: #111111; /* پسزمینه اصلی (مشکی) */
|
||||||
|
--color-surface: #1f2326; /* پسزمینه بخشها (خاکستری تیرهتر) */
|
||||||
|
--color-card-bg: #2a2f34; /* پسزمینه کارتها */
|
||||||
|
--color-border: #333333; /* رنگ جداکنندهها و حاشیهها */
|
||||||
|
--color-gold: #e5b56e; /* رنگ شاخص (طلایی سفارشی) */
|
||||||
|
--color-gold-hover: #e9bc7e; /* رنگ هاور طلایی سفارشی */
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text-primary: #F5F5F5; /* متن اصلی (سفید دودی) */
|
||||||
|
--color-text-secondary: #E0E0E0; /* متن ثانویه (خاکستری روشن) */
|
||||||
|
|
||||||
|
/* Bootstrap Overrides */
|
||||||
|
--bs-body-bg: var(--color-dark-bg);
|
||||||
|
--bs-body-color: var(--color-text-primary);
|
||||||
|
--bs-border-color: var(--color-border);
|
||||||
|
--bs-primary: var(--color-gold);
|
||||||
|
--bs-primary-rgb: 229, 181, 110;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--section-padding-lg: 6rem;
|
||||||
|
--section-padding-md: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Base & Typography --- */
|
||||||
|
body {
|
||||||
|
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.8;
|
||||||
|
font-weight: 400;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 700; /* فونت ضخیمتر برای عناوین */
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-gold);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-gold-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Layout & Spacing --- */
|
||||||
|
|
||||||
|
.section-padding {
|
||||||
|
padding-top: var(--section-padding-md);
|
||||||
|
padding-bottom: var(--section-padding-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.section-padding {
|
||||||
|
padding-top: var(--section-padding-lg);
|
||||||
|
padding-bottom: var(--section-padding-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--color-gold);
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For right-aligned titles */
|
||||||
|
.text-md-end .section-title::after,
|
||||||
|
.text-end .section-title::after {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Page Specific --- */
|
||||||
|
|
||||||
|
/* Hero Section */
|
||||||
|
.hero-section .hero-title {
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow: 0 2px 20px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section .hero-subtitle {
|
||||||
|
text-shadow: 0 2px 15px rgba(0,0,0,0.5);
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* About Us Section */
|
||||||
|
.about-us-image {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 15px 40px rgba(0,0,0,0.4);
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
.about-us-image:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- General Components --- */
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--color-card-bg);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 15px; /* کمی گردتر */
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.5);
|
||||||
|
border-color: rgba(var(--bs-primary-rgb), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.card-static:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); /* Keep original shadow */
|
||||||
|
border-color: rgba(255, 255, 255, 0.05); /* Keep original border */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.card-header, .card-footer {
|
||||||
|
background-color: rgba(0,0,0,0.1);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-gold);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
color: #111; /* رنگ متن تیره برای کنتراست روی دکمه طلایی */
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover, .btn-primary:focus {
|
||||||
|
background-color: var(--color-gold-hover);
|
||||||
|
border-color: var(--color-gold-hover);
|
||||||
|
color: #000;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(218, 165, 32, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
border-color: var(--color-gold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Utilities --- */
|
||||||
|
.text-gold {
|
||||||
|
color: var(--color-gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #bbbbbb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-surface {
|
||||||
|
background-color: var(--color-surface) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Header --- */
|
||||||
|
.site-header {
|
||||||
|
background-color: rgba(17, 17, 17, 0.85);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header.header-scrolled {
|
||||||
|
border-color: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .navbar-brand {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .nav-link {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .nav-link:hover, .site-header .nav-link.active {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: rgba(var(--bs-primary-rgb), 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(229, 181, 110, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Product Card --- */
|
||||||
|
.product-card {
|
||||||
|
/* This class is a specific implementation of the .card component. */
|
||||||
|
/* It inherits border, background, shadow, etc. from .card */
|
||||||
|
padding: 0; /* Remove card-body padding if any is added globally */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The hover effect for product-card is slightly different, so we override the transform */
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-8px); /* Keep the slightly larger lift */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-image {
|
||||||
|
aspect-ratio: 3 / 4;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover; /* پوشش کامل کادر بدون تغییر نسبت */
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover .product-image img {
|
||||||
|
transform: scale(1.08); /* افکت زوم روی هاور */
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-info {
|
||||||
|
padding: 1.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-title a {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card .product-price {
|
||||||
|
color: var(--color-gold);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Footer --- */
|
||||||
|
.site-footer {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer h5 {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer p,
|
||||||
|
.site-footer .text-white-50 {
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer a,
|
||||||
|
.site-footer a.text-white-50 {
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer a:hover {
|
||||||
|
color: var(--color-gold) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer .social-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer .social-icon:hover {
|
||||||
|
color: var(--color-gold);
|
||||||
|
}
|
||||||
BIN
assets/images/pexels/about-us-34942790.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/pexels/about-us-35056828.jpg
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
assets/images/products/new_leather_product_1.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/images/products/new_leather_product_2.jpg
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/images/products/new_leather_product_3.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 82 KiB |
115
assets/js/checkout_validation.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const form = document.querySelector('form[action="checkout_handler.php"]');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const requiredFields = form.querySelectorAll('[required]');
|
||||||
|
|
||||||
|
const validateField = (field) => {
|
||||||
|
const errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||||
|
if (!errorContainer) return;
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
if (field.value.trim() === '') {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'این فیلد نمیتواند خالی باشد.';
|
||||||
|
} else if (field.type === 'email' && field.value.trim() !== '' && !/^[\S]+@[\S]+\.[\S]+$/.test(field.value)) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'لطفاً یک ایمیل معتبر وارد کنید.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
field.classList.add('is-invalid');
|
||||||
|
errorContainer.textContent = errorMessage;
|
||||||
|
errorContainer.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
field.classList.remove('is-invalid');
|
||||||
|
errorContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
// Create a container for the error message if it doesn't exist
|
||||||
|
let errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||||
|
if (!errorContainer) {
|
||||||
|
errorContainer = document.createElement('div');
|
||||||
|
errorContainer.className = 'invalid-feedback';
|
||||||
|
// Insert after the input field
|
||||||
|
field.parentNode.insertBefore(errorContainer, field.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
field.addEventListener('blur', () => {
|
||||||
|
validateField(field);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also validate on input to give immediate feedback
|
||||||
|
field.addEventListener('input', () => {
|
||||||
|
// Only remove error, don't show it while typing
|
||||||
|
if (field.classList.contains('is-invalid')) {
|
||||||
|
validateField(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
let isFormValid = true;
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
if (!validateField(field)) {
|
||||||
|
isFormValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFormValid) {
|
||||||
|
event.preventDefault(); // Stop form submission
|
||||||
|
// Find the first invalid field and focus it for better UX
|
||||||
|
const firstInvalidField = form.querySelector('.is-invalid');
|
||||||
|
if(firstInvalidField) {
|
||||||
|
firstInvalidField.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle address selection logic from the original file
|
||||||
|
const savedAddressSelect = document.getElementById('saved_address');
|
||||||
|
if (savedAddressSelect) {
|
||||||
|
savedAddressSelect.addEventListener('change', function() {
|
||||||
|
// Clear all fields first
|
||||||
|
document.getElementById('first_name').value = '';
|
||||||
|
document.getElementById('last_name').value = '';
|
||||||
|
document.getElementById('phone_number').value = '';
|
||||||
|
document.getElementById('province').value = '';
|
||||||
|
document.getElementById('city').value = '';
|
||||||
|
document.getElementById('address_line').value = '';
|
||||||
|
document.getElementById('postal_code').value = '';
|
||||||
|
|
||||||
|
// Clear validation states
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
field.classList.remove('is-invalid');
|
||||||
|
const errorContainer = field.parentElement.querySelector('.invalid-feedback');
|
||||||
|
if(errorContainer) errorContainer.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.value) {
|
||||||
|
try {
|
||||||
|
const address = JSON.parse(this.value);
|
||||||
|
document.getElementById('first_name').value = address.first_name || '';
|
||||||
|
document.getElementById('last_name').value = address.last_name || '';
|
||||||
|
document.getElementById('phone_number').value = address.phone_number || '';
|
||||||
|
document.getElementById('province').value = address.province || '';
|
||||||
|
document.getElementById('city').value = address.city || '';
|
||||||
|
document.getElementById('address_line').value = address.address_line || '';
|
||||||
|
document.getElementById('postal_code').value = address.postal_code || '';
|
||||||
|
|
||||||
|
// Re-validate all fields after filling them
|
||||||
|
requiredFields.forEach(field => validateField(field));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse address JSON:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
30
assets/js/main.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
// Initialize AOS (Animate on Scroll)
|
||||||
|
AOS.init({
|
||||||
|
duration: 800, // Animation duration in ms
|
||||||
|
offset: 100, // Offset (in px) from the original trigger point
|
||||||
|
once: true, // Whether animation should happen only once - while scrolling down
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a class to the header when the page is scrolled
|
||||||
|
const header = document.querySelector('.site-header');
|
||||||
|
if (header) {
|
||||||
|
const scrollThreshold = 50; // Pixels to scroll before adding the class
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > scrollThreshold) {
|
||||||
|
header.classList.add('header-scrolled');
|
||||||
|
} else {
|
||||||
|
header.classList.remove('header-scrolled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for the scroll event
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
// Initial check in case the page is already scrolled on load
|
||||||
|
handleScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
BIN
assets/pasted-20251201-132552-31c63669.jpg
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
assets/pasted-20251201-133153-936f72d3.jpg
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
assets/pasted-20251201-133805-eb28435e.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/pasted-20251201-135328-2aedc448.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
assets/pasted-20251203-190906-4bf15fd7.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
assets/pasted-20251204-015148-05b34c99.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/pasted-20251204-202505-499ec228.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/pasted-20251206-134906-08663223.jpg
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
assets/pasted-20251207-145857-9f50f97d.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
assets/pasted-20251207-191805-ff2e9ada.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/pasted-20251208-162357-8b88f726.jpg
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
assets/pasted-20251208-162516-eecfa280.jpg
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
assets/pasted-20251208-191752-291d87d1.jpg
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/pasted-20251208-202214-728417bd.jpg
Normal file
|
After Width: | Height: | Size: 329 KiB |
BIN
assets/vm-shot-2025-12-04T01-47-45-573Z.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/vm-shot-2025-12-06T13-48-54-374Z.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
232
auth_handler.php
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/session_config.php';
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'mail/MailService.php';
|
||||||
|
|
||||||
|
// Main router for authentication actions
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'send_otp':
|
||||||
|
handle_send_otp();
|
||||||
|
break;
|
||||||
|
case 'verify_otp':
|
||||||
|
handle_verify_otp();
|
||||||
|
break;
|
||||||
|
case 'resend_otp':
|
||||||
|
handle_resend_otp();
|
||||||
|
break;
|
||||||
|
case 'google_callback':
|
||||||
|
handle_google_callback();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'درخواست نامعتبر است.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_resend_otp() {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
if (!isset($_SESSION['otp_identifier'])) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'جلسه شما یافت نشد. لطفا دوباره تلاش کنید.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = $_SESSION['otp_identifier'];
|
||||||
|
$login_method = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? 'email' : 'phone';
|
||||||
|
|
||||||
|
// Generate a new, cryptographically secure 6-digit OTP for resend
|
||||||
|
$otp = random_int(100000, 999999);
|
||||||
|
$expires = date('Y-m-d H:i:s', time() + (10 * 60)); // 10 minutes expiry
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A new OTP is inserted. The verification logic automatically picks the latest valid one.
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO otp_codes (identifier, code, expires_at) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$identifier, $otp, $expires]);
|
||||||
|
|
||||||
|
// FOR TESTING: Always show the OTP for debugging purposes
|
||||||
|
$_SESSION['show_otp_for_debugging'] = $otp;
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'otp' => $otp, 'message' => 'کد جدید با موفقیت ارسال شد.']);
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Throwable $t) {
|
||||||
|
error_log("OTP Resend Error: " . $t->getMessage());
|
||||||
|
echo json_encode(['success' => false, 'message' => 'خطایی در سیستم هنگام ارسال مجدد کد رخ داد.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_send_otp() {
|
||||||
|
$pdo = db();
|
||||||
|
$identifier = '';
|
||||||
|
$login_method = '';
|
||||||
|
|
||||||
|
// Simplified and corrected logic
|
||||||
|
if (isset($_POST['email'])) {
|
||||||
|
// Trim whitespace from the email input
|
||||||
|
$identifier = trim($_POST['email']);
|
||||||
|
if (filter_var($identifier, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$login_method = 'email';
|
||||||
|
} else {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'لطفا یک ایمیل معتبر وارد کنید.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} elseif (isset($_POST['phone'])) {
|
||||||
|
// Trim whitespace from the phone input
|
||||||
|
$identifier = trim($_POST['phone']);
|
||||||
|
if (preg_match('/^09[0-9]{9}$/', $identifier)) {
|
||||||
|
$login_method = 'phone';
|
||||||
|
} else {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'لطفا یک شماره تلفن معتبر (مانند 09123456789) وارد کنید.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Neither email nor phone was submitted
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'ایمیل یا شماره تلفن ارسال نشده است.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Generate a cryptographically secure 6-digit OTP
|
||||||
|
$otp = random_int(100000, 999999);
|
||||||
|
$expires = date('Y-m-d H:i:s', time() + (10 * 60)); // 10 minutes expiry
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO otp_codes (identifier, code, expires_at) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$identifier, $otp, $expires]);
|
||||||
|
|
||||||
|
$_SESSION['otp_identifier'] = $identifier;
|
||||||
|
|
||||||
|
// FOR TESTING: Always show the OTP for debugging purposes for both email and phone
|
||||||
|
$_SESSION['show_otp_for_debugging'] = $otp;
|
||||||
|
|
||||||
|
|
||||||
|
header('Location: verify.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Throwable $t) {
|
||||||
|
error_log("OTP Generation Error: " . $t->getMessage());
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطایی در سیستم رخ داد. لطفا دوباره تلاش کنید.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_verify_otp() {
|
||||||
|
if (empty($_POST['otp_code']) || empty($_SESSION['otp_identifier'])) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'جلسه شما منقضی شده است. لطفا دوباره تلاش کنید.'];
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$identifier = $_SESSION['otp_identifier'];
|
||||||
|
$otp_code = $_POST['otp_code'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM otp_codes WHERE identifier = ? AND code = ? AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1");
|
||||||
|
$stmt->execute([$identifier, $otp_code]);
|
||||||
|
$otp_entry = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($otp_entry) {
|
||||||
|
// OTP is correct, clean up and log the user in
|
||||||
|
$delete_stmt = $pdo->prepare("DELETE FROM otp_codes WHERE identifier = ?");
|
||||||
|
$delete_stmt->execute([$identifier]);
|
||||||
|
unset($_SESSION['otp_identifier']);
|
||||||
|
unset($_SESSION['show_otp_for_debugging']);
|
||||||
|
|
||||||
|
// Determine if login was via email or phone
|
||||||
|
$is_email = filter_var($identifier, FILTER_VALIDATE_EMAIL);
|
||||||
|
$column = $is_email ? 'email' : 'phone';
|
||||||
|
|
||||||
|
$user_stmt = $pdo->prepare("SELECT * FROM users WHERE $column = ?");
|
||||||
|
$user_stmt->execute([$identifier]);
|
||||||
|
$user = $user_stmt->fetch();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
// User exists, log them in
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['user_name'] = trim($user['first_name'] . ' ' . $user['last_name']);
|
||||||
|
$_SESSION['is_admin'] = $user['is_admin'];
|
||||||
|
} else {
|
||||||
|
// User does not exist, create a new one
|
||||||
|
$insert_column = $is_email ? 'email' : 'phone';
|
||||||
|
$insert_stmt = $pdo->prepare("INSERT INTO users ($insert_column, created_at) VALUES (?, NOW())");
|
||||||
|
$insert_stmt->execute([$identifier]);
|
||||||
|
$newUserId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$_SESSION['user_id'] = $newUserId;
|
||||||
|
$_SESSION['user_name'] = $identifier; // Placeholder name
|
||||||
|
$_SESSION['is_admin'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: profile.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Invalid or expired OTP
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'کد تایید نامعتبر یا منقضی شده است.'];
|
||||||
|
header('Location: verify.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable $t) {
|
||||||
|
// Reverted to production error handling
|
||||||
|
error_log("OTP Verification Error: " . $t->getMessage());
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطایی در پایگاه داده رخ داد. لطفا دوباره تلاش کنید.'];
|
||||||
|
header('Location: verify.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_google_callback() {
|
||||||
|
if (!isset($_SESSION['google_user_info'])) {
|
||||||
|
header('Location: login.php?error=google_auth_failed');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$google_user = $_SESSION['google_user_info'];
|
||||||
|
$email = $google_user['email'];
|
||||||
|
$fullName = $google_user['name'];
|
||||||
|
$nameParts = explode(' ', $fullName, 2);
|
||||||
|
$firstName = $nameParts[0];
|
||||||
|
$lastName = isset($nameParts[1]) ? $nameParts[1] : '';
|
||||||
|
|
||||||
|
// Clear the temporary session data
|
||||||
|
unset($_SESSION['google_user_info']);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
|
||||||
|
$stmt->execute([$email]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['user_name'] = trim($user['first_name'] . ' ' . $user['last_name']);
|
||||||
|
$_SESSION['is_admin'] = $user['is_admin'];
|
||||||
|
} else {
|
||||||
|
$insertStmt = $pdo->prepare("INSERT INTO users (first_name, last_name, email, password, is_admin, created_at) VALUES (?, ?, ?, NULL, 0, NOW())");
|
||||||
|
$insertStmt->execute([$firstName, $lastName, $email]);
|
||||||
|
$newUserId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$_SESSION['user_id'] = $newUserId;
|
||||||
|
$_SESSION['user_name'] = $fullName;
|
||||||
|
$_SESSION['is_admin'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: profile.php');
|
||||||
|
exit();
|
||||||
|
|
||||||
|
} catch (Throwable $t) {
|
||||||
|
error_log('Database error during Google auth processing: ' . $t->getMessage());
|
||||||
|
header('Location: login.php?error=db_error');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
111
cart.php
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
$page_title = 'سبد خرید';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
|
$cart_items = $_SESSION['cart'] ?? [];
|
||||||
|
$total_price = 0;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="section-padding">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<?php if (empty($cart_items)): ?>
|
||||||
|
<div class="card card-body text-center p-4 p-md-5" data-aos="fade-up">
|
||||||
|
<div class="d-inline-block mx-auto mb-4">
|
||||||
|
<i class="ri-shopping-cart-2-line display-1 text-gold"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="mb-3">سبد خرید شما خالی است</h2>
|
||||||
|
<p class="text-muted fs-5 mb-4">به نظر میرسد هنوز محصولی به سبد خرید خود اضافه نکردهاید. همین حالا گشتی در فروشگاه بزنید.</p>
|
||||||
|
<div class="d-inline-block">
|
||||||
|
<a href="shop.php" class="btn btn-primary btn-lg">
|
||||||
|
<i class="ri-store-2-line me-2"></i>
|
||||||
|
رفتن به فروشگاه
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="text-center" data-aos="fade-down">
|
||||||
|
<h1 class="section-title">سبد خرید شما</h1>
|
||||||
|
<p class="text-muted fs-5">جزئیات سفارش خود را بررسی و نهایی کنید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-5 mt-5">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<?php foreach ($cart_items as $item_id => $item):
|
||||||
|
$item_total = $item['price'] * $item['quantity'];
|
||||||
|
$total_price += $item_total;
|
||||||
|
?>
|
||||||
|
<div class="card card-body mb-4" data-aos="fade-up">
|
||||||
|
<div class="remove-item-btn">
|
||||||
|
<form action="cart_handler.php" method="POST" class="d-inline">
|
||||||
|
<input type="hidden" name="product_id" value="<?php echo $item['product_id']; ?>">
|
||||||
|
<input type="hidden" name="product_color" value="<?php echo htmlspecialchars($item['color'] ?? ''); ?>">
|
||||||
|
<input type="hidden" name="action" value="remove">
|
||||||
|
<button type="submit" class="btn btn-link text-decoration-none p-0"><i class="ri-close-circle-line"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="row align-items-center g-3">
|
||||||
|
<div class="col-md-2 col-3 cart-item-image">
|
||||||
|
<a href="product.php?id=<?php echo $item['product_id']; ?>">
|
||||||
|
<img src="<?php echo htmlspecialchars($item['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($item['name']); ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-9 cart-item-details">
|
||||||
|
<h5><a href="product.php?id=<?php echo $item['product_id']; ?>"><?php echo htmlspecialchars($item['name']); ?></a></h5>
|
||||||
|
<?php if (!empty($item['color'])) : ?>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<small class="text-muted me-2">رنگ:</small>
|
||||||
|
<span class="cart-item-color-swatch" style="background-color: <?php echo htmlspecialchars($item['color']); ?>;"></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-7">
|
||||||
|
<form action="cart_handler.php" method="POST" class="quantity-selector">
|
||||||
|
<input type="hidden" name="product_id" value="<?php echo $item['product_id']; ?>">
|
||||||
|
<input type="hidden" name="product_color" value="<?php echo htmlspecialchars($item['color'] ?? ''); ?>">
|
||||||
|
<input type="hidden" name="action" value="update">
|
||||||
|
|
||||||
|
<button type="submit" name="quantity" value="<?php echo $item['quantity'] + 1; ?>" class="btn">+</button>
|
||||||
|
<input type="text" value="<?php echo $item['quantity']; ?>" class="quantity-input" readonly>
|
||||||
|
<button type="submit" name="quantity" value="<?php echo $item['quantity'] - 1; ?>" class="btn" <?php echo $item['quantity'] <= 1 ? 'disabled' : ''; ?>>-</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 col-5 text-end">
|
||||||
|
<span class="item-price"><?php echo number_format($item_total); ?> <small>تومان</small></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card card-body position-sticky" style="top: 2rem;" data-aos="fade-left">
|
||||||
|
<h4 class="card-title">خلاصه سفارش</h4>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="label">جمع کل</span>
|
||||||
|
<span class="value"><?php echo number_format($total_price); ?> تومان</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="label">هزینه ارسال</span>
|
||||||
|
<span class="value text-success">رایگان</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-total">
|
||||||
|
<div class="summary-item">
|
||||||
|
<span class="label">مبلغ نهایی</span>
|
||||||
|
<span class="value"><?php echo number_format($total_price); ?> تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<a href="checkout.php" class="btn btn-primary btn-lg btn-checkout"><i class="ri-secure-payment-line me-2"></i>ادامه و پرداخت</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
110
cart_handler.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/session_config.php';
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Initialize cart if it doesn't exist
|
||||||
|
if (!isset($_SESSION['cart'])) {
|
||||||
|
$_SESSION['cart'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get POST data
|
||||||
|
$product_id = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
|
||||||
|
$quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);
|
||||||
|
$color = filter_input(INPUT_POST, 'product_color', FILTER_SANITIZE_STRING);
|
||||||
|
$action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING);
|
||||||
|
|
||||||
|
if (!$action || !$product_id) {
|
||||||
|
header('Location: shop.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique ID for the cart item based on product ID and color
|
||||||
|
$cart_item_id = $product_id . ($color ? '_' . str_replace('#', '', $color) : '');
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'add':
|
||||||
|
if ($quantity > 0) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// Fetch product details including colors
|
||||||
|
$stmt = $pdo->prepare("SELECT name, price, image_url, colors FROM products WHERE id = ?");
|
||||||
|
$stmt->execute([$product_id]);
|
||||||
|
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($product) {
|
||||||
|
// --- START COLOR VALIDATION ---
|
||||||
|
$available_colors = [];
|
||||||
|
if (!empty($product['colors'])) {
|
||||||
|
$colors_raw = explode(',', $product['colors']);
|
||||||
|
foreach ($colors_raw as $c) {
|
||||||
|
$trimmed_c = trim($c);
|
||||||
|
if ($trimmed_c) $available_colors[] = $trimmed_c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($available_colors) > 1 && empty($color)) {
|
||||||
|
// For multi-color products, a color must be selected.
|
||||||
|
$_SESSION['flash_message'] = [
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => 'برای افزودن این محصول، انتخاب یکی از رنگها الزامی است.'
|
||||||
|
];
|
||||||
|
header('Location: product.php?id=' . $product_id);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// --- END COLOR VALIDATION ---
|
||||||
|
|
||||||
|
// If item is already in the cart (same product ID and color), just update the quantity.
|
||||||
|
if (isset($_SESSION['cart'][$cart_item_id])) {
|
||||||
|
$_SESSION['cart'][$cart_item_id]['quantity'] += $quantity;
|
||||||
|
} else {
|
||||||
|
// Otherwise, add the new item to the cart.
|
||||||
|
$_SESSION['cart'][$cart_item_id] = [
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'name' => $product['name'],
|
||||||
|
'price' => $product['price'],
|
||||||
|
'image_url' => $product['image_url'],
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'color' => $color
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$_SESSION['flash_message'] = [
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'محصول با موفقیت به سبد خرید اضافه شد!'
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'محصول یافت نشد.'];
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Cart Add Error: " . $e->getMessage());
|
||||||
|
$_SESSION['flash_message'] = [
|
||||||
|
'type' => 'error',
|
||||||
|
'message' => 'مشکلی در افزودن محصول به سبد خرید رخ داد.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Redirect back to the previous page (likely the product page) to show the flash message.
|
||||||
|
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? 'shop.php'));
|
||||||
|
exit;
|
||||||
|
|
||||||
|
case 'update':
|
||||||
|
if ($quantity > 0) {
|
||||||
|
if (isset($_SESSION['cart'][$cart_item_id])) {
|
||||||
|
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If quantity is 0 or less, remove the item.
|
||||||
|
unset($_SESSION['cart'][$cart_item_id]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'remove':
|
||||||
|
if (isset($_SESSION['cart'][$cart_item_id])) {
|
||||||
|
unset($_SESSION['cart'][$cart_item_id]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For 'update' and 'remove' actions, redirect to the cart page to show changes.
|
||||||
|
header('Location: cart.php');
|
||||||
|
exit;
|
||||||
194
checkout.php
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
|
// 1. Check if cart exists and is not empty. If not, redirect to shop.
|
||||||
|
if (empty($_SESSION['cart'])) {
|
||||||
|
header('Location: shop.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart_items = $_SESSION['cart'];
|
||||||
|
$product_details = [];
|
||||||
|
$total_price = 0;
|
||||||
|
|
||||||
|
// 2. Process cart items directly from the session
|
||||||
|
foreach ($cart_items as $cart_item_id => $item) {
|
||||||
|
// Ensure item has required data
|
||||||
|
if (!isset($item['price'], $item['quantity'], $item['name'], $item['image_url'])) {
|
||||||
|
// Skip malformed items
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_total = $item['price'] * $item['quantity'];
|
||||||
|
$total_price += $item_total;
|
||||||
|
|
||||||
|
// Store details for display
|
||||||
|
$product_details[] = [
|
||||||
|
'id' => $item['product_id'],
|
||||||
|
'name' => $item['name'],
|
||||||
|
'price' => $item['price'],
|
||||||
|
'image_url' => $item['image_url'],
|
||||||
|
'quantity' => $item['quantity'],
|
||||||
|
'color' => $item['color'] ?? '', // Handle case where color might not be set
|
||||||
|
'total' => $item_total
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 3. If after all checks, product_details is empty (e.g. invalid items in cart), redirect.
|
||||||
|
if (empty($product_details)) {
|
||||||
|
// Clear the invalid cart and redirect
|
||||||
|
unset($_SESSION['cart']);
|
||||||
|
header('Location: shop.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fetch user data if logged in
|
||||||
|
$user_id = $_SESSION['user_id'] ?? null;
|
||||||
|
$user = [];
|
||||||
|
$address = [];
|
||||||
|
|
||||||
|
if ($user_id) {
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$user_stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$user_stmt->execute([$user_id]);
|
||||||
|
$user = $user_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
|
||||||
|
$address_stmt = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY id DESC LIMIT 1");
|
||||||
|
$address_stmt->execute([$user_id]);
|
||||||
|
$address = $address_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Checkout user fetch error: " . $e->getMessage());
|
||||||
|
// Do not block the page, guest checkout is still possible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$shipping_cost = 50000;
|
||||||
|
$grand_total = $total_price + $shipping_cost;
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="checkout-page-wrapper">
|
||||||
|
<section class="section-padding">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center" data-aos="fade-down">
|
||||||
|
<h1 class="section-title">تکمیل سفارش و پرداخت</h1>
|
||||||
|
<p class="text-muted fs-5">اطلاعات خود را برای ارسال سفارش وارد کنید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-5 mt-5">
|
||||||
|
<!-- Billing Details Column -->
|
||||||
|
<div class="col-lg-8" data-aos="fade-right">
|
||||||
|
<h3 class="mb-4">جزئیات صورتحساب</h3>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (!empty($_SESSION['checkout_errors'])) {
|
||||||
|
echo '<div class="alert alert-danger"><ul>';
|
||||||
|
foreach ($_SESSION['checkout_errors'] as $error) {
|
||||||
|
echo '<li>' . htmlspecialchars($error) . '</li>';
|
||||||
|
}
|
||||||
|
echo '</ul></div>';
|
||||||
|
// Unset the session variable so it doesn't show again on refresh
|
||||||
|
unset($_SESSION['checkout_errors']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form id="checkout-form" action="checkout_handler.php" method="POST">
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">اطلاعات تماس</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="firstName" class="form-label">نام</label>
|
||||||
|
<input type="text" class="form-control" id="firstName" name="first_name" value="<?= htmlspecialchars($user['first_name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="lastName" class="form-label">نام خانوادگی</label>
|
||||||
|
<input type="text" class="form-control" id="lastName" name="last_name" value="<?= htmlspecialchars($user['last_name'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="email" class="form-label">ایمیل (اختیاری)</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($user['email'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="phone" class="form-label">تلفن همراه</label>
|
||||||
|
<input type="tel" class="form-control" id="phone" name="phone_number" value="<?= htmlspecialchars($address['phone_number'] ?? $user['phone_number'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">آدرس جهت ارسال</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="address" class="form-label">آدرس</label>
|
||||||
|
<input type="text" class="form-control" id="address" name="address_line" placeholder="خیابان اصلی، کوچه فرعی، پلاک ۱۲۳" value="<?= htmlspecialchars($address['address_line'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="city" class="form-label">شهر</label>
|
||||||
|
<input type="text" class="form-control" id="city" name="city" value="<?= htmlspecialchars($address['city'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="state" class="form-label">استان</label>
|
||||||
|
<input type="text" class="form-control" id="state" name="province" value="<?= htmlspecialchars($address['province'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label for="zip" class="form-label">کد پستی</label>
|
||||||
|
<input type="text" class="form-control" id="zip" name="postal_code" value="<?= htmlspecialchars($address['postal_code'] ?? '') ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Summary Column -->
|
||||||
|
<div class="col-lg-4" data-aos="fade-left">
|
||||||
|
<div class="card card-body position-sticky" style="top: 2rem;">
|
||||||
|
<h3 class="card-title">خلاصه سفارش شما</h3>
|
||||||
|
<ul class="summary-item-list">
|
||||||
|
<?php foreach ($product_details as $item) : ?>
|
||||||
|
<li>
|
||||||
|
<span class="product-name"><?= htmlspecialchars($item['name']) ?> <span class="text-muted">(x<?= $item['quantity'] ?>)</span></span>
|
||||||
|
<span class="product-total">T <?= number_format($item['total']) ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="summary-totals">
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">جمع کل</span>
|
||||||
|
<span class="value">T <?= number_format($total_price) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row">
|
||||||
|
<span class="label">هزینه ارسال</span>
|
||||||
|
<span class="value">T <?= number_format($shipping_cost) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="total-row grand-total mt-3">
|
||||||
|
<span class="label">مبلغ قابل پرداخت</span>
|
||||||
|
<span class="value">T <?= number_format($grand_total) ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<button type="submit" form="checkout-form" class="btn btn-primary btn-lg">
|
||||||
|
<i class="ri-secure-payment-line me-2"></i>
|
||||||
|
پرداخت و ثبت نهایی سفارش
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
159
checkout_handler.php
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/session_config.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// 1. Ensure it's a POST request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: checkout.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if cart is empty
|
||||||
|
if (empty($_SESSION['cart'])) {
|
||||||
|
$_SESSION['error_message'] = 'سبد خرید شما خالی است.';
|
||||||
|
header('Location: cart.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Collect and trim form data
|
||||||
|
$first_name = trim($_POST['first_name'] ?? '');
|
||||||
|
$last_name = trim($_POST['last_name'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$phone_number = trim($_POST['phone_number'] ?? '');
|
||||||
|
$address_line = trim($_POST['address_line'] ?? '');
|
||||||
|
$city = trim($_POST['city'] ?? '');
|
||||||
|
$province = trim($_POST['province'] ?? '');
|
||||||
|
$postal_code = trim($_POST['postal_code'] ?? '');
|
||||||
|
|
||||||
|
// 4. Basic Validation
|
||||||
|
$errors = [];
|
||||||
|
if (empty($first_name)) $errors[] = 'فیلد نام الزامی است.';
|
||||||
|
if (empty($last_name)) $errors[] = 'فیلد نام خانوادگی الزامی است.';
|
||||||
|
if (empty($phone_number)) $errors[] = 'فیلد تلفن همراه الزامی است.';
|
||||||
|
if (empty($address_line)) $errors[] = 'فیلد آدرس الزامی است.';
|
||||||
|
if (empty($city)) $errors[] = 'فیلد شهر الزامی است.';
|
||||||
|
if (empty($province)) $errors[] = 'فیلد استان الزامی است.';
|
||||||
|
if (empty($postal_code)) $errors[] = 'فیلد کد پستی الزامی است.';
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$_SESSION['checkout_errors'] = $errors;
|
||||||
|
// Store submitted data to re-populate the form
|
||||||
|
$_SESSION['form_data'] = $_POST;
|
||||||
|
header('Location: checkout.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// == Server-Side Calculation ==
|
||||||
|
$cart = $_SESSION['cart'];
|
||||||
|
$product_ids = array_keys($cart);
|
||||||
|
|
||||||
|
$items_for_json = [];
|
||||||
|
$total_price = 0;
|
||||||
|
|
||||||
|
if (!empty($product_ids)) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($product_ids), '?'));
|
||||||
|
$stmt = db()->prepare("SELECT id, name, price FROM products WHERE id IN ($placeholders)");
|
||||||
|
$stmt->execute($product_ids);
|
||||||
|
$products_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Create a map for quick lookup
|
||||||
|
$products_by_id = [];
|
||||||
|
foreach($products_data as $product) {
|
||||||
|
$products_by_id[$product['id']] = $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($cart as $product_id => $details) {
|
||||||
|
if (isset($products_by_id[$product_id])) {
|
||||||
|
$product = $products_by_id[$product_id];
|
||||||
|
$price = $product['price'];
|
||||||
|
$quantity = $details['quantity'];
|
||||||
|
$total_price += $price * $quantity;
|
||||||
|
|
||||||
|
$items_for_json[] = [
|
||||||
|
'id' => $product_id,
|
||||||
|
'name' => $product['name'],
|
||||||
|
'price' => $price,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'color' => $details['color']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$shipping_cost = 50000;
|
||||||
|
$grand_total = $total_price + $shipping_cost;
|
||||||
|
|
||||||
|
// == Database Operations ==
|
||||||
|
$pdo = db();
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 5. User Handling (Guest or Logged in)
|
||||||
|
$user_id = $_SESSION['user_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$user_id) {
|
||||||
|
// For guests, check if user exists by phone
|
||||||
|
$user_stmt = $pdo->prepare("SELECT id FROM users WHERE phone_number = ?");
|
||||||
|
$user_stmt->execute([$phone_number]);
|
||||||
|
$existing_user = $user_stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($existing_user) {
|
||||||
|
$user_id = $existing_user['id'];
|
||||||
|
} else {
|
||||||
|
// Create a new user
|
||||||
|
$user_insert_stmt = $pdo->prepare("INSERT INTO users (first_name, last_name, email, phone_number, is_admin) VALUES (?, ?, ?, ?, 0)");
|
||||||
|
$user_insert_stmt->execute([$first_name, $last_name, $email, $phone_number]);
|
||||||
|
$user_id = $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
// Log the new/guest user in
|
||||||
|
$_SESSION['user_id'] = $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Generate a unique tracking ID
|
||||||
|
$tracking_id = 'FL-' . strtoupper(bin2hex(random_bytes(5)));
|
||||||
|
|
||||||
|
// 7. Insert the order into the database
|
||||||
|
$order_stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO orders (user_id, billing_name, billing_email, billing_phone, billing_province, billing_city, billing_address, billing_postal_code, total_amount, items_json, status, tracking_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
$full_name = $first_name . ' ' . $last_name;
|
||||||
|
$items_json_encoded = json_encode($items_for_json, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$order_stmt->execute([
|
||||||
|
$user_id,
|
||||||
|
$full_name,
|
||||||
|
$email,
|
||||||
|
$phone_number,
|
||||||
|
$province,
|
||||||
|
$city,
|
||||||
|
$address_line,
|
||||||
|
$postal_code,
|
||||||
|
$grand_total, // Storing the final amount including shipping
|
||||||
|
$items_json_encoded,
|
||||||
|
$tracking_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
// 8. Clear cart and redirect to a success page
|
||||||
|
unset($_SESSION['cart']);
|
||||||
|
unset($_SESSION['form_data']);
|
||||||
|
|
||||||
|
header('Location: track_order.php?tracking_id=' . $tracking_id);
|
||||||
|
exit();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
// Log the detailed error for developers
|
||||||
|
error_log('Checkout Error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
// Set a user-friendly error message and redirect
|
||||||
|
$_SESSION['checkout_errors'] = ['یک خطای غیرمنتظره در هنگام ثبت سفارش رخ داد. لطفاً لحظاتی دیگر دوباره تلاش کنید.'];
|
||||||
|
$_SESSION['form_data'] = $_POST;
|
||||||
|
header('Location: checkout.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
5
composer.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"google/apiclient": "^2.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
1284
composer.lock
generated
Normal file
135
contact.php
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
session_start(); // Ensure session is started
|
||||||
|
$page_title = 'تماس با ما';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
require_once 'mail/MailService.php';
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['contact_form'])) {
|
||||||
|
$name = trim(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING));
|
||||||
|
$email = trim(filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL));
|
||||||
|
$message = trim(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING));
|
||||||
|
|
||||||
|
if (empty($name) || empty($email) || empty($message)) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'warning', 'message' => 'لطفاً تمام فیلدها را پر کنید.'];
|
||||||
|
} elseif (!$email) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'آدرس ایمیل وارد شده معتبر نیست.'];
|
||||||
|
} else {
|
||||||
|
$to_email = getenv('MAIL_TO') ?: 'support@atimeh.com'; // Fallback email
|
||||||
|
$subject = "پیام جدید از فرم تماس وبسایت";
|
||||||
|
$email_result = MailService::sendContactMessage($name, $email, $message, $to_email, $subject);
|
||||||
|
|
||||||
|
if (!empty($email_result['success'])) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!'];
|
||||||
|
} else {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'خطا در ارسال پیام. لطفاً بعداً دوباره تلاش کنید.'];
|
||||||
|
error_log("MailService Error: " . ($email_result['error'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the same page to prevent form resubmission
|
||||||
|
header("Location: contact.php");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for flash message
|
||||||
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
|
if ($flash_message) {
|
||||||
|
// Clear the message from session so it doesn't show again
|
||||||
|
unset($_SESSION['flash_message']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container py-5 my-5">
|
||||||
|
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||||
|
<h1>ارتباط با ما</h1>
|
||||||
|
<p class="fs-5 text-muted">نظرات، پیشنهادات و سوالات شما برای ما ارزشمند است.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="contact-card p-4 p-lg-5" data-aos="fade-up">
|
||||||
|
<div class="row g-5">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="contact-info h-100 d-flex flex-column justify-content-center">
|
||||||
|
<h3 class="mb-4">راههای ارتباطی</h3>
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
|
<i class="ri-map-pin-line mt-1 me-3"></i>
|
||||||
|
<div>
|
||||||
|
<strong>آدرس:</strong>
|
||||||
|
<p class="text-muted mb-0">تهران، خیابان هنر، کوچه خلاقیت، پلاک ۱۲</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
|
<i class="ri-mail-line mt-1 me-3"></i>
|
||||||
|
<div>
|
||||||
|
<strong>ایمیل:</strong>
|
||||||
|
<p class="mb-0"><a href="mailto:info@atimeh.com">info@atimeh.com</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
|
<i class="ri-phone-line mt-1 me-3"></i>
|
||||||
|
<div>
|
||||||
|
<strong>تلفن:</strong>
|
||||||
|
<p class="mb-0"><a href="tel:+982112345678">۰۲۱-۱۲۳۴۵۶۷۸</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="my-4" style="border-color: var(--luxury-border);">
|
||||||
|
<h4 class="h5 mb-3">ما را دنبال کنید</h4>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="#" class="social-btn"><i class="ri-instagram-line"></i></a>
|
||||||
|
<a href="#" class="social-btn"><i class="ri-telegram-line"></i></a>
|
||||||
|
<a href="#" class="social-btn"><i class="ri-whatsapp-line"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<h3 class="mb-4">فرم تماس</h3>
|
||||||
|
<form action="contact.php" method="POST">
|
||||||
|
<input type="hidden" name="contact_form" value="1">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="name" class="form-label">نام شما</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="email" class="form-label">ایمیل</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="message" class="form-label">پیام شما</label>
|
||||||
|
<textarea class="form-control" id="message" name="message" rows="7" required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary">ارسال پیام</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SweetAlert for flash messages -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
<?php if ($flash_message): ?>
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||||
|
icon: '<?php echo $flash_message["type"]; ?>',
|
||||||
|
toast: true,
|
||||||
|
position: 'top-start',
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 5000,
|
||||||
|
timerProgressBar: true,
|
||||||
|
showCloseButton: true,
|
||||||
|
didOpen: (toast) => {
|
||||||
|
toast.addEventListener('mouseenter', Swal.stopTimer);
|
||||||
|
toast.addEventListener('mouseleave', Swal.resumeTimer);
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
popup: 'dark-theme-toast'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
@ -15,3 +15,9 @@ function db() {
|
|||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Google API configuration
|
||||||
|
define('GOOGLE_CLIENT_ID', '915631311746-o6gk076l6lfvuboin99u2h8cgqilc0qk.apps.googleusercontent.com');
|
||||||
|
define('GOOGLE_CLIENT_SECRET', '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');
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||||
|
define('GOOGLE_REDIRECT_URL', $protocol . $_SERVER['HTTP_HOST'] . '/google_callback.php');
|
||||||
|
|||||||
2
db/migrations/001_add_colors_to_products.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add the colors column to the products table if it doesn't exist
|
||||||
|
ALTER TABLE `products` ADD COLUMN IF NOT EXISTS `colors` VARCHAR(255) DEFAULT NULL COMMENT 'Comma-separated list of available colors';
|
||||||
2
db/migrations/002_add_is_featured_to_products.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add the is_featured column to the products table if it doesn't exist
|
||||||
|
ALTER TABLE `products` ADD COLUMN IF NOT EXISTS `is_featured` BOOLEAN DEFAULT 0;
|
||||||
13
db/migrations/003_create_orders_table.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `orders` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`customer_name` VARCHAR(255) NOT NULL,
|
||||||
|
`customer_email` VARCHAR(255) NOT NULL,
|
||||||
|
`customer_address` TEXT NOT NULL,
|
||||||
|
`customer_phone` VARCHAR(50) DEFAULT NULL,
|
||||||
|
`total_amount` DECIMAL(10, 2) NOT NULL,
|
||||||
|
`items_json` JSON NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Add the status column to the orders table if it doesn't exist
|
||||||
|
ALTER TABLE `orders` ADD COLUMN IF NOT EXISTS `status` VARCHAR(50) NOT NULL DEFAULT 'Pending' COMMENT 'e.g., Pending, Processing, Shipped, Delivered, Canceled';
|
||||||
11
db/migrations/004_create_users_table.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- Create users table to store customer information
|
||||||
|
CREATE TABLE IF NOT EXISTS `users` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`first_name` VARCHAR(100) NOT NULL,
|
||||||
|
`last_name` VARCHAR(100) NOT NULL,
|
||||||
|
`email` VARCHAR(150) NOT NULL UNIQUE,
|
||||||
|
`phone_number` VARCHAR(20) DEFAULT NULL UNIQUE,
|
||||||
|
`password` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
12
db/migrations/005_create_user_addresses_table.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- Create user_addresses table to store customer shipping addresses
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_addresses` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`user_id` INT NOT NULL,
|
||||||
|
`province` VARCHAR(100) NOT NULL,
|
||||||
|
`city` VARCHAR(100) NOT NULL,
|
||||||
|
`address_line` TEXT NOT NULL,
|
||||||
|
`postal_code` VARCHAR(20) NOT NULL,
|
||||||
|
`is_default` BOOLEAN DEFAULT FALSE,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
2
db/migrations/006_add_is_admin_to_users.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add is_admin flag to users table to differentiate admins from regular users
|
||||||
|
ALTER TABLE `users` ADD `is_admin` BOOLEAN NOT NULL DEFAULT FALSE AFTER `password`;
|
||||||
18
db/migrations/007_update_orders_table.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-- Update orders table to support structured addresses and link to users
|
||||||
|
|
||||||
|
-- Add user_id to link orders to the users table (can be NULL for guest checkouts)
|
||||||
|
ALTER TABLE `orders` ADD COLUMN `user_id` INT NULL DEFAULT NULL AFTER `id`, ADD CONSTRAINT `fk_orders_users` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Add structured shipping address fields
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
ADD COLUMN `shipping_province` VARCHAR(100) NOT NULL AFTER `customer_phone`,
|
||||||
|
ADD COLUMN `shipping_city` VARCHAR(100) NOT NULL AFTER `shipping_province`,
|
||||||
|
ADD COLUMN `shipping_address_line` TEXT NOT NULL AFTER `shipping_city`,
|
||||||
|
ADD COLUMN `shipping_postal_code` VARCHAR(20) NOT NULL AFTER `shipping_address_line`;
|
||||||
|
|
||||||
|
-- Rename old columns to avoid confusion, but keep them for any old data
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
CHANGE `customer_name` `billing_name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
CHANGE `customer_email` `billing_email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
CHANGE `customer_address` `legacy_customer_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||||
|
|
||||||
9
db/migrations/008_create_otp_table.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `otp_codes` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`email` VARCHAR(255) NOT NULL,
|
||||||
|
`code_hash` VARCHAR(255) NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`is_used` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
INDEX `email_index` (`email`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
1
db/migrations/009_add_tracking_id_to_orders.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `orders` ADD `tracking_id` VARCHAR(255) UNIQUE NULL DEFAULT NULL AFTER `status`;
|
||||||
1
db/migrations/010_rename_phone_column_in_orders.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `orders` CHANGE `customer_phone` `billing_phone` VARCHAR(50) DEFAULT NULL;
|
||||||
6
db/migrations/011_consolidate_address_columns.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-- Rename shipping_* columns to billing_* to match the application logic
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
CHANGE `shipping_province` `billing_province` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
CHANGE `shipping_city` `billing_city` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
CHANGE `shipping_address_line` `billing_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
CHANGE `shipping_postal_code` `billing_postal_code` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
|
||||||
16
db/migrations/012_fix_encoding_for_orders_table.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-- Convert the orders table and its text-based columns to utf8mb4 to support Persian characters
|
||||||
|
ALTER TABLE `orders` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Additionally, ensure individual text columns are correctly set
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
MODIFY `billing_name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `legacy_customer_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_phone` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_province` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_city` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_address` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `billing_postal_code` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `items_json` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `status` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||||
|
MODIFY `tracking_id` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
6
db/migrations/013_create_page_views_table.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `page_views` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`view_timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`page_url` VARCHAR(2048) NOT NULL,
|
||||||
|
`ip_address` VARCHAR(45) NOT NULL
|
||||||
|
);
|
||||||
7
db/migrations/014_add_indexes_to_orders.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
-- Add indexes to the orders table to improve query performance for the dashboard
|
||||||
|
|
||||||
|
-- Index for the status column, as it's frequently used in WHERE clauses
|
||||||
|
ALTER TABLE `orders` ADD INDEX `idx_status` (`status`);
|
||||||
|
|
||||||
|
-- Index for the order date column, as it's used for grouping and filtering by date
|
||||||
|
ALTER TABLE `orders` ADD INDEX `idx_created_at` (`created_at`);
|
||||||
1
db/migrations/015_allow_null_password_in_users.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` CHANGE `password` `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;
|
||||||
1
db/migrations/016_add_phone_to_users.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(255) NULL AFTER `email`;
|
||||||
3
db/migrations/017_fix_otp_table.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `otp_codes`
|
||||||
|
CHANGE COLUMN `email` `identifier` VARCHAR(255) NOT NULL,
|
||||||
|
CHANGE COLUMN `code_hash` `code` VARCHAR(255) NOT NULL;
|
||||||
3
db/migrations/018_allow_null_names_in_users.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY COLUMN `first_name` VARCHAR(100) NULL,
|
||||||
|
MODIFY COLUMN `last_name` VARCHAR(100) NULL;
|
||||||
1
db/migrations/019_allow_null_email_in_users.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` CHANGE `email` `email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;
|
||||||
34
debug.log
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
Google callback received: Array
|
||||||
|
(
|
||||||
|
[code] => 4/0ATX87lPjrFIOuwWliENWcywUFg2GEgcus6DYYxEZCL3ISMUJlz2hqnXSPJ1U_xb5nX_WAg
|
||||||
|
[scope] => email profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid
|
||||||
|
[authuser] => 0
|
||||||
|
[prompt] => none
|
||||||
|
)
|
||||||
|
|
||||||
|
Google token response: Array
|
||||||
|
(
|
||||||
|
[access_token] => ya29.A0Aa7pCA82ZzUaONLUeRj6TaiCF4mN_rnoysKXYJx0sBfNIlsRFNhsWQCH9KRqJqs82imX0t3UqTAPol9kD6c-XJKKI2ulxWmO7vabFCvWoaF2LR6fMNTH4iaruLxAws6xvgyObdGfkQgGHBDu2JBrMvEi0bLjqAMf5qOZA1mmRuR2CJzDnHTZoCqSaf7VeweMSAD8FkMx3Kn1t9CWs8CJce-OUBrQghfntFzqbvhbgf4rQynhpjg2iLtrvXmP_PPMIb_WJDTuvB9jrDBXi46McpOPPyheygaCgYKAUISARISFQHGX2MioKN_UM1Usr69JF1Ts3UnCQ0293
|
||||||
|
[expires_in] => 3598
|
||||||
|
[scope] => https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile
|
||||||
|
[token_type] => Bearer
|
||||||
|
[id_token] => eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ1NDNlMjFhMDI3M2VmYzY2YTQ3NTAwMDI0NDFjYjIxNTFjYjIzNWYiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI5MTU2MzEzMTE3NDYtbzZnazA3Nmw2bGZ2dWJvaW45OXUyaDhjZ3FpbGMwcWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI5MTU2MzEzMTE3NDYtbzZnazA3Nmw2bGZ2dWJvaW45OXUyaDhjZ3FpbGMwcWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTExNjMwMjQwMTY0NTUxMTM0OTYiLCJlbWFpbCI6ImR1c3QuYWk4OUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6InUyZXNtVzcxdGo2RGRETTBaOEN6SkEiLCJuYW1lIjoiZHVzdCBhaSIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NLaVRaZmJYVThOc3NzUW1Sb1V4dllKWkVTVldfVmpRU3FmRnhoNjZnazRPVFNrY2c9czk2LWMiLCJnaXZlbl9uYW1lIjoiZHVzdCIsImZhbWlseV9uYW1lIjoiYWkiLCJpYXQiOjE3NjUyMjQxMTQsImV4cCI6MTc2NTIyNzcxNH0.UvgktDzIgMhJLKvQfSGs9GTfodjShyXNRvnPs60vtnyGhdb0d6E8nD_l4kF5HXlcJAMpb4T7QVNCKvXdeG8gI68-_n-FIUfIqkePh167Qh553gHw-8K7v8vmmvDpVvWg4gPXBqARsgZc6_53qAEd6b2aUGGiRDicCwBkS6tDk4We14bIO71g7d70WEnmBLIE5YA7FIj9PYMfWMs0r9oN8fgG1Qt29LO3L4AQ7P8QzqZ3bNL4OiZC_kl0wsVK6TBDuoXFxUMPsUhkvUNr4A67mJa900wxjW9TrzNG8ZJBiwybgdKIY71r_xtEpPemTHuhYsmvaOlzhJ4RkngneNCY8Q
|
||||||
|
[created] => 1765224114
|
||||||
|
)
|
||||||
|
|
||||||
|
Google callback received: Array
|
||||||
|
(
|
||||||
|
[code] => 4/0ATX87lPjrFIOuwWliENWcywUFg2GEgcus6DYYxEZCL3ISMUJlz2hqnXSPJ1U_xb5nX_WAg
|
||||||
|
[scope] => email profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid
|
||||||
|
[authuser] => 0
|
||||||
|
[prompt] => none
|
||||||
|
)
|
||||||
|
|
||||||
|
Google token response: Array
|
||||||
|
(
|
||||||
|
[error] => invalid_grant
|
||||||
|
[error_description] => Bad Request
|
||||||
|
)
|
||||||
|
|
||||||
|
Google Auth Exception: Token error: Bad Request
|
||||||
|
OTP Send Mail Error: PHPMailer error: SMTP Error: data not accepted.
|
||||||
1
execution_test.log
Normal file
@ -0,0 +1 @@
|
|||||||
|
google_callback.php was executed at 2025-12-08 19:56:56
|
||||||
86
faq.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'سوالات متداول';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="container py-5 my-5">
|
||||||
|
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||||
|
<h1>سوالات متداول</h1>
|
||||||
|
<p class="fs-5 text-muted">پاسخ به برخی از سوالات شما</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10 col-xl-8">
|
||||||
|
<div class="accordion" id="faqAccordion">
|
||||||
|
|
||||||
|
<div class="accordion-item" data-aos="fade-up">
|
||||||
|
<h2 class="accordion-header" id="headingOne">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
|
||||||
|
چگونه میتوانم سفارش خود را ثبت کنم؟
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p>شما میتوانید با مراجعه به بخش فروشگاه، محصولات مورد نظر خود را به سبد خرید اضافه کرده و سپس با تکمیل اطلاعات و پرداخت، سفارش خود را نهایی کنید.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<h2 class="accordion-header" id="headingTwo">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
||||||
|
زمان ارسال سفارش چقدر است؟
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p>سفارشها در تهران طی ۲ تا ۳ روز کاری و در سایر شهرها طی ۴ تا ۷ روز کاری از طریق پست پیشتاز ارسال میشوند. کد رهگیری پستی پس از ارسال، برای شما پیامک خواهد شد.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<h2 class="accordion-header" id="headingThree">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
|
||||||
|
آیا امکان بازگشت کالا وجود دارد؟
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p>بله، در صورت عدم رضایت از محصول یا وجود هرگونه مغایرت، تا ۷ روز پس از دریافت کالا فرصت دارید تا آن را بازگردانید. لطفاً توجه داشته باشید که محصول نباید استفاده شده باشد و بستهبندی آن آسیب ندیده باشد. برای هماهنگی با پشتیبانی تماس بگیرید.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<h2 class="accordion-header" id="headingFour">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
|
||||||
|
چگونه میتوانم سفارشم را پیگیری کنم؟
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p>پس از ارسال سفارش، یک کد رهگیری ۲۴ رقمی از طریق پیامک برای شما ارسال میشود. شما میتوانید با مراجعه به وبسایت رسمی پست و وارد کردن این کد، از آخرین وضعیت بسته خود مطلع شوید. همچنین میتوانید از طریق صفحه "پیگیری سفارش" در سایت ما نیز اقدام کنید.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-item" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<h2 class="accordion-header" id="headingFive">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFive" aria-expanded="false" aria-controls="collapseFive">
|
||||||
|
آیا محصولات شما دارای ضمانت هستند؟
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseFive" class="accordion-collapse collapse" aria-labelledby="headingFive" data-bs-parent="#faqAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p>بله، تمامی محصولات چرم ما دارای ۶ ماه ضمانت کیفیت دوخت و یراقآلات هستند. این ضمانت شامل آسیبهای ناشی از استفاده نادرست نمیشود.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
65
google_callback.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
// MUST be called before session_start()
|
||||||
|
require_once 'includes/session_config.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
|
||||||
|
// Google API configuration
|
||||||
|
define('GOOGLE_CLIENT_ID', '915631311746-u10nasn59smdjn3ofle2a186vobmgll7.apps.googleusercontent.com');
|
||||||
|
define('GOOGLE_CLIENT_SECRET', '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');
|
||||||
|
define('GOOGLE_REDIRECT_URL', 'https://atimah-leather.dev.flatlogic.app/google_callback.php');
|
||||||
|
|
||||||
|
// Check if the user has a temporary identifier from the initial login, and clear it.
|
||||||
|
if (isset($_SESSION['otp_identifier'])) {
|
||||||
|
unset($_SESSION['otp_identifier']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = new Google_Client();
|
||||||
|
$client->setClientId(GOOGLE_CLIENT_ID);
|
||||||
|
$client->setClientSecret(GOOGLE_CLIENT_SECRET);
|
||||||
|
$client->setRedirectUri(GOOGLE_REDIRECT_URL);
|
||||||
|
$client->addScope("email");
|
||||||
|
$client->addScope("profile");
|
||||||
|
|
||||||
|
// Handle the OAuth 2.0 server response
|
||||||
|
if (isset($_GET['code'])) {
|
||||||
|
try {
|
||||||
|
error_log('Google callback received: ' . print_r($_GET, true));
|
||||||
|
$token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
|
||||||
|
error_log('Google token response: ' . print_r($token, true));
|
||||||
|
|
||||||
|
if (isset($token['error'])) {
|
||||||
|
throw new Exception('Token error: ' . ($token['error_description'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$client->setAccessToken($token['access_token']);
|
||||||
|
|
||||||
|
$google_oauth = new Google_Service_Oauth2($client);
|
||||||
|
$google_account_info = $google_oauth->userinfo->get();
|
||||||
|
|
||||||
|
$userInfo = [
|
||||||
|
'email' => $google_account_info->email,
|
||||||
|
'name' => $google_account_info->name,
|
||||||
|
];
|
||||||
|
|
||||||
|
$_SESSION['google_user_info'] = $userInfo;
|
||||||
|
|
||||||
|
// Explicitly save the session data before redirecting.
|
||||||
|
session_write_close();
|
||||||
|
|
||||||
|
header('Location: auth_handler.php?action=google_callback');
|
||||||
|
exit();
|
||||||
|
|
||||||
|
} catch (Throwable $t) {
|
||||||
|
// Log the actual error to the server's error log for inspection.
|
||||||
|
error_log('Google Auth Exception: ' . $t->getMessage());
|
||||||
|
|
||||||
|
header('Location: login.php?error=google_auth_failed_exception');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's the initial request, redirect to Google's OAuth 2.0 server
|
||||||
|
header('Location: ' . $client->createAuthUrl());
|
||||||
|
exit();
|
||||||
|
}
|
||||||
1
hero_video.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"id":34942790,"local_path":"assets\/images\/pexels\/about-us-34942790.jpg","photographer":"Blanca Isela","photographer_url":"https:\/\/www.pexels.com\/@blanca-isela-2156722885","original_url":"https:\/\/images.pexels.com\/photos\/34942790\/pexels-photo-34942790.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"}
|
||||||
58
includes/footer.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
</main> <!-- close main container -->
|
||||||
|
|
||||||
|
<footer class="site-footer mt-5 py-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
|
||||||
|
<h5 class="fw-bold mb-3">آتیمه</h5>
|
||||||
|
<p class="text-white-50">تجربه اصالت و کیفیت در محصولات چرمی دستدوز. ما به هنر و ماندگاری اعتقاد داریم.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-6 mb-4 mb-lg-0">
|
||||||
|
<h5 class="fw-bold mb-3">دسترسی سریع</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="shop.php" class="text-white-50">فروشگاه</a></li>
|
||||||
|
<li><a href="track_order.php" class="text-white-50">پیگیری سفارش</a></li>
|
||||||
|
<li><a href="about.php" class="text-white-50">درباره ما</a></li>
|
||||||
|
<li><a href="terms.php" class="text-white-50">قوانین و مقررات</a></li>
|
||||||
|
<li><a href="faq.php" class="text-white-50">سوالات متداول</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
|
||||||
|
<h5 class="fw-bold mb-3">تماس با ما</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bi bi-geo-alt-fill me-2 text-primary"></i>
|
||||||
|
<span class="text-white-50">تهران، خیابان هنر، پلاک ۱۲</span>
|
||||||
|
</li>
|
||||||
|
<li class="d-flex align-items-center mb-2">
|
||||||
|
<i class="bi bi-telephone-fill me-2 text-primary"></i>
|
||||||
|
<a href="tel:+982122334455" class="text-white-50">۰۲۱-۲۲۳۳۴۴۵۵</a>
|
||||||
|
</li>
|
||||||
|
<li class="d-flex align-items-center">
|
||||||
|
<i class="bi bi-envelope-fill me-2 text-primary"></i>
|
||||||
|
<a href="mailto:info@atimeh.com" class="text-white-50">info@atimeh.com</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<h5 class="fw-bold mb-3">ما را دنبال کنید</h5>
|
||||||
|
<p class="text-white-50">از جدیدترین محصولات و تخفیفها باخبر شوید.</p>
|
||||||
|
<div class="d-flex mt-3 social-icons">
|
||||||
|
<a href="#" class="social-icon me-3"><i class="bi bi-instagram"></i></a>
|
||||||
|
<a href="#" class="social-icon me-3"><i class="bi bi-telegram"></i></a>
|
||||||
|
<a href="#" class="social-icon"><i class="bi bi-whatsapp"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center text-white-50 pt-4 mt-4 border-top border-secondary">
|
||||||
|
<p>© <?php echo date("Y"); ?> تمام حقوق برای چرم آتیمه محفوظ است.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||||
|
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
135
includes/header.php
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
// Enforce session cookie settings BEFORE starting the session
|
||||||
|
require_once __DIR__ . '/session_config.php';
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log page views for non-admin pages
|
||||||
|
if (strpos($_SERVER['REQUEST_URI'], '/admin/') === false) {
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
// Check if the table exists to avoid errors before migration
|
||||||
|
$table_check = $pdo->query("SHOW TABLES LIKE 'page_views'");
|
||||||
|
if ($table_check->rowCount() > 0) {
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$page_url = $_SERVER['REQUEST_URI'];
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO page_views (page_url, ip_address) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$page_url, $ip_address]);
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Silently fail or log to a file to not break the page for users
|
||||||
|
error_log("Could not log page view: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart_item_count = isset($_SESSION['cart']) ? array_sum(array_column($_SESSION['cart'], 'quantity')) : 0;
|
||||||
|
$page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?php echo htmlspecialchars($page_title); ?></title>
|
||||||
|
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'خرید محصولات چرمی لوکس و با کیفیت.'); ?>">
|
||||||
|
|
||||||
|
<!-- IRANSans Font -->
|
||||||
|
<link rel="stylesheet" href="https://font-ir.s3.ir-thr-at1.arvanstorage.com/IRANSans/css/IRANSans.css">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
|
<!-- Remix Icon CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- AOS CSS -->
|
||||||
|
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Main Theme CSS -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/theme.css?v=<?php echo time(); ?>">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Apply theme from local storage before page load to prevent flashing
|
||||||
|
(function() {
|
||||||
|
const theme = localStorage.getItem('theme') || 'dark';
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body class="dark-luxury">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<?php
|
||||||
|
$is_admin_page = strpos($_SERVER['REQUEST_URI'], '/admin/') !== false;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<header class="site-header sticky-top py-3">
|
||||||
|
<nav class="navbar navbar-expand-lg container">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand fw-bold fs-4" href="index.php">آتیمه</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="mainNavbar">
|
||||||
|
<ul class="navbar-nav mx-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="index.php">خانه</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="shop.php">فروشگاه</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="about.php">درباره ما</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="contact.php">تماس با ما</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<a href="cart.php" class="ms-4 position-relative">
|
||||||
|
<i class="ri-shopping-bag-line fs-5"></i>
|
||||||
|
<?php if ($cart_item_count > 0): ?>
|
||||||
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||||
|
<?php echo $cart_item_count; ?>
|
||||||
|
<span class="visually-hidden">محصول در سبد خرید</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['user_id'])): ?>
|
||||||
|
<a href="profile.php" class="ms-4 d-flex align-items-center text-decoration-none" title="حساب کاربری">
|
||||||
|
<i class="ri-user-line fs-5 me-2"></i>
|
||||||
|
<span><?php echo htmlspecialchars($_SESSION['user_name']); ?></span>
|
||||||
|
</a>
|
||||||
|
<?php if (!empty($_SESSION['is_admin'])): ?>
|
||||||
|
<a href="/admin/index.php" class="ms-3" title="پنل مدیریت">
|
||||||
|
<i class="ri-shield-user-line fs-5"></i>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="logout.php" class="ms-3" title="خروج">
|
||||||
|
<i class="ri-logout-box-r-line fs-5"></i>
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="login.php" class="btn btn-primary btn-sm ms-3">ورود / ثبتنام</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
258
includes/jdf.php
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
Farsi Jalali (Shamsi) Date and Time Functions
|
||||||
|
Copyright (C) 2000-2019 Sallar Kaboli (http://sallar.kaboli.org)
|
||||||
|
Latest version available at: http://jdf.scr.ir
|
||||||
|
|
||||||
|
LICENSE: FREE FOR NON-COMMERCIAL USE
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
function jdate($format, $timestamp = '', $none = '', $time_zone = 'Asia/Tehran', $tr_num = 'fa')
|
||||||
|
{
|
||||||
|
|
||||||
|
$T_sec=0;/* <= آرشیو شود */
|
||||||
|
|
||||||
|
if($time_zone != 'local') date_default_timezone_set(($time_zone === '') ? 'Asia/Tehran' : $time_zone);
|
||||||
|
$ts=$T_sec+(($timestamp==='') ? time() : tr_num($timestamp));
|
||||||
|
$date=explode(' _ ',date('H_i_j_n_O_P_s_w_Y',$ts));
|
||||||
|
list($j_y,$j_m,$j_d)=gregorian_to_jalali($date[8],$date[3],$date[2]);
|
||||||
|
$doy=($j_m<7)?(($j_m-1)*31)+$j_d-1:(($j_m-7)*30)+$j_d+185;
|
||||||
|
$kab=(((($j_y%33)%4)-1)%4==0);
|
||||||
|
$sl=strlen($format);
|
||||||
|
$out='';
|
||||||
|
for($i=0; $i<$sl; $i++)
|
||||||
|
{
|
||||||
|
$sub=substr($format,$i,1);
|
||||||
|
if($sub=='\\')
|
||||||
|
{
|
||||||
|
$out.=substr($format,++$i,1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch($sub)
|
||||||
|
{
|
||||||
|
|
||||||
|
case 'a':$out.=($date[0]<12)?'ق.ظ':'ب.ظ';break;
|
||||||
|
case 'A':$out.=($date[0]<12)?'قبل از ظهر':'بعد از ظهر';break;
|
||||||
|
case 'B':$out.=floor(1+($ts/86400));break;
|
||||||
|
case 'c':$out.=date('Y-m-d\TH:i:sP',$ts);break;
|
||||||
|
case 'd':$out.=($j_d<10)?'0'.$j_d:$j_d;break;
|
||||||
|
case 'D':$out.=jdate_words(array('rh'=>$date[7]),' ');break;
|
||||||
|
case 'F':$out.=jdate_words(array('mm'=>$j_m),' ');break;
|
||||||
|
case 'g':$out.=($date[0]>12)?$date[0]-12:$date[0];break;
|
||||||
|
case 'G':$out.=$date[0];break;
|
||||||
|
case 'h':$out.=((($date[0]>12)?$date[0]-12:$date[0])<10)?'0'.(($date[0]>12)?$date[0]-12:$date[0]):(($date[0]>12)?$date[0]-12:$date[0]);break;
|
||||||
|
case 'H':$out.=($date[0]<10)?'0'.$date[0]:$date[0];break;
|
||||||
|
case 'i':$out.=($date[1]<10)?'0'.$date[1]:$date[1];break;
|
||||||
|
case 'j':$out.=$j_d;break;
|
||||||
|
case 'l':$out.=jdate_words(array('rh'=>$date[7]),' ');break;
|
||||||
|
case 'L':$out.=$kab;break;
|
||||||
|
case 'm':$out.=($j_m<10)?'0'.$j_m:$j_m;break;
|
||||||
|
case 'M':$out.=jdate_words(array('mn'=>$j_m),' ');break;
|
||||||
|
case 'n':$out.=$j_m;break;
|
||||||
|
case 'N':$out.=$date[7]+1;break;
|
||||||
|
case 'o':
|
||||||
|
$j_y_plus= $j_y+1;
|
||||||
|
$j_y_minus= $j_y-1;
|
||||||
|
$fall_start=gregorian_to_jalali(date('Y', $ts),3,21);
|
||||||
|
$fall_end=gregorian_to_jalali(date('Y', $ts),12,21);
|
||||||
|
if($j_d > $fall_start[2] && $j_m <4)
|
||||||
|
{
|
||||||
|
$out.=$j_y_minus;
|
||||||
|
}
|
||||||
|
elseif($j_d < $fall_end[2] && $j_m > 10)
|
||||||
|
{
|
||||||
|
$out.=$j_y_plus;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$out.=$j_y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'O':$out.=$date[4];break;
|
||||||
|
case 'p':$out.=jdate_words(array('mb'=>$j_m),' ');break;
|
||||||
|
case 'P':$out.=$date[5];break;
|
||||||
|
case 'q':$out.=jdate_words(array('sh'=>$j_y),' ');break;
|
||||||
|
case 'r':$out.=jdate_words(array('rh'=>$date[7]),' ') .'، ' . $j_d.' ' . jdate_words(array('mm'=>$j_m),' ') .' ' . $j_y .' ' . $date[0].':' . $date[1].':' . $date[6] .' ' . $date[4];break;
|
||||||
|
case 's':$out.=($date[6]<10)?'0'.$date[6]:$date[6];break;
|
||||||
|
case 'S':$out.='ام';break;
|
||||||
|
case 't':$out.=($j_m!=12)?(31-(int)($j_m/6.5)):($kab+29);break;
|
||||||
|
case 'U':$out.=$ts;break;
|
||||||
|
case 'w':$out.=$date[7];break;
|
||||||
|
case 'W':$out.=(int)($doy/7);break;
|
||||||
|
case 'y':$out.=substr($j_y,2,2);break;
|
||||||
|
case 'Y':$out.=$j_y;break;
|
||||||
|
case 'z':$out.=$doy;break;
|
||||||
|
default:$out.=$sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ($tr_num!='en')?tr_num($out):$out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jstrftime($format, $timestamp = '', $none = '', $time_zone = 'Asia/Tehran', $tr_num = 'fa')
|
||||||
|
{
|
||||||
|
|
||||||
|
$T_sec=0;/* <= آرشیو شود */
|
||||||
|
|
||||||
|
if($time_zone != 'local') date_default_timezone_set(($time_zone === '') ? 'Asia/Tehran' : $time_zone);
|
||||||
|
$ts=$T_sec+(($timestamp==='')?time():tr_num($timestamp));
|
||||||
|
$date=explode(' _ ',date('h_H_i_j_n_s_w_Y',$ts));
|
||||||
|
list($j_y,$j_m,$j_d)=gregorian_to_jalali($date[7],$date[4],$date[3]);
|
||||||
|
$doy=($j_m<7)?(($j_m-1)*31)+$j_d-1:(($j_m-7)*30)+$j_d+185;
|
||||||
|
$kab=(((($j_y%33)%4)-1)%4==0);
|
||||||
|
$sl=strlen($format);
|
||||||
|
$out='';
|
||||||
|
for($i=0; $i<$sl; $i++)
|
||||||
|
{
|
||||||
|
$sub=substr($format,$i,1);
|
||||||
|
if($sub=='%')
|
||||||
|
{
|
||||||
|
$sub=substr($format,++$i,1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$out.=$sub;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch($sub)
|
||||||
|
{
|
||||||
|
|
||||||
|
case 'a':$out.=jdate_words(array('rh'=>$date[6]),' ');break;
|
||||||
|
case 'A':$out.=jdate_words(array('RL'=>$date[6]),' ');break;
|
||||||
|
case 'b':$out.=jdate_words(array('mm'=>$j_m),' ');break;
|
||||||
|
case 'B':$out.=jdate_words(array('MM'=>$j_m),' ');break;
|
||||||
|
case 'c':$out.=jdate('D M j H:i:s Y');break;
|
||||||
|
case 'C':$out.=(int)($j_y/100);break;
|
||||||
|
case 'd':$out.=($j_d<10)?'0'.$j_d:$j_d;break;
|
||||||
|
case 'D':$out.=substr($j_y,2,2).'/' .( ($j_m<10)?'0'.$j_m:$j_m ).'/' .( ($j_d<10)?'0'.$j_d:$j_d );break;
|
||||||
|
case 'e':$out.=($j_d<10)?' '.$j_d:$j_d;break;
|
||||||
|
case 'H':$out.=($date[1]<10)?'0'.$date[1]:$date[1];break;
|
||||||
|
case 'I':$out.=($date[0]<10)?'0'.$date[0]:$date[0];break;
|
||||||
|
case 'j':$out.=($doy<100)?(($doy<10)?'00'.$doy:'0'.$doy):$doy;break;
|
||||||
|
case 'm':$out.=($j_m<10)?'0'.$j_m:$j_m;break;
|
||||||
|
case 'M':$out.=($date[2]<10)?'0'.$date[2]:$date[2];break;
|
||||||
|
case 'p':$out.=($date[1]<12)?'قبل از ظهر':'بعد از ظهر';break;
|
||||||
|
case 'P':$out.=($date[1]<12)?'ق.ظ':'ب.ظ';break;
|
||||||
|
case 's':$out.=floor($ts);break;
|
||||||
|
case 'S':$out.=($date[5]<10)?'0'.$date[5]:$date[5];break;
|
||||||
|
case 'u':$out.=$date[6]+1;break;
|
||||||
|
case 'U':$out.=(int)($doy/7);break;
|
||||||
|
case 'V':$out.=(int)($doy/7);break;
|
||||||
|
case 'w':$out.=$date[6];break;
|
||||||
|
case 'W':$out.=(int)($doy/7);break;
|
||||||
|
case 'x':$out.=substr($j_y,2,2).'/' .( ($j_m<10)?'0'.$j_m:$j_m ).'/' .( ($j_d<10)?'0'.$j_d:$j_d );break;
|
||||||
|
case 'X':$out.=($date[0]<10)?'0'.$date[0]:$date[0].':' .( ($date[1]<10)?'0'.$date[1]:$date[1] ).':' .( ($date[6]<10)?'0'.$date[6]:$date[6] );break;
|
||||||
|
case 'y':$out.=substr($j_y,2,2);break;
|
||||||
|
case 'Y':$out.=$j_y;break;
|
||||||
|
case 'Z':$out.=date('T',$ts);break;
|
||||||
|
case '%':$out.='%';break;
|
||||||
|
|
||||||
|
default:$out.=$sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ($tr_num!='en')?tr_num($out):$out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gregorian_to_jalali($gy,$gm,$gd,$mod='')
|
||||||
|
{
|
||||||
|
$g_d_m=array(0,31,59,90,120,151,181,212,243,273,304,334);
|
||||||
|
$gy2=($gm>2)?($gy+1):$gy;
|
||||||
|
$days=355666+(365*$gy)+((int)(($gy2+3)/4))-((int)(($gy2+99)/100))+((int)(($gy2+399)/400))+$gd+$g_d_m[$gm-1];
|
||||||
|
$jy=-1595+(33*((int)($days/12053)));
|
||||||
|
$days%=12053;
|
||||||
|
$jy+=4*((int)($days/1461));
|
||||||
|
$days%=1461;
|
||||||
|
if($days > 365)
|
||||||
|
{
|
||||||
|
$jy+=(int)(($days-1)/365);
|
||||||
|
$days=($days-1)%365;
|
||||||
|
}
|
||||||
|
$jm=($days<186)?1+(int)($days/31):7+(int)(($days-186)/30);
|
||||||
|
$jd=1+(($days<186)?($days%31):(($days-186)%30));
|
||||||
|
return($mod=='')?array($jy,$jm,$jd):$jy.$mod.$jm.$mod.$jd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jalali_to_gregorian($jy,$jm,$jd,$mod='')
|
||||||
|
{
|
||||||
|
$jy+=1595;
|
||||||
|
$days=-355668+(365*$jy)+(((int)($jy/33))*8)+((int)((($jy%33)+3)/4));
|
||||||
|
if((($jy%33)%4)==0 and $gy%100!=0 and $gy%400!=0){$days++;}
|
||||||
|
$jd+=($jm<7)?($jm-1)*31:(($jm-7)*30)+186;
|
||||||
|
$days+=$jd;
|
||||||
|
$gy=400*((int)($days/146097));
|
||||||
|
$days%=146097;
|
||||||
|
if($days > 36524)
|
||||||
|
{
|
||||||
|
$gy+=100*((int)(--$days/36524));
|
||||||
|
$days%=36524;
|
||||||
|
if($days >= 365){$days++;}
|
||||||
|
}
|
||||||
|
$gy+=4*((int)($days/1461));
|
||||||
|
$days%=1461;
|
||||||
|
$gy+=(int)(($days-1)/365);
|
||||||
|
$days=($days-1)%365;
|
||||||
|
$gd=$days+1;
|
||||||
|
foreach(array(0,31,(($gy%4==0 and $gy%100!=0) or ($gy%400==0))?29:28,31,30,31,30,31,31,30,31,30,31) as $gm=>$v)
|
||||||
|
{
|
||||||
|
if($gd<=$v)break;
|
||||||
|
$gd-=$v;
|
||||||
|
}
|
||||||
|
return($mod=='')?array($gy,$gm,$gd):$gy.$mod.$gm.$mod.$gd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jdate_words($array,$mod='')
|
||||||
|
{
|
||||||
|
foreach($array as $type=>$num)
|
||||||
|
{
|
||||||
|
$num=(int)tr_num($num);
|
||||||
|
switch($type)
|
||||||
|
{
|
||||||
|
case 'ss':
|
||||||
|
$sl=strlen($num);
|
||||||
|
$xy3=substr($num,2-$sl,1);
|
||||||
|
$h3=jdate_words(array('h'=>$xy3));
|
||||||
|
$h34=jdate_words(array('h'=>$xy3+1));
|
||||||
|
$xy4=substr($num,3-$sl,1);
|
||||||
|
$h4=jdate_words(array('h'=>$xy4));
|
||||||
|
$h44=jdate_words(array('h'=>$xy4+1));
|
||||||
|
if($sl==4)
|
||||||
|
{
|
||||||
|
$f=($num<2000)?(($num<1400 and $num>1299)?jdate_words(array('h'=>13)).'صد و '.jdate_words(array('ss'=>substr($num,2,2))):''):jdate_words(array('h'=>substr($num,0,1))).' هزار و '.jdate_words(array('ss'=>substr($num,1,3)));
|
||||||
|
}
|
||||||
|
elseif($sl==3)
|
||||||
|
{
|
||||||
|
$f=($num<200)?jdate_words(array('h'=>1)).'صد و '.jdate_words(array('ss'=>substr($num,1,2))):jdate_words(array('h'=>substr($num,0,1))).'صد و '.jdate_words(array('ss'=>substr($num,1,2)));
|
||||||
|
}
|
||||||
|
elseif($sl==2)
|
||||||
|
{
|
||||||
|
$f=($num>9 and $num<21)?jdate_words(array('h'=>$num)):
|
||||||
|
jdate_words(array('h'=>(int)($num/10))).' و '.jdate_words(array('h'=>$num%10));
|
||||||
|
}
|
||||||
|
else{$f=jdate_words(array('h'=>$num));}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'mm':$key=array('فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند');$f=$key[$num-1];break;
|
||||||
|
case 'mn':$key=array('فرو','ارد','خرد','تیر','مرد','شهر','مهر','آبا','آذر','دی','بهم','اسف');$f=$key[$num-1];break;
|
||||||
|
case 'rh':$key=array('یکشنبه','دوشنبه','سه شنبه','چهارشنبه','پنجشنبه','جمعه','شنبه');$f=$key[$num];break;
|
||||||
|
case 'RL':$key=array('یک','دو','سه','چهار','پنج','جمعه','شنبه');$f=$key[$num];break;
|
||||||
|
case 'sh':$key=array('مار','اسب','گوسفند','میمون','مرغ','سگ','خوک','موش','گاو','پلنگ','خرگوش','نهنگ');$f=$key[$num%12];break;
|
||||||
|
case 'mb':$key=array('حمل','ثور','جوزا','سرطان','اسد','سنبله','میزان','عقرب','قوس','جدی','دلو','حوت');$f=$key[$num-1];break;
|
||||||
|
case 'h':$key=array('صفر','یک','دو','سه','چهار','پنج','شش','هفت','هشت','نه','ده','یازده','دوازده','سیزده','چهارده','پانزده','شانزده','هفده','هجده','نوزده','بیست');$f=$key[$num];break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tr_num($str,$mod='en',$mf='٫')
|
||||||
|
{
|
||||||
|
$num_a=array('0','1','2','3','4','5','6','7','8','9','.');
|
||||||
|
$key_a=array('۰','۱','۲','۳','۴','۵','۶','۷','۸','۹',$mf);
|
||||||
|
return($mod=='fa')?str_replace($num_a,$key_a,$str):str_replace($key_a,$num_a,$str);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
25
includes/pexels.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
function pexels_key() {
|
||||||
|
$k = getenv('PEXELS_KEY');
|
||||||
|
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
|
||||||
|
}
|
||||||
|
function pexels_get($url) {
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
|
||||||
|
CURLOPT_TIMEOUT => 15,
|
||||||
|
]);
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function download_to($srcUrl, $destPath) {
|
||||||
|
$data = file_get_contents($srcUrl);
|
||||||
|
if ($data === false) return false;
|
||||||
|
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
|
||||||
|
return file_put_contents($destPath, $data) !== false;
|
||||||
|
}
|
||||||
11
includes/session_config.php
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
// Force session cookie parameters for cross-domain compatibility.
|
||||||
|
session_set_cookie_params([
|
||||||
|
'lifetime' => 86400, // 24 hours
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '', // Set your domain if needed, empty for current host
|
||||||
|
'secure' => true, // Must be true for SameSite=None
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'None' // Allows cross-site cookie sending
|
||||||
|
]);
|
||||||
|
?>
|
||||||
253
index.php
@ -1,150 +1,111 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
$page_title = 'صفحه اصلی';
|
||||||
@ini_set('display_errors', '1');
|
include 'includes/header.php';
|
||||||
@error_reporting(E_ALL);
|
|
||||||
@date_default_timezone_set('UTC');
|
|
||||||
|
|
||||||
$phpVersion = PHP_VERSION;
|
|
||||||
$now = date('Y-m-d H:i:s');
|
// Load dynamic content
|
||||||
|
$about_us_image_data = json_decode(file_get_contents('about_us_image.json'), true);
|
||||||
|
$about_us_image_url = $about_us_image_data ? str_replace('\\/', '/', $about_us_image_data['local_path']) : 'assets/images/pexels/about-us-34942790.jpg';
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
<!-- Hero Section -->
|
||||||
<head>
|
<section class="hero-section vh-100 d-flex justify-content-center align-items-center position-relative text-white text-center">
|
||||||
<meta charset="utf-8" />
|
<div class="hero-video-background">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<video playsinline autoplay muted loop poster="assets/images/pexels/about-us-34942790.jpg">
|
||||||
<title>New Style</title>
|
<source src="https://videos.pexels.com/video-files/8065365/8065365-hd_1920_1080_25fps.mp4" type="video/mp4">
|
||||||
<?php
|
ویدیوی شما توسط مرورگر پشتیبانی نمیشود.
|
||||||
// Read project preview data from environment
|
</video>
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<div class="hero-video-overlay"></div>
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
||||||
?>
|
|
||||||
<?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>
|
|
||||||
<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>
|
||||||
</main>
|
<div class="container position-relative">
|
||||||
<footer>
|
<h1 class="display-3 fw-bold" data-aos="fade-down">اصالت در هر نگاه</h1>
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<p class="lead fs-4 mb-4" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دستدوز، آفریده برای ماندگاری.</p>
|
||||||
</footer>
|
<a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">مشاهده محصولات</a>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Featured Products Section -->
|
||||||
|
<section id="featured-products" class="section-padding">
|
||||||
|
<div class="container">
|
||||||
|
<?php
|
||||||
|
if (isset($_SESSION['success_message'])) {
|
||||||
|
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">' . $_SESSION['success_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||||
|
unset($_SESSION['success_message']);
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['error_message'])) {
|
||||||
|
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . $_SESSION['error_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
|
||||||
|
unset($_SESSION['error_message']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="section-title text-center" data-aos="fade-up">
|
||||||
|
<h1>مجموعه برگزیده ما</h1>
|
||||||
|
<p class="fs-5 text-muted">دستچین شده برای سلیقههای خاص.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 g-lg-5">
|
||||||
|
<?php
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT * FROM products WHERE is_featured = 1 ORDER BY created_at DESC LIMIT 3");
|
||||||
|
$featured_products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($featured_products)) {
|
||||||
|
echo '<div class="col-12"><p class="text-center text-muted">هیچ محصولی برای نمایش وجود ندارد.</p></div>';
|
||||||
|
} else {
|
||||||
|
$delay = 0;
|
||||||
|
foreach ($featured_products as $product) {
|
||||||
|
?>
|
||||||
|
<div class="col" data-aos="fade-up" data-aos-delay="<?= $delay ?>">
|
||||||
|
<div class="product-card h-100">
|
||||||
|
<div class="product-image">
|
||||||
|
<a href="product.php?id=<?= $product['id'] ?>">
|
||||||
|
<img src="<?= htmlspecialchars($product['image_url']) ?>" alt="<?= htmlspecialchars($product['name']) ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="product-info text-center">
|
||||||
|
<h3 class="product-title"><a href="product.php?id=<?= $product['id'] ?>"><?= htmlspecialchars($product['name']) ?></a></h3>
|
||||||
|
<p class="product-price"><?= number_format($product['price']) ?> تومان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$delay += 150;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Database error: " . $e->getMessage());
|
||||||
|
echo '<div class="col-12"><p class="text-center text-danger">خطا در بارگذاری محصولات.</p></div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-5" data-aos="fade-up">
|
||||||
|
<a href="shop.php" class="btn btn-primary">مشاهده تمام محصولات</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- About Us Section -->
|
||||||
|
<section id="about-us" class="section-padding bg-surface">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center g-5">
|
||||||
|
<div class="col-md-6" data-aos="fade-right">
|
||||||
|
<img src="<?= htmlspecialchars($about_us_image_url) ?>" alt="درباره ما" class="about-us-image img-fluid">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6" data-aos="fade-left">
|
||||||
|
<div class="section-title text-md-end text-start">
|
||||||
|
<h1>داستان آتیمه</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted fs-5 mt-3 text-md-end text-start">ما در آتیمه، به تلفیق هنر سنتی و طراحی مدرن باور داریم. هر محصول، حاصل ساعتها کار دست هنرمندان ماهر و استفاده از بهترین چرمهای طبیعی است. هدف ما خلق آثاری است که نه تنها یک وسیله، بلکه بخشی از داستان و استایل شما باشند.</p>
|
||||||
|
<div class="text-md-end text-start">
|
||||||
|
<a href="about.php" class="btn btn-primary mt-3" data-aos="fade-up" data-aos-delay="200">بیشتر بدانید</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php include 'includes/footer.php'; ?>
|
||||||
|
|||||||
174
login.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/session_config.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: profile.php'); // Redirect to profile page if logged in
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page_title = "ورود یا ثبتنام";
|
||||||
|
$page_description = "به آتیمه، خانه چرم و اصالت خوش آمدید. وارد حساب کاربری خود شوید یا یک حساب جدید بسازید تا از تجربه خرید لذت ببرید.";
|
||||||
|
$page_keywords = "ورود, ثبت نام, چرم, آتیمه, حساب کاربری";
|
||||||
|
|
||||||
|
// Using a specific body class for targeted styling
|
||||||
|
$body_class = "login-page-modern";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= htmlspecialchars($page_title); ?> - آتیمه</title>
|
||||||
|
<meta name="description" content="<?= htmlspecialchars($page_description); ?>">
|
||||||
|
<meta name="keywords" content="<?= htmlspecialchars($page_keywords); ?>">
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<meta name="robots" content="index, follow">
|
||||||
|
<link rel="canonical" href="https://yourdomain.com/login.php" /> <!-- Replace with your actual domain -->
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
|
||||||
|
<!-- Remixicon -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Main CSS -->
|
||||||
|
<link rel="stylesheet" href="assets/css/theme.css?v=<?= time(); ?>">
|
||||||
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time(); ?>">
|
||||||
|
</head>
|
||||||
|
<body class="<?= $body_class; ?>">
|
||||||
|
|
||||||
|
<main class="login-container">
|
||||||
|
<div class="login-form-wrapper">
|
||||||
|
<div class="login-header text-center mb-4">
|
||||||
|
<a href="index.php" class="logo-link">
|
||||||
|
<h1 class="logo-title h2">آتیمه</h1>
|
||||||
|
</a>
|
||||||
|
<p class="tagline">اصالت و زیبایی در دستان شما</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="form-title text-center mb-4">ورود یا ثبت نام</h2>
|
||||||
|
|
||||||
|
<?php if(isset($_SESSION['flash_message'])): ?>
|
||||||
|
<div class="alert alert-<?= htmlspecialchars($_SESSION['flash_message']['type']); ?> alert-dismissible fade show my-3" role="alert">
|
||||||
|
<?= htmlspecialchars($_SESSION['flash_message']['message']); ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['flash_message']); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="auth_handler.php?action=send_otp" method="POST" class="needs-validation" novalidate>
|
||||||
|
<div class="login-toggle-container mb-4">
|
||||||
|
<div class="btn-group w-100" role="group" aria-label="Login method toggle">
|
||||||
|
<input type="radio" class="btn-check" name="login_method" id="email_toggle" value="email" autocomplete="off" checked>
|
||||||
|
<label class="btn btn-outline-primary" for="email_toggle">ایمیل</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="login_method" id="phone_toggle" value="phone" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-primary" for="phone_toggle">تلفن همراه</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Input -->
|
||||||
|
<div id="email_input_group">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input type="email" class="form-control" id="email_input" name="email" placeholder="ایمیل خود را وارد کنید" required>
|
||||||
|
<label for="email_input">ایمیل</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
لطفا یک ایمیل معتبر وارد کنید.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phone Input (hidden by default) -->
|
||||||
|
<div id="phone_input_group" style="display: none;">
|
||||||
|
<div class="form-floating mb-3">
|
||||||
|
<input type="tel" class="form-control" id="phone_input" name="phone" placeholder="09123456789" pattern="09[0-9]{9}" required>
|
||||||
|
<label for="phone_input">تلفن همراه</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
لطفا یک شماره تلفن معتبر (مانند 09123456789) وارد کنید.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">ادامه</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="or-separator text-center my-3">
|
||||||
|
<span class="text-muted">یا</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="google_callback.php" class="btn btn-light border w-100 d-flex align-items-center justify-content-center py-2 shadow-sm">
|
||||||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" alt="Google icon" style="width: 20px; height: 20px;" class="me-2">
|
||||||
|
<span class="fw-bold text-secondary">ورود با گوگل</span>
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-footer text-center mt-4">
|
||||||
|
<p><a href="index.php"><i class="ri-arrow-right-line align-middle"></i> بازگشت به فروشگاه</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Standard Bootstrap validation script
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var forms = document.querySelectorAll('.needs-validation');
|
||||||
|
Array.prototype.slice.call(forms)
|
||||||
|
.forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (event) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add('was-validated');
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
// New, simplified toggle logic
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const emailToggle = document.getElementById('email_toggle');
|
||||||
|
const phoneToggle = document.getElementById('phone_toggle');
|
||||||
|
|
||||||
|
const emailGroup = document.getElementById('email_input_group');
|
||||||
|
const phoneGroup = document.getElementById('phone_input_group');
|
||||||
|
|
||||||
|
const emailInput = document.getElementById('email_input');
|
||||||
|
const phoneInput = document.getElementById('phone_input');
|
||||||
|
|
||||||
|
function toggleInputs(showEmail) {
|
||||||
|
if (showEmail) {
|
||||||
|
emailGroup.style.display = 'block';
|
||||||
|
emailInput.disabled = false;
|
||||||
|
phoneGroup.style.display = 'none';
|
||||||
|
phoneInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
emailGroup.style.display = 'none';
|
||||||
|
emailInput.disabled = true;
|
||||||
|
phoneGroup.style.display = 'block';
|
||||||
|
phoneInput.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emailToggle.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
toggleInputs(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
phoneToggle.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
toggleInputs(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
toggleInputs(emailToggle.checked);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
logout.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
// This file provides a simple entry point for logging out.
|
||||||
|
require_once __DIR__ . '/auth_handler.php';
|
||||||
54
migrate.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Simple migration runner
|
||||||
|
function run_migrations() {
|
||||||
|
$pdo = db();
|
||||||
|
$migrations_dir = __DIR__ . '/db/migrations';
|
||||||
|
|
||||||
|
// Create migrations table if it doesn't exist
|
||||||
|
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (migration VARCHAR(255) PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
|
||||||
|
|
||||||
|
// Get executed migrations
|
||||||
|
$executed_migrations_stmt = $pdo->query("SELECT migration FROM migrations");
|
||||||
|
$executed_migrations = $executed_migrations_stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$migration_files = glob($migrations_dir . '/*.sql');
|
||||||
|
sort($migration_files);
|
||||||
|
|
||||||
|
foreach ($migration_files as $file) {
|
||||||
|
$migration_name = basename($file);
|
||||||
|
|
||||||
|
if (in_array($migration_name, $executed_migrations)) {
|
||||||
|
continue; // Skip already executed migration
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Running migration: {$migration_name}...
|
||||||
|
";
|
||||||
|
$sql = file_get_contents($file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->exec($sql);
|
||||||
|
|
||||||
|
// Log the migration
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
|
||||||
|
$stmt->execute([$migration_name]);
|
||||||
|
|
||||||
|
echo "Migration {$migration_name} executed successfully.
|
||||||
|
";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error running migration {$migration_name}: " . $e->getMessage() . "
|
||||||
|
";
|
||||||
|
// Stop on error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "All new migrations have been executed.
|
||||||
|
";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run it
|
||||||
|
run_migrations();
|
||||||
|
|
||||||
160
product.php
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
|
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||||
|
$product = null;
|
||||||
|
$db_error = '';
|
||||||
|
|
||||||
|
if (!$product_id) {
|
||||||
|
// Redirect or show error if ID is not valid
|
||||||
|
header("Location: shop.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
||||||
|
$stmt->execute([$product_id]);
|
||||||
|
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Database Error: " . $e->getMessage());
|
||||||
|
$db_error = "<p>خطا در برقراری ارتباط با پایگاه داده.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If product not found, show a message and stop
|
||||||
|
if (!$product) {
|
||||||
|
echo '<main class="container py-5 text-center"><div class="alert alert-danger">محصولی با این شناسه یافت نشد.</div><div><a href="shop.php" class="btn btn-primary mt-3">بازگشت به فروشگاه</a></div></main>';
|
||||||
|
require_once 'includes/footer.php';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set page title after fetching product name
|
||||||
|
$page_title = htmlspecialchars($product['name']);
|
||||||
|
|
||||||
|
// Parse comma-separated colors string
|
||||||
|
$available_colors = [];
|
||||||
|
if (!empty($product['colors'])) {
|
||||||
|
$colors_raw = explode(',', $product['colors']);
|
||||||
|
foreach ($colors_raw as $color) {
|
||||||
|
$trimmed_color = trim($color);
|
||||||
|
if (!empty($trimmed_color)) {
|
||||||
|
$available_colors[] = $trimmed_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="container section-padding">
|
||||||
|
<div class="row g-5">
|
||||||
|
<div class="col-lg-6" data-aos="fade-right">
|
||||||
|
<div class="card card-static p-3">
|
||||||
|
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($product['name']); ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6" data-aos="fade-left">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body p-4 p-lg-5">
|
||||||
|
<h1 class="display-5 fw-bold mb-3"><?php echo htmlspecialchars($product['name']); ?></h1>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
<p class="display-6 fw-bold m-0"><?php echo number_format($product['price']); ?> تومان</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="fs-5 mb-4 text-muted"><?php echo nl2br(htmlspecialchars($product['description'])); ?></p>
|
||||||
|
|
||||||
|
<form action="cart_handler.php" method="POST" class="mt-auto">
|
||||||
|
<input type="hidden" name="product_id" value="<?php echo $product['id']; ?>">
|
||||||
|
<input type="hidden" name="action" value="add">
|
||||||
|
|
||||||
|
<?php if (!empty($available_colors)): ?>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="mb-3">انتخاب رنگ:</h5>
|
||||||
|
<div class="color-swatches">
|
||||||
|
<?php foreach ($available_colors as $index => $color_hex): ?>
|
||||||
|
<input type="radio" class="btn-check" name="product_color" id="color_<?php echo $index; ?>" value="<?php echo htmlspecialchars($color_hex); ?>" autocomplete="off" <?php echo (count($available_colors) === 1) ? 'checked' : ''; ?>/>
|
||||||
|
<label class="btn" for="color_<?php echo $index; ?>" style="background-color: <?php echo htmlspecialchars($color_hex); ?>;"></label>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="row align-items-center mb-4">
|
||||||
|
<div class="col-md-5 col-lg-4 quantity-input-wrapper">
|
||||||
|
<label for="quantity" class="form-label fw-bold">تعداد:</label>
|
||||||
|
<input type="number" name="quantity" id="quantity" class="form-control quantity-input text-center" value="1" min="1" max="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2 add-to-cart-btn">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="ri-shopping-bag-add-line"></i> افزودن به سبد خرید</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- SweetAlert for color validation -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// --- 1. Flash Message Handling (from server-side) ---
|
||||||
|
<?php
|
||||||
|
if (isset($_SESSION['flash_message'])) {
|
||||||
|
$flash_message = $_SESSION['flash_message'];
|
||||||
|
unset($_SESSION['flash_message']);
|
||||||
|
echo "Swal.fire({
|
||||||
|
title: '".addslashes($flash_message['message'])."',
|
||||||
|
icon: '". $flash_message['type'] ."',
|
||||||
|
toast: true,
|
||||||
|
position: 'top-start',
|
||||||
|
showConfirmButton: false,
|
||||||
|
timer: 4000,
|
||||||
|
timerProgressBar: true,
|
||||||
|
showCloseButton: true,
|
||||||
|
didOpen: (toast) => {
|
||||||
|
toast.addEventListener('mouseenter', Swal.stopTimer);
|
||||||
|
toast.addEventListener('mouseleave', Swal.resumeTimer);
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
popup: 'dark-theme-toast'
|
||||||
|
}
|
||||||
|
});";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
// --- 2. Client-side Color Selection Validation ---
|
||||||
|
const form = document.querySelector('form[action="cart_handler.php"]');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function(event) {
|
||||||
|
const availableColors = <?php echo json_encode($available_colors); ?>;
|
||||||
|
const hasMultipleColors = Array.isArray(availableColors) && availableColors.length > 1;
|
||||||
|
|
||||||
|
if (hasMultipleColors) {
|
||||||
|
const selectedColor = document.querySelector('input[name="product_color"]:checked');
|
||||||
|
if (!selectedColor) {
|
||||||
|
event.preventDefault(); // Stop form submission
|
||||||
|
Swal.fire({
|
||||||
|
title: 'لطفاً یک رنگ انتخاب کنید',
|
||||||
|
text: 'برای افزودن این محصول به سبد خرید، انتخاب رنگ الزامی است.',
|
||||||
|
icon: 'warning',
|
||||||
|
confirmButtonText: 'متوجه شدم',
|
||||||
|
customClass: {
|
||||||
|
popup: 'dark-theme-popup',
|
||||||
|
title: 'dark-theme-title',
|
||||||
|
htmlContainer: 'dark-theme-content',
|
||||||
|
confirmButton: 'dark-theme-button'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
428
profile.php
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/jdf.php'; // For Jalali date conversion
|
||||||
|
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = $_SESSION['user_id'];
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
// Handle form submissions for account page
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
|
$action = $_POST['action'];
|
||||||
|
$redirect_page = $_GET['page'] ?? 'dashboard';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'update_details') {
|
||||||
|
$first_name = trim($_POST['first_name'] ?? '');
|
||||||
|
$last_name = trim($_POST['last_name'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
|
||||||
|
if (empty($first_name) || empty($last_name) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception('لطفاً تمام فیلدها را به درستی پر کنید.');
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET first_name = ?, last_name = ?, email = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$first_name, $last_name, $email, $user_id]);
|
||||||
|
$_SESSION['profile_message'] = 'اطلاعات شما با موفقیت بهروزرسانی شد.';
|
||||||
|
$_SESSION['profile_message_type'] = 'success';
|
||||||
|
$redirect_page = 'account';
|
||||||
|
} elseif ($action === 'update_password') {
|
||||||
|
$new_password = $_POST['new_password'] ?? '';
|
||||||
|
$confirm_password = $_POST['confirm_password'] ?? '';
|
||||||
|
|
||||||
|
if (strlen($new_password) < 8) {
|
||||||
|
throw new Exception('رمز عبور جدید باید حداقل ۸ کاراکتر باشد.');
|
||||||
|
} elseif ($new_password !== $confirm_password) {
|
||||||
|
throw new Exception('رمزهای عبور جدید با هم مطابقت ندارند.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET password = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$hashed_password, $user_id]);
|
||||||
|
$_SESSION['profile_message'] = 'رمز عبور شما با موفقیت تغییر کرد.';
|
||||||
|
$_SESSION['profile_message_type'] = 'success';
|
||||||
|
$redirect_page = 'account';
|
||||||
|
|
||||||
|
} elseif ($action === 'add_address') {
|
||||||
|
$province = trim($_POST['province'] ?? '');
|
||||||
|
$city = trim($_POST['city'] ?? '');
|
||||||
|
$address_line = trim($_POST['address_line'] ?? '');
|
||||||
|
$postal_code = trim($_POST['postal_code'] ?? '');
|
||||||
|
$is_default = isset($_POST['is_default']);
|
||||||
|
if (empty($province) || empty($city) || empty($address_line) || empty($postal_code)) {
|
||||||
|
throw new Exception('لطفاً تمام فیلدهای آدرس را پر کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
if ($is_default) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE user_addresses SET is_default = 0 WHERE user_id = ?");
|
||||||
|
$stmt->execute([$user_id]);
|
||||||
|
}
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO user_addresses (user_id, province, city, address_line, postal_code, is_default) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$user_id, $province, $city, $address_line, $postal_code, $is_default ? 1 : 0]);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$_SESSION['profile_message'] = 'آدرس جدید با موفقیت اضافه شد.';
|
||||||
|
$_SESSION['profile_message_type'] = 'success';
|
||||||
|
$redirect_page = 'addresses';
|
||||||
|
} elseif ($action === 'delete_address') {
|
||||||
|
$address_id = $_POST['address_id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM user_addresses WHERE id = ? AND user_id = ?");
|
||||||
|
if (!$stmt->execute([$address_id, $user_id])) throw new Exception('خطا در حذف آدرس.');
|
||||||
|
|
||||||
|
$_SESSION['profile_message'] = 'آدرس با موفقیت حذف شد.';
|
||||||
|
$_SESSION['profile_message_type'] = 'success';
|
||||||
|
$redirect_page = 'addresses';
|
||||||
|
} elseif ($action === 'set_default_address') {
|
||||||
|
$address_id = $_POST['address_id'] ?? 0;
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmt1 = $pdo->prepare("UPDATE user_addresses SET is_default = 0 WHERE user_id = ?");
|
||||||
|
$stmt1->execute([$user_id]);
|
||||||
|
$stmt2 = $pdo->prepare("UPDATE user_addresses SET is_default = 1 WHERE id = ? AND user_id = ?");
|
||||||
|
$stmt2->execute([$address_id, $user_id]);
|
||||||
|
$pdo->commit();
|
||||||
|
$_SESSION['profile_message'] = 'آدرس پیشفرض با موفقیت تغییر کرد.';
|
||||||
|
$_SESSION['profile_message_type'] = 'success';
|
||||||
|
$redirect_page = 'addresses';
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
if ($e->errorInfo[1] == 1062) { // Duplicate entry
|
||||||
|
$_SESSION['profile_message'] = 'این ایمیل قبلاً ثبت شده است.';
|
||||||
|
} else {
|
||||||
|
$_SESSION['profile_message'] = 'یک خطای پایگاه داده رخ داد: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
$_SESSION['profile_message_type'] = 'danger';
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$_SESSION['profile_message'] = $e->getMessage();
|
||||||
|
$_SESSION['profile_message_type'] = 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: profile.php?page=' . $redirect_page);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine current page
|
||||||
|
$page = $_GET['page'] ?? 'dashboard';
|
||||||
|
$page_map = [
|
||||||
|
'dashboard' => 'داشبورد',
|
||||||
|
'orders' => 'سفارشات من',
|
||||||
|
'addresses' => 'آدرسهای من',
|
||||||
|
'account' => 'جزئیات حساب',
|
||||||
|
];
|
||||||
|
$page_title = $page_map[$page] ?? 'حساب کاربری';
|
||||||
|
|
||||||
|
// Retrieve flash message
|
||||||
|
if (isset($_SESSION['profile_message'])) {
|
||||||
|
$flash_message = $_SESSION['profile_message'];
|
||||||
|
$flash_message_type = $_SESSION['profile_message_type'];
|
||||||
|
unset($_SESSION['profile_message']);
|
||||||
|
unset($_SESSION['profile_message_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all necessary data
|
||||||
|
$stmt_user = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt_user->execute([$user_id]);
|
||||||
|
$user = $stmt_user->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$stmt_orders = $pdo->prepare("SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC");
|
||||||
|
$stmt_orders->execute([$user_id]);
|
||||||
|
$orders = $stmt_orders->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$stmt_addresses = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY is_default DESC, id DESC");
|
||||||
|
$stmt_addresses->execute([$user_id]);
|
||||||
|
$addresses = $stmt_addresses->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$total_purchase_amount = array_reduce($orders, function ($sum, $order) {
|
||||||
|
return strtolower($order['status']) === 'completed' ? $sum + $order['total_amount'] : $sum;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= htmlspecialchars($page_title) ?> - پنل کاربری</title>
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.0.3/Vazirmatn-font-face.css">
|
||||||
|
<link rel="stylesheet" href="assets/css/theme.css?v=<?= time() ?>">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="admin/assets/css/admin_style.css?v=<?= time() ?>">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<style>
|
||||||
|
/* Minor adjustments for profile page to match admin styles */
|
||||||
|
.admin-main-content { background-color: var(--admin-bg); }
|
||||||
|
.table th, .table td { vertical-align: middle; }
|
||||||
|
.form-label { font-weight: 600; color: var(--admin-text-muted); }
|
||||||
|
.card-header h4 { margin: 0; font-size: 1.1rem; }
|
||||||
|
.order-status {
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 50rem;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.order-status.status-completed { background: var(--admin-success); color: #111; }
|
||||||
|
.order-status.status-pending { background: var(--admin-warning); color: #111; }
|
||||||
|
.order-status.status-shipped { background: var(--admin-info); color: #111; }
|
||||||
|
.order-status.status-cancelled { background: var(--admin-danger); color: #fff; }
|
||||||
|
|
||||||
|
.stat-cards-grid-reports {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.stat-card-report {
|
||||||
|
background-color: var(--admin-card-bg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
}
|
||||||
|
.stat-card-report p { margin-bottom: 0.5rem; color: var(--admin-text-muted); }
|
||||||
|
.stat-card-report h3 { margin: 0; font-size: 2rem; color: var(--admin-text); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="admin-body">
|
||||||
|
|
||||||
|
<div class="admin-wrapper">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="admin-sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h2><a href="index.php">آتیمه<span>.</span></a></h2>
|
||||||
|
</div>
|
||||||
|
<ul class="admin-nav">
|
||||||
|
<li>
|
||||||
|
<a class="admin-nav-link <?= ($page === 'dashboard') ? 'active' : '' ?>" href="profile.php?page=dashboard">
|
||||||
|
<i class="fas fa-tachometer-alt"></i><span>داشبورد</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="admin-nav-link <?= ($page === 'orders') ? 'active' : '' ?>" href="profile.php?page=orders">
|
||||||
|
<i class="fas fa-clipboard-list"></i><span>سفارشات من</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="admin-nav-link <?= ($page === 'addresses') ? 'active' : '' ?>" href="profile.php?page=addresses">
|
||||||
|
<i class="fas fa-map-marker-alt"></i><span>آدرسهای من</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="admin-nav-link <?= ($page === 'account') ? 'active' : '' ?>" href="profile.php?page=account">
|
||||||
|
<i class="fas fa-user-cog"></i><span>جزئیات حساب</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<a href="index.php"><i class="fas fa-home fa-fw"></i> <span>بازگشت به سایت</span></a>
|
||||||
|
<a href="logout.php"><i class="fas fa-sign-out-alt fa-fw"></i> <span>خروج از حساب</span></a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="admin-main-content">
|
||||||
|
<header class="admin-header-bar">
|
||||||
|
<button id="sidebar-toggle" class="btn d-lg-none">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<div class="admin-header-title">
|
||||||
|
<h1><?= htmlspecialchars($page_title) ?></h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<?php if (isset($flash_message)): ?>
|
||||||
|
<script>
|
||||||
|
Swal.fire({
|
||||||
|
title: '<?= ($flash_message_type === 'success') ? 'موفق' : 'خطا' ?>',
|
||||||
|
text: '<?= addslashes(htmlspecialchars($flash_message)) ?>',
|
||||||
|
icon: '<?= htmlspecialchars($flash_message_type) ?>',
|
||||||
|
confirmButtonText: 'باشه'
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($page === 'dashboard'): ?>
|
||||||
|
<div class="card mb-4" style="background-color: var(--admin-card-bg); border-color: var(--admin-border);">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3 style="color: var(--admin-text);">سلام، <?= htmlspecialchars($user['first_name'] ?? 'کاربر'); ?> عزیز!</h3>
|
||||||
|
<p class="text-muted">به پنل کاربری خود خوش آمدید. از اینجا میتوانید آخرین سفارشات خود را مشاهده کرده و حساب خود را مدیریت کنید.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-cards-grid-reports">
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>تعداد کل سفارشات</p>
|
||||||
|
<h3><?= count($orders); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card-report">
|
||||||
|
<p>مجموع خرید (تکمیل شده)</p>
|
||||||
|
<h3><?= number_format($total_purchase_amount); ?> تومان</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($page === 'orders'): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h4>تاریخچه سفارشات</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if (empty($orders)): ?>
|
||||||
|
<p class="text-center text-muted">شما هنوز هیچ سفارشی ثبت نکردهاید.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>مبلغ کل</th>
|
||||||
|
<th class="text-end">رهگیری</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($orders as $order): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= htmlspecialchars($order['id']); ?></td>
|
||||||
|
<td><?= jdate('d F Y', strtotime($order['created_at'])); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="order-status status-<?= strtolower(htmlspecialchars($order['status'])) ?>">
|
||||||
|
<?= htmlspecialchars($order['status']); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><?= number_format($order['total_amount']); ?> تومان</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a href="track_order.php?tracking_id=<?= htmlspecialchars($order['tracking_id']); ?>" class="btn btn-sm btn-outline-primary">نمایش</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($page === 'addresses'): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h4>آدرسهای من</h4>
|
||||||
|
<button class="btn btn-sm btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#add-address-form">
|
||||||
|
<i class="fas fa-plus"></i> افزودن آدرس
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="collapse mb-4" id="add-address-form">
|
||||||
|
<form method="POST" action="profile.php?page=addresses">
|
||||||
|
<input type="hidden" name="action" value="add_address">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">استان</label><input type="text" class="form-control" name="province" required></div>
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">شهر</label><input type="text" class="form-control" name="city" required></div>
|
||||||
|
<div class="col-md-4 mb-3"><label class="form-label">کد پستی</label><input type="text" class="form-control" name="postal_code" required></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3"><label class="form-label">آدرس کامل</label><textarea class="form-control" name="address_line" rows="2" required></textarea></div>
|
||||||
|
<div class="form-check mb-3"><input class="form-check-input" type="checkbox" name="is_default" id="is_default"><label class="form-check-label" for="is_default">انتخاب به عنوان آدرس پیشفرض</label></div>
|
||||||
|
<button type="submit" class="btn btn-success">ذخیره آدرس</button>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($addresses)): ?>
|
||||||
|
<p class="text-center text-muted">شما هنوز هیچ آدرسی ثبت نکردهاید.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($addresses as $address): ?>
|
||||||
|
<div class="d-flex justify-content-between align-items-center border-bottom py-2">
|
||||||
|
<div>
|
||||||
|
<p class="mb-0" style="color: var(--admin-text);"><?= htmlspecialchars(implode(', ', array_filter([$address['province'], $address['city'], $address['address_line'], "کدپستی: ".$address['postal_code']]))) ?></p>
|
||||||
|
<?php if ($address['is_default']): ?><span class="badge bg-primary">پیشفرض</span><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<?php if (!$address['is_default']): ?>
|
||||||
|
<form method="POST" action="profile.php?page=addresses" class="ms-2">
|
||||||
|
<input type="hidden" name="action" value="set_default_address">
|
||||||
|
<input type="hidden" name="address_id" value="<?= $address['id']; ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-secondary">پیشفرض</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" action="profile.php?page=addresses" onsubmit="return confirm('آیا از حذف این آدرس مطمئن هستید؟');">
|
||||||
|
<input type="hidden" name="action" value="delete_address">
|
||||||
|
<input type="hidden" name="address_id" value="<?= $address['id']; ?>">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">حذف</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($page === 'account'): ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h4>جزئیات حساب</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="profile.php?page=account">
|
||||||
|
<input type="hidden" name="action" value="update_details">
|
||||||
|
<div class="mb-3"><label class="form-label">نام</label><input type="text" class="form-control" name="first_name" value="<?= htmlspecialchars($user['first_name'] ?? ''); ?>" required></div>
|
||||||
|
<div class="mb-3"><label class="form-label">نام خانوادگی</label><input type="text" class="form-control" name="last_name" value="<?= htmlspecialchars($user['last_name'] ?? ''); ?>" required></div>
|
||||||
|
<div class="mb-3"><label class="form-label">آدرس ایمیل</label><input type="email" class="form-control" name="email" value="<?= htmlspecialchars($user['email'] ?? ''); ?>" required></div>
|
||||||
|
<button type="submit" class="btn btn-primary">ذخیره تغییرات</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h4>تغییر رمز عبور</h4></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="profile.php?page=account">
|
||||||
|
<input type="hidden" name="action" value="update_password">
|
||||||
|
<div class="mb-3"><label class="form-label">رمز عبور جدید</label><input type="password" class="form-control" name="new_password" required></div>
|
||||||
|
<div class="mb-3"><label class="form-label">تکرار رمز عبور جدید</label><input type="password" class="form-control" name="confirm_password" required></div>
|
||||||
|
<button type="submit" class="btn btn-primary">تغییر رمز عبور</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-danger">صفحه مورد نظر یافت نشد.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-backdrop"></div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const sidebar = document.querySelector('.admin-sidebar');
|
||||||
|
const backdrop = document.querySelector('.sidebar-backdrop');
|
||||||
|
const sidebarToggle = document.getElementById('sidebar-toggle');
|
||||||
|
|
||||||
|
if (sidebarToggle) {
|
||||||
|
sidebarToggle.addEventListener('click', () => {
|
||||||
|
sidebar.classList.toggle('open');
|
||||||
|
backdrop.classList.toggle('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backdrop) {
|
||||||
|
backdrop.addEventListener('click', () => {
|
||||||
|
sidebar.classList.remove('open');
|
||||||
|
backdrop.classList.remove('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
shop.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'فروشگاه';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Fetch all products from the database
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
$stmt = $pdo->query("SELECT * FROM products ORDER BY created_at DESC");
|
||||||
|
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Database error: " . $e->getMessage());
|
||||||
|
$products = [];
|
||||||
|
$db_error = "خطا در بارگذاری محصولات. لطفا بعدا تلاش کنید.";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="container section-padding">
|
||||||
|
<div class="text-center" data-aos="fade-down">
|
||||||
|
<h1 class="section-title">مجموعه کامل محصولات</h1>
|
||||||
|
<p class="fs-5 text-muted">دستسازههایی از چرم طبیعی، با عشق و دقت.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($db_error)): ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<?= $db_error; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (empty($products) && empty($db_error)): ?>
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-center text-muted fs-4">در حال حاضر محصولی برای نمایش وجود ندارد.</p>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4 g-lg-5 mt-5">
|
||||||
|
<?php
|
||||||
|
$delay = 0;
|
||||||
|
foreach ($products as $product):
|
||||||
|
?>
|
||||||
|
<div class="col" data-aos="fade-up" data-aos-delay="<?= $delay ?>">
|
||||||
|
<div class="card product-card h-100">
|
||||||
|
<div class="product-image">
|
||||||
|
<a href="product.php?id=<?= htmlspecialchars($product['id']) ?>">
|
||||||
|
<img src="<?= htmlspecialchars($product['image_url']) ?>" alt="<?= htmlspecialchars($product['name']) ?>">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="product-info text-center">
|
||||||
|
<h3 class="product-title">
|
||||||
|
<a href="product.php?id=<?= htmlspecialchars($product['id']) ?>">
|
||||||
|
<?= htmlspecialchars($product['name']) ?>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<p class="product-price"><?= number_format($product['price']) ?> تومان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
$delay = ($delay + 100) % 400; // Stagger animation delay
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
53
terms.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = 'قوانین و مقررات';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<main class="container py-5 my-5">
|
||||||
|
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||||
|
<h1>قوانین و مقررات</h1>
|
||||||
|
<p class="fs-5 text-muted">لطفاً پیش از استفاده از خدمات ما، این موارد را به دقت مطالعه فرمایید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-10 col-xl-8">
|
||||||
|
<div class="terms-content">
|
||||||
|
<div class="card mb-4" data-aos="fade-up">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h3 class="fw-bold mb-3">۱. تعاریف و کلیات</h3>
|
||||||
|
<p class="lh-lg">لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی میباشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-4" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h3 class="fw-bold mb-3">۲. شرایط استفاده از حساب کاربری</h3>
|
||||||
|
<p class="lh-lg">کاربران متعهد میشوند که اطلاعات خود را به درستی وارد کرده و در حفظ امنیت حساب کاربری خود کوشا باشند. هرگونه فعالیت از طریق حساب کاربری، به منزله فعالیت شخص کاربر تلقی خواهد شد. در این صورت دنیای جدیدی از تحلیلهای متنی و پردازش زبان طبیعی پدیدار خواهد شد.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h3 class="fw-bold mb-3">۳. حریم خصوصی</h3>
|
||||||
|
<p class="lh-lg">ما به حریم خصوصی شما احترام میگذاریم. اطلاعات شما نزد ما محفوظ است و تحت هیچ شرایطی در اختیار اشخاص ثالث قرار نخواهد گرفت، مگر با حکم قضایی. برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی میباشد.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h3 class="fw-bold mb-3">۴. مالکیت معنوی</h3>
|
||||||
|
<p class="lh-lg">کلیه محتوای این وبسایت، از جمله متون، طرحها، لوگوها و تصاویر، متعلق به فروشگاه آتیمه بوده و هرگونه کپیبرداری و استفاده تجاری بدون کسب اجازه کتبی، پیگرد قانونی خواهد داشت.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
<h3 class="fw-bold mb-3">۵. قوانین بازگشت کالا</h3>
|
||||||
|
<p class="lh-lg">رضایت شما اولویت ماست. شرایط بازگشت کالا و رویههای مربوط به آن به طور کامل در صفحه "سوالات متداول" شرح داده شده است. لطفاً پیش از خرید، این بخش را مطالعه فرمایید. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
249
track_order.php
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<?php
|
||||||
|
$page_title = "پیگیری سفارش";
|
||||||
|
include 'includes/header.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container section-padding">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8 col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-5">
|
||||||
|
<h1 class="text-center"><i class="ri-search-eye-line me-2"></i>پیگیری سفارش</h1>
|
||||||
|
<p class="text-center text-muted">کد رهگیری سفارش خود را برای مشاهده جزئیات وارد کنید.</p>
|
||||||
|
|
||||||
|
<form id="track-order-form" class="mt-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="text" id="tracking_id" name="tracking_id" class="form-control form-control-lg" placeholder="کد رهگیری سفارش" required>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg"><i class="ri-search-line me-2"></i>جستجو</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="result-message" class="mt-4 text-center"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Tracking Modal -->
|
||||||
|
<div class="tracking-modal-container" id="tracking-modal">
|
||||||
|
<div class="modal-overlay"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>جزئیات سفارش <span id="modal-order-id"></span></h3>
|
||||||
|
<button class="modal-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="order-summary">
|
||||||
|
<div class="detail-item"><strong>تاریخ ثبت:</strong> <span id="modal-order-date"></span></div>
|
||||||
|
<div class="detail-item"><strong>مبلغ کل:</strong> <span id="modal-order-amount"></span></div>
|
||||||
|
<div class="detail-item"><strong>تخفیف:</strong> <span id="modal-order-discount"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<h4>وضعیت سفارش: <span id="modal-order-status-text" style="font-weight: bold;"></span></h4>
|
||||||
|
<div class="status-tracker" id="modal-status-tracker">
|
||||||
|
<div class="status-progress"></div>
|
||||||
|
<div class="status-step" data-status="placed">
|
||||||
|
<div class="dot"></div><span class="label">ثبت سفارش</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-step" data-status="processing">
|
||||||
|
<div class="dot"></div><span class="label">در حال پردازش</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-step" data-status="shipped">
|
||||||
|
<div class="dot"></div><span class="label">ارسال شده</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-step" data-status="completed">
|
||||||
|
<div class="dot"></div><span class="label">تحویل شده</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shipping-details">
|
||||||
|
<h4>اطلاعات ارسال</h4>
|
||||||
|
<div class="detail-item"><strong>تحویل گیرنده:</strong> <span id="modal-shipping-name"></span></div>
|
||||||
|
<div class="detail-item"><strong>آدرس:</strong> <span id="modal-shipping-address"></span></div>
|
||||||
|
<div class="detail-item"><strong>کدپستی:</strong> <span id="modal-shipping-postal-code"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="products-list">
|
||||||
|
<h4>محصولات سفارش</h4>
|
||||||
|
<div id="modal-products-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const form = document.getElementById('track-order-form');
|
||||||
|
const modal = document.getElementById('tracking-modal');
|
||||||
|
const overlay = document.querySelector('.modal-overlay');
|
||||||
|
const closeBtn = document.querySelector('.modal-close-btn');
|
||||||
|
const resultMessage = document.getElementById('result-message');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const trackingId = document.getElementById('tracking_id').value;
|
||||||
|
|
||||||
|
resultMessage.innerHTML = `<div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">Loading...</span></div> در حال جستجو...`;
|
||||||
|
resultMessage.className = 'text-center text-muted';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/get_order_details.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tracking_id: trackingId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
resultMessage.innerHTML = '';
|
||||||
|
displayOrderDetails(data.order, data.products);
|
||||||
|
modal.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
resultMessage.innerHTML = data.message;
|
||||||
|
resultMessage.className = 'text-center text-danger fw-bold';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch Error:', error);
|
||||||
|
resultMessage.innerHTML = 'خطا در برقراری ارتباط با سرور. لطفاً اتصال اینترنت خود را بررسی کنید.';
|
||||||
|
resultMessage.className = 'text-center text-danger fw-bold';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function displayOrderDetails(order, products) {
|
||||||
|
document.getElementById('modal-order-id').textContent = '#' + order.id;
|
||||||
|
document.getElementById('modal-order-date').textContent = order.order_date;
|
||||||
|
document.getElementById('modal-order-amount').textContent = order.total_amount;
|
||||||
|
document.getElementById('modal-order-discount').textContent = order.discount_amount;
|
||||||
|
|
||||||
|
document.getElementById('modal-shipping-name').textContent = order.shipping_name;
|
||||||
|
document.getElementById('modal-shipping-address').textContent = order.shipping_address;
|
||||||
|
document.getElementById('modal-shipping-postal-code').textContent = order.shipping_postal_code;
|
||||||
|
|
||||||
|
const productsContainer = document.getElementById('modal-products-list');
|
||||||
|
productsContainer.innerHTML = '';
|
||||||
|
if (products && products.length > 0) {
|
||||||
|
products.forEach(p => {
|
||||||
|
const imageUrl = p.image_url ? p.image_url : 'assets/images/placeholder.png';
|
||||||
|
productsContainer.innerHTML += `
|
||||||
|
<div class="product-item">
|
||||||
|
<img src="${imageUrl}" alt="${p.name}" onerror="this.onerror=null;this.src='assets/images/placeholder.png';">
|
||||||
|
<div class="product-info">
|
||||||
|
<span class="product-name">${p.name}</span>
|
||||||
|
<div class="product-meta">
|
||||||
|
<span class="product-quantity">تعداد: ${p.quantity}</span>
|
||||||
|
${p.color ? `
|
||||||
|
<span class="product-color-wrapper">
|
||||||
|
رنگ: <span class="product-color-dot" style="background-color: ${p.color};"></span>
|
||||||
|
</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-price">${p.price}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
productsContainer.innerHTML = '<p class="text-center text-muted">محصولی برای این سفارش یافت نشد.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatusTracker(order.status, order.status_persian);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatusTracker(status, statusPersian) {
|
||||||
|
console.log('--- Debugging Status ---');
|
||||||
|
console.log('Received status:', status);
|
||||||
|
|
||||||
|
const statusTextEl = document.getElementById('modal-order-status-text');
|
||||||
|
const tracker = document.getElementById('modal-status-tracker');
|
||||||
|
const progress = tracker.querySelector('.status-progress');
|
||||||
|
const steps = Array.from(tracker.querySelectorAll('.status-step'));
|
||||||
|
|
||||||
|
// 1. Reset all dynamic styles and classes
|
||||||
|
steps.forEach(step => {
|
||||||
|
step.classList.remove('active', 'completed');
|
||||||
|
const dot = step.querySelector('.dot');
|
||||||
|
if (dot) dot.style.backgroundColor = ''; // Reset to default CSS color
|
||||||
|
});
|
||||||
|
tracker.classList.remove('is-cancelled');
|
||||||
|
progress.style.width = '0%';
|
||||||
|
progress.style.backgroundColor = ''; // Reset to default CSS color
|
||||||
|
|
||||||
|
// 2. Map API status to internal status keys
|
||||||
|
const statusKeyMap = {
|
||||||
|
'pending': 'placed',
|
||||||
|
'processing': 'processing',
|
||||||
|
'shipped': 'shipped',
|
||||||
|
'delivered': 'completed',
|
||||||
|
'completed': 'completed',
|
||||||
|
'cancelled': 'cancelled'
|
||||||
|
};
|
||||||
|
const mappedStatus = status ? statusKeyMap[status.toLowerCase()] : 'placed';
|
||||||
|
|
||||||
|
// 3. Define display properties for each status, using CSS variables
|
||||||
|
const statusDisplayMap = {
|
||||||
|
'placed': { text: 'ثبت شده', colorVar: '--status-default-dark', progress: '0%', stepIndex: 0 },
|
||||||
|
'processing': { text: 'در حال پردازش', colorVar: '--status-processing', progress: '33%', stepIndex: 1 },
|
||||||
|
'shipped': { text: 'ارسال شده', colorVar: '--status-shipped', progress: '66%', stepIndex: 2 },
|
||||||
|
'completed': { text: 'تحویل شده', colorVar: '--status-completed', progress: '100%', stepIndex: 3 },
|
||||||
|
'cancelled': { text: 'لغو شده', colorVar: '--status-cancelled', progress: '0%', stepIndex: -1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayInfo = statusDisplayMap[mappedStatus] || statusDisplayMap['placed'];
|
||||||
|
const currentStatusColor = `var(${displayInfo.colorVar})`;
|
||||||
|
const completedColor = `var(${statusDisplayMap['completed'].colorVar})`;
|
||||||
|
const cancelledColor = `var(${statusDisplayMap['cancelled'].colorVar})`;
|
||||||
|
|
||||||
|
console.log(`Mapped status: ${mappedStatus}, Index: ${displayInfo.stepIndex}`);
|
||||||
|
|
||||||
|
// 4. Update main status text color and content
|
||||||
|
statusTextEl.textContent = statusPersian || displayInfo.text;
|
||||||
|
statusTextEl.style.color = currentStatusColor;
|
||||||
|
|
||||||
|
// 5. Handle the special 'cancelled' state
|
||||||
|
if (mappedStatus === 'cancelled') {
|
||||||
|
tracker.classList.add('is-cancelled');
|
||||||
|
progress.style.backgroundColor = cancelledColor;
|
||||||
|
progress.style.width = '100%';
|
||||||
|
steps.forEach(s => {
|
||||||
|
const dot = s.querySelector('.dot');
|
||||||
|
if (dot) dot.style.backgroundColor = cancelledColor;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 6. Handle normal order progression
|
||||||
|
progress.style.backgroundColor = completedColor; // Progress bar is always green for consistency
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
progress.style.width = displayInfo.progress;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Update step classes and dot colors
|
||||||
|
if (displayInfo.stepIndex >= 0) {
|
||||||
|
// Mark all past steps as completed (green)
|
||||||
|
for (let i = 0; i < displayInfo.stepIndex; i++) {
|
||||||
|
if (steps[i]) {
|
||||||
|
steps[i].classList.add('completed');
|
||||||
|
const dot = steps[i].querySelector('.dot');
|
||||||
|
if (dot) dot.style.backgroundColor = completedColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mark current step as active (yellow, blue, or green)
|
||||||
|
if (steps[displayInfo.stepIndex]) {
|
||||||
|
steps[displayInfo.stepIndex].classList.add('active');
|
||||||
|
const dot = steps[displayInfo.stepIndex].querySelector('.dot');
|
||||||
|
if (dot) dot.style.backgroundColor = currentStatusColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modal.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBtn.addEventListener('click', closeModal);
|
||||||
|
overlay.addEventListener('click', closeModal);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php include 'includes/footer.php'; ?>
|
||||||