Compare commits
10 Commits
a1d3188d59
...
c225ebebc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c225ebebc1 | ||
|
|
8a6a18d0c2 | ||
|
|
f28a0493a0 | ||
|
|
41e2f42e41 | ||
|
|
f7f8238fee | ||
|
|
7da45b4e24 | ||
|
|
6c608b6ba5 | ||
|
|
3f48850ff5 | ||
|
|
20dd5c8f61 | ||
|
|
f745cb0f27 |
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"}
|
||||||
@ -1,81 +1,90 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require_once __DIR__ . '/auth_check.php';
|
require_once __DIR__ . '/auth_check.php';
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
|
|
||||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
if ($flash_message) {
|
if ($flash_message) {
|
||||||
unset($_SESSION['flash_message']);
|
unset($_SESSION['flash_message']);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fa" dir="rtl">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>افزودن محصول جدید</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
<!-- SweetAlert2 -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-dark text-white">
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
<style>
|
||||||
<div class="row justify-content-center">
|
.form-grid {
|
||||||
<div class="col-md-8">
|
display: grid;
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
<h1 class="font-lalezar">افزودن محصول جدید</h1>
|
gap: 1.5rem;
|
||||||
<a href="index.php" class="btn btn-outline-light">بازگشت</a>
|
}
|
||||||
|
</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>
|
||||||
<div class="card bg-dark-2">
|
|
||||||
<div class="card-body p-4">
|
<div class="form-group">
|
||||||
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
|
<label for="description" class="form-label">توضیحات</label>
|
||||||
<div class="mb-3">
|
<textarea class="form-control" id="description" name="description" rows="5" required></textarea>
|
||||||
<label for="name" class="form-label">نام محصول</label>
|
</div>
|
||||||
<input type="text" class="form-control bg-dark text-white" id="name" name="name" required>
|
|
||||||
</div>
|
<div class="form-grid">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="description" class="form-label">توضیحات</label>
|
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||||
<textarea class="form-control bg-dark text-white" id="description" name="description" rows="3" required></textarea>
|
<input type="number" class="form-control" id="price" name="price" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="price" class="form-label">قیمت (تومان)</label>
|
<label for="stock" class="form-label">موجودی</label>
|
||||||
<input type="number" class="form-control bg-dark text-white" id="price" name="price" required>
|
<input type="number" class="form-control" id="stock" name="stock" required value="0">
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="image" class="form-label">تصویر محصول</label>
|
|
||||||
<input type="file" class="form-control bg-dark text-white" id="image" name="image" accept="image/*" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
|
|
||||||
<input type="text" class="form-control bg-dark text-white" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
|
|
||||||
<div class="form-text">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="is_featured" name="is_featured" value="1">
|
|
||||||
<label class="form-check-label" for="is_featured">محصول ویژه</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-100">افزودن محصول</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const style = getComputedStyle(document.body);
|
||||||
<?php if ($flash_message): ?>
|
<?php if ($flash_message): ?>
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
|
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
|
||||||
html: '<?php echo addslashes($flash_message["message"]); ?>', // Use html to render <br> tags
|
html: '<?php echo addslashes($flash_message["message"]); ?>',
|
||||||
icon: '<?php echo $flash_message["type"]; ?>',
|
icon: '<?php echo $flash_message["type"]; ?>',
|
||||||
confirmButtonText: 'باشه'
|
confirmButtonText: 'باشه',
|
||||||
|
background: style.getPropertyValue('--admin-surface'),
|
||||||
|
color: style.getPropertyValue('--admin-text')
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
|
||||||
</html>
|
<?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;
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
require_once __DIR__ . '/auth_handler.php';
|
||||||
|
|
||||||
// Check if the user is logged in. If not, redirect to the login page.
|
// Check if the user is logged in. If not, redirect to the login page.
|
||||||
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
|
if (!is_admin()) {
|
||||||
header('Location: login.php');
|
header('Location: login.php');
|
||||||
exit;
|
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';
|
||||||
|
?>
|
||||||
@ -3,15 +3,10 @@ session_start();
|
|||||||
require_once __DIR__ . '/auth_check.php';
|
require_once __DIR__ . '/auth_check.php';
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||||
if ($flash_message) {
|
if (!$product_id) {
|
||||||
unset($_SESSION['flash_message']);
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'شناسه محصول نامعتبر است.'];
|
||||||
}
|
header('Location: products.php');
|
||||||
|
|
||||||
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
||||||
|
|
||||||
if ($product_id <= 0) {
|
|
||||||
header('Location: index.php');
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,90 +15,113 @@ try {
|
|||||||
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
||||||
$stmt->execute([$product_id]);
|
$stmt->execute([$product_id]);
|
||||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$product) {
|
if (!$product) {
|
||||||
header('Location: index.php');
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'محصول مورد نظر یافت نشد.'];
|
||||||
|
header('Location: products.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
die("Error fetching product: " . $e->getMessage());
|
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطا در اتصال به پایگاه داده.'];
|
||||||
|
header('Location: products.php');
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/header.php';
|
||||||
?>
|
?>
|
||||||
<!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($product['name']); ?></title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
<!-- SweetAlert2 -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-dark text-white">
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
<style>
|
||||||
<div class="row justify-content-center">
|
.form-grid {
|
||||||
<div class="col-md-8">
|
display: grid;
|
||||||
<h1 class="font-lalezar mb-4">ویرایش محصول</h1>
|
grid-template-columns: 2fr 1fr;
|
||||||
<div class="card bg-dark-2">
|
gap: 2rem;
|
||||||
<div class="card-body p-4">
|
}
|
||||||
<form action="handler.php?action=edit" method="POST" enctype="multipart/form-data">
|
.image-preview-container {
|
||||||
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
|
background-color: var(--admin-bg);
|
||||||
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
|
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="mb-3">
|
<div class="admin-header">
|
||||||
<label for="name" class="form-label">نام محصول</label>
|
<h1>ویرایش محصول: <?php echo htmlspecialchars($product['name']); ?></h1>
|
||||||
<input type="text" class="form-control bg-dark text-white" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="description" class="form-label">توضیحات</label>
|
<form action="handler.php?action=edit" method="post" enctype="multipart/form-data">
|
||||||
<textarea class="form-control bg-dark text-white" id="description" name="description" rows="3" required><?php echo htmlspecialchars($product['description']); ?></textarea>
|
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
|
||||||
</div>
|
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
|
||||||
<div class="mb-3">
|
|
||||||
<label for="price" class="form-label">قیمت (به تومان)</label>
|
<div class="form-grid">
|
||||||
<input type="number" class="form-control bg-dark text-white" id="price" name="price" min="0" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
<div class="card">
|
||||||
</div>
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="colors" class="form-label">رنگها</label>
|
<label for="name" class="form-label">نام محصول</label>
|
||||||
<input type="text" class="form-control bg-dark text-white" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>">
|
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||||
<div class="form-text">رنگهای موجود را با کاما از هم جدا کنید (مثال: #FFFFFF, #000000).</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="image" class="form-label">تصویر محصول</label>
|
<label for="description" class="form-label">توضیحات</label>
|
||||||
<input type="file" class="form-control bg-dark text-white" id="image" name="image" accept="image/*">
|
<textarea class="form-control" id="description" name="description" rows="5" required><?php echo htmlspecialchars($product['description']); ?></textarea>
|
||||||
<div class="form-text mt-2">تصویر فعلی:</div>
|
</div>
|
||||||
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" class="img-thumbnail mt-2" width="100">
|
|
||||||
</div>
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||||
<div class="mb-3 form-check">
|
<div class="form-group">
|
||||||
<input type="checkbox" class="form-check-input" id="is_featured" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?>>
|
<label for="price" class="form-label">قیمت (تومان)</label>
|
||||||
<label class="form-check-label" for="is_featured">نمایش در محصولات ویژه</label>
|
<input type="number" class="form-control" id="price" name="price" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-end gap-2 mt-4">
|
<div class="form-group">
|
||||||
<a href="index.php" class="btn btn-secondary">انصراف</a>
|
<label for="stock" class="form-label">موجودی</label>
|
||||||
<button type="submit" class="btn btn-primary">بهروزرسانی محصول</button>
|
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<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>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
function previewImage(event) {
|
||||||
<?php if ($flash_message): ?>
|
const reader = new FileReader();
|
||||||
Swal.fire({
|
reader.onload = () => document.getElementById('image-preview').src = reader.result;
|
||||||
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
|
if (event.target.files[0]) reader.readAsDataURL(event.target.files[0]);
|
||||||
html: '<?php echo addslashes($flash_message["message"]); ?>', // Use html to render <br> tags
|
}
|
||||||
icon: '<?php echo $flash_message["type"]; ?>',
|
|
||||||
confirmButtonText: 'باشه'
|
|
||||||
});
|
|
||||||
<?php endif; ?>
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
|
||||||
</html>
|
<?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>
|
||||||
@ -1,169 +1,77 @@
|
|||||||
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
require_once __DIR__ . '/auth_check.php';
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
require_once __DIR__ . '/auth_check.php';
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: index.php');
|
||||||
$action = $_REQUEST['action'] ?? '';
|
exit;
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Default redirect location
|
|
||||||
$redirect_to = 'index.php';
|
|
||||||
|
|
||||||
switch ($action) {
|
|
||||||
case 'add':
|
|
||||||
$redirect_to = 'add_product.php'; // Redirect back to form on error
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$name = trim($_POST['name'] ?? '');
|
|
||||||
$description = trim($_POST['description'] ?? '');
|
|
||||||
$price = filter_var($_POST['price'], FILTER_VALIDATE_FLOAT);
|
|
||||||
$colors = trim($_POST['colors'] ?? '');
|
|
||||||
$is_featured = isset($_POST['is_featured']) ? 1 : 0;
|
|
||||||
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (empty($name)) $errors[] = "Product name is required.";
|
|
||||||
if (empty($description)) $errors[] = "Description is required.";
|
|
||||||
if ($price === false) $errors[] = "Price is invalid or missing.";
|
|
||||||
|
|
||||||
$image_path = '';
|
|
||||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
|
||||||
$upload_dir = __DIR__ . '/../assets/images/products/';
|
|
||||||
if (!is_dir($upload_dir)) {
|
|
||||||
if (!mkdir($upload_dir, 0777, true)) {
|
|
||||||
$errors[] = "Image directory does not exist and could not be created.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_writable($upload_dir)) {
|
|
||||||
$errors[] = "Image directory is not writable. Please check server permissions.";
|
|
||||||
} else {
|
|
||||||
$filename = uniqid('product_', true) . '_' . basename($_FILES['image']['name']);
|
|
||||||
$target_file = $upload_dir . $filename;
|
|
||||||
|
|
||||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
|
|
||||||
$image_path = 'assets/images/products/' . $filename;
|
|
||||||
} else {
|
|
||||||
$errors[] = "Failed to move uploaded file.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$file_error = $_FILES['image']['error'] ?? UPLOAD_ERR_NO_FILE;
|
|
||||||
$upload_errors = [
|
|
||||||
UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the server's maximum upload size (upload_max_filesize).",
|
|
||||||
UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the maximum size specified in the form.",
|
|
||||||
UPLOAD_ERR_PARTIAL => "The file was only partially uploaded.",
|
|
||||||
UPLOAD_ERR_NO_FILE => "No file was selected for upload.",
|
|
||||||
UPLOAD_ERR_NO_TMP_DIR => "Server configuration error: Missing a temporary folder for uploads.",
|
|
||||||
UPLOAD_ERR_CANT_WRITE => "Server error: Failed to write the uploaded file to disk.",
|
|
||||||
UPLOAD_ERR_EXTENSION => "A PHP extension prevented the file upload.",
|
|
||||||
];
|
|
||||||
$error_message = $upload_errors[$file_error] ?? "An unknown upload error occurred (Code: {$file_error}).";
|
|
||||||
// Only trigger error if the action is 'add', where image is mandatory
|
|
||||||
if ($action === 'add') {
|
|
||||||
$errors[] = "Image Upload Failed: " . $error_message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($errors)) {
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO products (name, description, price, image_url, colors, is_featured) VALUES (?, ?, ?, ?, ?, ?)";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$name, $description, $price, $image_path, $colors, $is_featured]);
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت اضافه شد!'];
|
|
||||||
$redirect_to = 'index.php';
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'خطا در افزودن محصول: ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error_message = 'لطفاً تمام خطاها را برطرف کنید:<br><br>' . implode('<br>', $errors);
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => $error_message];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'edit':
|
|
||||||
$id = $_POST['id'] ?? $_GET['id'] ?? null;
|
|
||||||
$redirect_to = 'edit_product.php?id=' . $id;
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$id = filter_var($id, FILTER_VALIDATE_INT);
|
|
||||||
$name = trim($_POST['name'] ?? '');
|
|
||||||
$description = trim($_POST['description'] ?? '');
|
|
||||||
$price = filter_var($_POST['price'], FILTER_VALIDATE_FLOAT);
|
|
||||||
$colors = trim($_POST['colors'] ?? '');
|
|
||||||
$is_featured = isset($_POST['is_featured']) ? 1 : 0;
|
|
||||||
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
$errors[] = "شناسه محصول نامعتبر است.";
|
|
||||||
}
|
|
||||||
// Other validations...
|
|
||||||
|
|
||||||
$image_path = $_POST['current_image'] ?? '';
|
|
||||||
|
|
||||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
|
||||||
$upload_dir = __DIR__ . '/../assets/images/products/';
|
|
||||||
$filename = uniqid('product_', true) . '_' . basename($_FILES['image']['name']);
|
|
||||||
$target_file = $upload_dir . $filename;
|
|
||||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
|
|
||||||
if (!empty($image_path) && file_exists(__DIR__ . '/../' . $image_path)) {
|
|
||||||
unlink(__DIR__ . '/../' . $image_path);
|
|
||||||
}
|
|
||||||
$image_path = 'assets/images/products/' . $filename;
|
|
||||||
} else {
|
|
||||||
$errors[] = "خطا در آپلود تصویر جدید.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($errors)) {
|
|
||||||
try {
|
|
||||||
$sql = "UPDATE products SET name = ?, description = ?, price = ?, image_url = ?, colors = ?, is_featured = ? WHERE id = ?";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$name, $description, $price, $image_path, $colors, $is_featured, $id]);
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت ویرایش شد!'];
|
|
||||||
$redirect_to = 'index.php';
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'خطا در ویرایش محصول: ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error_message = 'فرم دارای خطا است:<br><br>' . implode('<br>', $errors);
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => $error_message];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
$id = filter_var($_GET['id'], FILTER_VALIDATE_INT);
|
|
||||||
if ($id) {
|
|
||||||
try {
|
|
||||||
// First, get the image path to delete the file
|
|
||||||
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$image_to_delete = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
// Delete the record
|
|
||||||
$sql = "DELETE FROM products WHERE id = ?";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
|
|
||||||
// If record deleted, delete the file
|
|
||||||
if ($stmt->rowCount() > 0 && $image_to_delete && file_exists(__DIR__ . '/../' . $image_to_delete)) {
|
|
||||||
unlink(__DIR__ . '/../' . $image_to_delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'محصول با موفقیت حذف شد.'];
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'خطا در حذف محصول: ' . $e->getMessage()];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'شناسه محصول نامعتبر است.'];
|
|
||||||
}
|
|
||||||
$redirect_to = 'index.php';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect back after the action
|
$action = $_POST['action'] ?? '';
|
||||||
header('Location: ' . $redirect_to);
|
|
||||||
|
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;
|
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>
|
||||||
192
admin/index.php
@ -3,12 +3,35 @@ session_start();
|
|||||||
require_once __DIR__ . '/auth_check.php';
|
require_once __DIR__ . '/auth_check.php';
|
||||||
require_once __DIR__ . '/../db/config.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 {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->query("SELECT id, name, price FROM products ORDER BY created_at DESC");
|
$total_products = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
|
||||||
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$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) {
|
} catch (PDOException $e) {
|
||||||
die("Error fetching products: " . $e->getMessage());
|
$dashboard_error = "<strong>خطا در بارگذاری اطلاعات:</strong> " . $e->getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
$flash_message = $_SESSION['flash_message'] ?? null;
|
$flash_message = $_SESSION['flash_message'] ?? null;
|
||||||
@ -16,102 +39,87 @@ if ($flash_message) {
|
|||||||
unset($_SESSION['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';
|
||||||
|
}
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fa" dir="rtl">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>پنل مدیریت - محصولات</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
<!-- SweetAlert2 -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-dark text-white">
|
|
||||||
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h1 class="font-lalezar">مدیریت محصولات</h1>
|
<?php if ($flash_message): ?>
|
||||||
<div class="d-flex gap-2">
|
<script>
|
||||||
<a href="add_product.php" class="btn btn-success">+ افزودن محصول جدید</a>
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
<a href="logout.php" class="btn btn-outline-danger">خروج</a>
|
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>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="card">
|
||||||
<table class="table table-dark table-striped table-hover">
|
<div class="card-header">آخرین سفارشات</div>
|
||||||
<thead>
|
<div class="card-body">
|
||||||
<tr>
|
<table class="table">
|
||||||
<th scope="col">#</th>
|
<thead>
|
||||||
<th scope="col">نام محصول</th>
|
|
||||||
<th scope="col">قیمت</th>
|
|
||||||
<th scope="col">عملیات</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($products)): ?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-center">هیچ محصولی یافت نشد.</td>
|
<th>شماره سفارش</th>
|
||||||
|
<th>نام مشتری</th>
|
||||||
|
<th>مبلغ کل</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>تاریخ</th>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
</thead>
|
||||||
<?php foreach ($products as $product): ?>
|
<tbody>
|
||||||
<tr>
|
<?php if (empty($recent_orders)): ?>
|
||||||
<th scope="row"><?php echo htmlspecialchars($product['id']); ?></th>
|
<tr><td colspan="5" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
|
||||||
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
<?php else: ?>
|
||||||
<td><?php echo number_format($product['price']); ?> تومان</td>
|
<?php foreach ($recent_orders as $order): ?>
|
||||||
<td>
|
<tr>
|
||||||
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-primary">ویرایش</a>
|
<td>#<?php echo htmlspecialchars($order['id']); ?></td>
|
||||||
<a href="handler.php?action=delete&id=<?php echo $product['id']; ?>" class="btn btn-sm btn-danger delete-btn">حذف</a>
|
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
|
||||||
</td>
|
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
|
||||||
</tr>
|
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
|
||||||
<?php endforeach; ?>
|
<td><?php echo date('Y-m-d', strtotime($order['created_at'])); ?></td>
|
||||||
<?php endif; ?>
|
</tr>
|
||||||
</tbody>
|
<?php endforeach; ?>
|
||||||
</table>
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
|
||||||
<a href="../index.php" class="btn btn-outline-light">بازگشت به سایت</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<?php endif; ?>
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// Flash message handling
|
|
||||||
<?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: 'باشه'
|
|
||||||
});
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
// Delete confirmation
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
const deleteButtons = document.querySelectorAll('.delete-btn');
|
|
||||||
deleteButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const href = this.getAttribute('href');
|
|
||||||
Swal.fire({
|
|
||||||
title: 'آیا مطمئن هستید؟',
|
|
||||||
text: "این عمل غیرقابل بازگشت است!",
|
|
||||||
icon: 'warning',
|
|
||||||
showCancelButton: true,
|
|
||||||
confirmButtonColor: '#d33',
|
|
||||||
cancelButtonColor: '#3085d6',
|
|
||||||
confirmButtonText: 'بله، حذف کن!',
|
|
||||||
cancelButtonText: 'انصراف'
|
|
||||||
}).then((result) => {
|
|
||||||
if (result.isConfirmed) {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
// If the user is already logged in, redirect them to the admin dashboard
|
|
||||||
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
|
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
@ -9,56 +8,62 @@ if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
|
|||||||
|
|
||||||
$error = '';
|
$error = '';
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
// WARNING: This is a highly insecure, hardcoded password for demonstration purposes only.
|
|
||||||
// In a real-world application, you MUST use a secure, hashed password system.
|
|
||||||
$hardcoded_password = 'admin123';
|
$hardcoded_password = 'admin123';
|
||||||
|
|
||||||
if (isset($_POST['password']) && $_POST['password'] === $hardcoded_password) {
|
if (isset($_POST['password']) && $_POST['password'] === $hardcoded_password) {
|
||||||
// On successful login, set a session variable
|
|
||||||
$_SESSION['is_admin'] = true;
|
$_SESSION['is_admin'] = true;
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$error = 'رمز عبور اشتباه است.';
|
$error = 'رمز عبور وارد شده اشتباه است.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$page_title = 'ورود به پنل مدیریت';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fa" dir="rtl">
|
<html lang="fa" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>ورود به پنل مدیریت</title>
|
<title><?= $page_title; ?></title>
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link rel="stylesheet" href="assets/css/admin_main.css?v=<?= time(); ?>">
|
||||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark text-white">
|
<body class="admin-theme">
|
||||||
|
|
||||||
<div class="container">
|
<div class="admin-login-wrapper">
|
||||||
<div class="row justify-content-center align-items-center" style="height: 100vh;">
|
<div class="admin-login-box">
|
||||||
<div class="col-md-4">
|
<h2>پنل مدیریت آتیمه</h2>
|
||||||
<div class="card bg-dark-2">
|
<p>برای دسترسی به پنل، لطفاً وارد شوید.</p>
|
||||||
<div class="card-body p-4">
|
|
||||||
<h1 class="font-lalezar text-center mb-4">ورود به پنل</h1>
|
<?php if ($error): ?>
|
||||||
<p class="text-center text-muted mb-4">رمز عبور: admin123</p>
|
<div class="alert alert-danger mb-3"><?= $error; ?></div>
|
||||||
<?php if ($error): ?>
|
<p class="text-center text-muted mb-4">رمز عبور پیشفرض: <code>admin123</code></p>
|
||||||
<div class="alert alert-danger"><?php echo $error; ?></div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
|
||||||
<form method="POST">
|
<form method="POST" action="login.php">
|
||||||
<div class="mb-3">
|
<div class="form-group">
|
||||||
<label for="password" class="form-label">رمز عبور</label>
|
<label for="password" class="form-label">رمز عبور</label>
|
||||||
<input type="password" class="form-control bg-dark text-white" id="password" name="password" required>
|
<input type="password" class="form-control" id="password" name="password" required autofocus>
|
||||||
</div>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button type="submit" class="btn btn-primary">ورود</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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>
|
||||||
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.']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,291 +1,579 @@
|
|||||||
/*
|
|
||||||
:root variables for the default Light Theme
|
|
||||||
*/
|
|
||||||
:root {
|
:root {
|
||||||
--primary-color: #8B4513; /* SaddleBrown */
|
--status-default: #444;
|
||||||
--secondary-color: #D2B48C; /* Tan */
|
--status-default-dark: #6c757d;
|
||||||
--accent-color: #C0A080;
|
--status-processing: #ffc107;
|
||||||
--background-color: #FDFBF7;
|
--status-shipped: #0d6efd;
|
||||||
--surface-color: #FFFFFF; /* For cards, headers, etc. */
|
--status-completed: #198754;
|
||||||
--text-color: #4A4A4A;
|
--status-cancelled: #dc3545;
|
||||||
--heading-color: #2F2F2F;
|
|
||||||
--border-color: #EAEAEA;
|
|
||||||
--footer-bg: #2C2C2C;
|
|
||||||
--white-color: #FFFFFF;
|
|
||||||
|
|
||||||
--font-family-sans-serif: 'Montserrat', sans-serif;
|
|
||||||
--body-font: 'Montserrat', sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.about-us-list {
|
||||||
Variables for the Dark Theme
|
width: 80vw;
|
||||||
We will apply these by adding a class="dark" to the <html> tag
|
display: grid;
|
||||||
*/
|
list-style: none;
|
||||||
html.dark {
|
|
||||||
--primary-color: #C0A080; /* Lighter tan for accents in dark mode */
|
|
||||||
--secondary-color: #8B4513; /* Darker brown */
|
|
||||||
--accent-color: #D2B48C;
|
|
||||||
--background-color: #1A1A1A; /* Very dark grey, almost black */
|
|
||||||
--surface-color: #2C2C2C; /* Dark grey for cards and surfaces */
|
|
||||||
--text-color: #D5D5D5; /* Light grey for body text */
|
|
||||||
--heading-color: #FFFFFF; /* White for headings */
|
|
||||||
--border-color: #444444; /* Grey for borders */
|
|
||||||
--footer-bg: #111111; /* Even darker for footer */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--body-font);
|
|
||||||
background-color: var(--background-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
line-height: 1.7;
|
|
||||||
font-weight: 400;
|
|
||||||
overflow-x: hidden;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--heading-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: var(--accent-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Buttons --- */
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: var(--background-color); /* To have contrast in both themes */
|
|
||||||
padding: 12px 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 50px;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
html.dark .btn-primary {
|
|
||||||
color: var(--heading-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.btn-primary:hover, .btn-primary:focus {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
border-color: var(--secondary-color);
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-primary {
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
color: var(--primary-color);
|
|
||||||
padding: 12px 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
border-radius: 50px;
|
|
||||||
}
|
|
||||||
.btn-outline-primary:hover {
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: var(--white-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Header --- */
|
|
||||||
.site-header {
|
|
||||||
background-color: var(--surface-color);
|
|
||||||
box-shadow: 0 2px 15px rgba(0,0,0,0.05);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .site-header {
|
|
||||||
box-shadow: 0 2px 15px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header .nav-link,
|
|
||||||
.site-header a {
|
|
||||||
color: var(--text-color) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header a.active,
|
|
||||||
.site-header a:hover {
|
|
||||||
color: var(--primary-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-header .badge {
|
|
||||||
background-color: var(--primary-color) !important;
|
|
||||||
color: var(--background-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .site-header .badge {
|
|
||||||
color: var(--heading-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* --- Footer --- */
|
|
||||||
.site-footer {
|
|
||||||
background-color: var(--footer-bg);
|
|
||||||
color: #AFAFAF;
|
|
||||||
}
|
|
||||||
.site-footer h5 {
|
|
||||||
color: var(--heading-color);
|
|
||||||
}
|
|
||||||
.site-footer a {
|
|
||||||
color: #AFAFAF !important;
|
|
||||||
}
|
|
||||||
.site-footer a:hover {
|
|
||||||
color: var(--white-color) !important;
|
|
||||||
}
|
|
||||||
.site-footer .border-top {
|
|
||||||
border-color: var(--border-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Product Card --- */
|
|
||||||
.product-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 15px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .product-card {
|
|
||||||
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
transform: translateY(-8px);
|
|
||||||
box-shadow: 0 15px 30px rgba(0,0,0,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .product-card:hover {
|
|
||||||
box-shadow: 0 15px 30px rgba(0,0,0,0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.product-card .product-image img {
|
|
||||||
transition: transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover .product-image img {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .product-info {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .product-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: var(--heading-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .product-price {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Product Page Color Swatches --- */
|
|
||||||
.color-swatches .btn-check + .btn {
|
|
||||||
border: 2px solid var(--border-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 0;
|
grid-template-columns: repeat(3, 1fr);
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
justify-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-swatches .btn-check:checked + .btn {
|
.about-us-item {
|
||||||
transform: scale(1.15);
|
width: 20vw;
|
||||||
box-shadow: 0 0 0 3px var(--surface-color), 0 0 0 5px var(--primary-color);
|
min-width: 200px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle specific colors that need border in light mode */
|
.inner {
|
||||||
.color-swatches [style*="#FFFFFF"] + label,
|
|
||||||
.color-swatches [style*="#ffffff"] + label {
|
|
||||||
border-color: #dedede;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .color-swatches [style*="#000000"] + label {
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Admin panel dark theme overrides --- */
|
|
||||||
.bg-dark {
|
|
||||||
background-color: var(--background-color) !important;
|
|
||||||
}
|
|
||||||
.text-white {
|
|
||||||
color: var(--text-color) !important;
|
|
||||||
}
|
|
||||||
.bg-dark-2 {
|
|
||||||
background-color: var(--surface-color) !important;
|
|
||||||
}
|
|
||||||
.form-control.bg-dark {
|
|
||||||
background-color: var(--background-color) !important;
|
|
||||||
color: var(--text-color) !important;
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
.form-control.bg-dark:focus {
|
|
||||||
background-color: var(--background-color) !important;
|
|
||||||
color: var(--text-color) !important;
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Product Image Aspect Ratio --- */
|
|
||||||
.product-image {
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--border-color); /* Placeholder bg */
|
inset: 0px;
|
||||||
aspect-ratio: 3 / 4; /* Enforce 3:4 aspect ratio */
|
overflow: hidden;
|
||||||
|
transition: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-image img {
|
.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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover; /* This will crop the image to fit */
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Responsive Design --- */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.display-3 {
|
footer {
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
.display-4 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
.display-5 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section {
|
|
||||||
height: auto; /* Adjust height for mobile */
|
|
||||||
padding: 100px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .product-info {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-footer {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-footer .col-lg-2, .site-footer .col-lg-3, .site-footer .col-lg-4 {
|
footer .row > * {
|
||||||
margin-bottom: 30px;
|
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 |
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -1 +1,30 @@
|
|||||||
// Custom JavaScript will go here
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
216
cart.php
@ -1,121 +1,111 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require_once 'db/config.php';
|
|
||||||
|
|
||||||
$cart_items_detailed = [];
|
|
||||||
$total_price = 0;
|
|
||||||
|
|
||||||
if (!empty($_SESSION['cart'])) {
|
|
||||||
$cart_item_ids = array_keys($_SESSION['cart']);
|
|
||||||
|
|
||||||
// Extract pure product IDs from the composite key (e.g., '1-Black' -> '1')
|
|
||||||
$product_ids = array_map(function($id) {
|
|
||||||
return (int)explode('-', $id)[0];
|
|
||||||
}, $cart_item_ids);
|
|
||||||
|
|
||||||
if (!empty($product_ids)) {
|
|
||||||
$placeholders = implode(',', array_fill(0, count($product_ids), '?'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$stmt = $pdo->prepare("SELECT id, name, price, image_url FROM products WHERE id IN ($placeholders)");
|
|
||||||
$stmt->execute(array_unique($product_ids));
|
|
||||||
$products_data = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
|
||||||
|
|
||||||
foreach ($_SESSION['cart'] as $cart_item_id => $item) {
|
|
||||||
$product_id = (int)explode('-', $cart_item_id)[0];
|
|
||||||
|
|
||||||
if (isset($products_data[$product_id])) {
|
|
||||||
$product = $products_data[$product_id];
|
|
||||||
$quantity = $item['quantity'];
|
|
||||||
$color = $item['color'];
|
|
||||||
$subtotal = $product['price'] * $quantity;
|
|
||||||
$total_price += $subtotal;
|
|
||||||
|
|
||||||
$cart_items_detailed[] = [
|
|
||||||
'cart_item_id' => $cart_item_id,
|
|
||||||
'product_id' => $product_id,
|
|
||||||
'name' => $product['name'],
|
|
||||||
'price' => $product['price'],
|
|
||||||
'image_url' => $product['image_url'],
|
|
||||||
'quantity' => $quantity,
|
|
||||||
'color' => $color,
|
|
||||||
'subtotal' => $subtotal
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
error_log("DB Error: " . $e->getMessage());
|
|
||||||
$cart_items_detailed = [];
|
|
||||||
$total_price = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$page_title = 'سبد خرید';
|
$page_title = 'سبد خرید';
|
||||||
include 'includes/header.php';
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
|
$cart_items = $_SESSION['cart'] ?? [];
|
||||||
|
$total_price = 0;
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="text-center mb-5">
|
<main>
|
||||||
<h1 class="display-4 fw-bold">سبد خرید شما</h1>
|
<section class="section-padding">
|
||||||
</div>
|
<div class="container">
|
||||||
|
|
||||||
<?php if (empty($cart_items_detailed)): ?>
|
<?php if (empty($cart_items)): ?>
|
||||||
<div class="text-center p-5 bg-light rounded-3">
|
<div class="card card-body text-center p-4 p-md-5" data-aos="fade-up">
|
||||||
<p class="lead">سبد خرید شما خالی است.</p>
|
<div class="d-inline-block mx-auto mb-4">
|
||||||
<a href="shop.php" class="btn btn-primary">بازگشت به فروشگاه</a>
|
<i class="ri-shopping-cart-2-line display-1 text-gold"></i>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<h2 class="mb-3">سبد خرید شما خالی است</h2>
|
||||||
<form action="cart_handler.php" method="POST">
|
<p class="text-muted fs-5 mb-4">به نظر میرسد هنوز محصولی به سبد خرید خود اضافه نکردهاید. همین حالا گشتی در فروشگاه بزنید.</p>
|
||||||
<input type="hidden" name="update_cart" value="1">
|
<div class="d-inline-block">
|
||||||
<div class="table-responsive">
|
<a href="shop.php" class="btn btn-primary btn-lg">
|
||||||
<table class="table table-hover align-middle">
|
<i class="ri-store-2-line me-2"></i>
|
||||||
<thead class="table-light">
|
رفتن به فروشگاه
|
||||||
<tr>
|
</a>
|
||||||
<th scope="col" colspan="2">محصول</th>
|
</div>
|
||||||
<th scope="col" class="text-center">قیمت</th>
|
</div>
|
||||||
<th scope="col" class="text-center">تعداد</th>
|
<?php else: ?>
|
||||||
<th scope="col" class="text-end">جمع کل</th>
|
<div class="text-center" data-aos="fade-down">
|
||||||
<th scope="col" class="text-center">حذف</th>
|
<h1 class="section-title">سبد خرید شما</h1>
|
||||||
</tr>
|
<p class="text-muted fs-5">جزئیات سفارش خود را بررسی و نهایی کنید.</p>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
<?php foreach ($cart_items_detailed as $item): ?>
|
<div class="row g-5 mt-5">
|
||||||
<tr>
|
<div class="col-lg-8">
|
||||||
<td style="width: 100px;">
|
<?php foreach ($cart_items as $item_id => $item):
|
||||||
<img src="<?php echo htmlspecialchars($item['image_url']); ?>" alt="<?php echo htmlspecialchars($item['name']); ?>" class="img-fluid rounded-3">
|
$item_total = $item['price'] * $item['quantity'];
|
||||||
</td>
|
$total_price += $item_total;
|
||||||
<td>
|
?>
|
||||||
<h5 class="mb-0"><?php echo htmlspecialchars($item['name']); ?></h5>
|
<div class="card card-body mb-4" data-aos="fade-up">
|
||||||
<?php if ($item['color']): ?>
|
<div class="remove-item-btn">
|
||||||
<small class="text-muted">رنگ: <?php echo htmlspecialchars($item['color']); ?></small>
|
<form action="cart_handler.php" method="POST" class="d-inline">
|
||||||
<?php endif; ?>
|
<input type="hidden" name="product_id" value="<?php echo $item['product_id']; ?>">
|
||||||
</td>
|
<input type="hidden" name="product_color" value="<?php echo htmlspecialchars($item['color'] ?? ''); ?>">
|
||||||
<td class="text-center"><strong><?php echo number_format($item['price']); ?></strong></td>
|
<input type="hidden" name="action" value="remove">
|
||||||
<td class="text-center" style="width: 120px;">
|
<button type="submit" class="btn btn-link text-decoration-none p-0"><i class="ri-close-circle-line"></i></button>
|
||||||
<input type="number" class="form-control text-center" name="quantities[<?php echo $item['cart_item_id']; ?>]" value="<?php echo $item['quantity']; ?>" min="1" max="10">
|
</form>
|
||||||
</td>
|
</div>
|
||||||
<td class="text-end"><strong><?php echo number_format($item['subtotal']); ?></strong></td>
|
<div class="row align-items-center g-3">
|
||||||
<td class="text-center">
|
<div class="col-md-2 col-3 cart-item-image">
|
||||||
<a href="cart_handler.php?action=remove&id=<?php echo $item['cart_item_id']; ?>" class="btn btn-sm btn-outline-danger">×</a>
|
<a href="product.php?id=<?php echo $item['product_id']; ?>">
|
||||||
</td>
|
<img src="<?php echo htmlspecialchars($item['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($item['name']); ?>">
|
||||||
</tr>
|
</a>
|
||||||
<?php endforeach; ?>
|
</div>
|
||||||
</tbody>
|
<div class="col-md-4 col-9 cart-item-details">
|
||||||
</table>
|
<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>
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 flex-wrap gap-3">
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
<button type="submit" class="btn btn-outline-secondary">بهروزرسانی سبد</button>
|
|
||||||
<div class="text-end">
|
|
||||||
<h4>جمع نهایی: <span class="fw-bold text-primary"><?php echo number_format($total_price); ?> تومان</span></h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="text-center mt-5">
|
|
||||||
<a href="checkout.php" class="btn btn-primary btn-lg">ادامه جهت تسویه حساب</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
|
||||||
142
cart_handler.php
@ -1,70 +1,110 @@
|
|||||||
<?php
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/session_config.php';
|
||||||
session_start();
|
session_start();
|
||||||
|
require_once 'db/config.php';
|
||||||
|
|
||||||
// Initialize the cart if it doesn't exist
|
// Initialize cart if it doesn't exist
|
||||||
if (!isset($_SESSION['cart'])) {
|
if (!isset($_SESSION['cart'])) {
|
||||||
$_SESSION['cart'] = [];
|
$_SESSION['cart'] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the form was submitted and it's an add-to-cart action
|
// Get POST data
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
|
$product_id = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
|
||||||
$product_id = isset($_POST['product_id']) ? (int)$_POST['product_id'] : 0;
|
$quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);
|
||||||
$quantity = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1;
|
$color = filter_input(INPUT_POST, 'product_color', FILTER_SANITIZE_STRING);
|
||||||
$color = isset($_POST['color']) ? trim($_POST['color']) : null;
|
$action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING);
|
||||||
|
|
||||||
if ($product_id > 0 && $quantity > 0) {
|
if (!$action || !$product_id) {
|
||||||
// Create a unique ID for the cart item based on product ID and color
|
header('Location: shop.php');
|
||||||
$cart_item_id = $product_id . ($color ? '-' . preg_replace('/[^a-zA-Z0-9_]/ ', '-', $color) : '');
|
|
||||||
|
|
||||||
// If the exact item (product + color) is already in the cart, update the quantity
|
|
||||||
if (isset($_SESSION['cart'][$cart_item_id])) {
|
|
||||||
$_SESSION['cart'][$cart_item_id]['quantity'] += $quantity;
|
|
||||||
} else {
|
|
||||||
// Otherwise, add it as a new item
|
|
||||||
$_SESSION['cart'][$cart_item_id] = [
|
|
||||||
'product_id' => $product_id,
|
|
||||||
'quantity' => $quantity,
|
|
||||||
'color' => $color
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to the cart page to show the updated cart
|
|
||||||
header('Location: cart.php');
|
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle removing an item from the cart
|
// Generate a unique ID for the cart item based on product ID and color
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'remove') {
|
$cart_item_id = $product_id . ($color ? '_' . str_replace('#', '', $color) : '');
|
||||||
$cart_item_id = isset($_GET['id']) ? $_GET['id'] : '';
|
|
||||||
if (!empty($cart_item_id) && isset($_SESSION['cart'][$cart_item_id])) {
|
|
||||||
unset($_SESSION['cart'][$cart_item_id]);
|
|
||||||
}
|
|
||||||
// Redirect back to the cart page
|
|
||||||
header('Location: cart.php');
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle updating quantities
|
switch ($action) {
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_cart'])){
|
case 'add':
|
||||||
if(!empty($_POST['quantities'])){
|
if ($quantity > 0) {
|
||||||
foreach($_POST['quantities'] as $cart_item_id => $quantity){
|
try {
|
||||||
$quantity = (int)$quantity;
|
$pdo = db();
|
||||||
if(!empty($cart_item_id) && isset($_SESSION['cart'][$cart_item_id])){
|
// Fetch product details including colors
|
||||||
if($quantity > 0){
|
$stmt = $pdo->prepare("SELECT name, price, image_url, colors FROM products WHERE id = ?");
|
||||||
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
|
$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 {
|
} else {
|
||||||
// Remove item if quantity is 0 or less
|
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'محصول یافت نشد.'];
|
||||||
unset($_SESSION['cart'][$cart_item_id]);
|
|
||||||
}
|
}
|
||||||
|
} 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: cart.php');
|
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? 'shop.php'));
|
||||||
exit;
|
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.
|
||||||
// If someone accesses this file directly without a valid action, redirect them to the shop.
|
header('Location: cart.php');
|
||||||
header('Location: shop.php');
|
|
||||||
exit;
|
exit;
|
||||||
322
checkout.php
@ -1,174 +1,194 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
require_once 'db/config.php';
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
// If cart is empty, redirect to shop page, there is nothing to checkout
|
// 1. Check if cart exists and is not empty. If not, redirect to shop.
|
||||||
if (empty($_SESSION['cart'])) {
|
if (empty($_SESSION['cart'])) {
|
||||||
header('Location: shop.php');
|
header('Location: shop.php');
|
||||||
exit;
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$p_title = "تسویه حساب";
|
$cart_items = $_SESSION['cart'];
|
||||||
$order_placed_successfully = false;
|
$product_details = [];
|
||||||
$error_message = '';
|
$total_price = 0;
|
||||||
|
|
||||||
// Handle form submission
|
// 2. Process cart items directly from the session
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
foreach ($cart_items as $cart_item_id => $item) {
|
||||||
// --- Data Validation ---
|
// Ensure item has required data
|
||||||
$name = trim($_POST['customer_name'] ?? '');
|
if (!isset($item['price'], $item['quantity'], $item['name'], $item['image_url'])) {
|
||||||
$email = trim($_POST['customer_email'] ?? '');
|
// Skip malformed items
|
||||||
$address = trim($_POST['customer_address'] ?? '');
|
continue;
|
||||||
|
|
||||||
if (empty($name) || empty($email) || empty($address)) {
|
|
||||||
$error_message = 'لطفاً تمام فیلدها را پر کنید.';
|
|
||||||
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
$error_message = 'لطفاً یک آدرس ایمیل معتبر وارد کنید.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(empty($error_message)) {
|
$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();
|
$pdo = db();
|
||||||
try {
|
$user_stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
// --- Server-side recalculation of total ---
|
$user_stmt->execute([$user_id]);
|
||||||
$product_ids = array_keys($_SESSION['cart']);
|
$user = $user_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
$placeholders = implode(',', array_fill(0, count($product_ids), '?'));
|
|
||||||
$stmt = $pdo->prepare("SELECT id, price FROM products WHERE id IN ($placeholders)");
|
|
||||||
$stmt->execute($product_ids);
|
|
||||||
$products_from_db = $stmt->fetchAll(PDO::FETCH_ASSOC | PDO::FETCH_UNIQUE);
|
|
||||||
|
|
||||||
$total_amount = 0;
|
$address_stmt = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY id DESC LIMIT 1");
|
||||||
foreach ($_SESSION['cart'] as $product_id => $quantity) {
|
$address_stmt->execute([$user_id]);
|
||||||
if(isset($products_from_db[$product_id])){
|
$address = $address_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||||
$total_amount += $products_from_db[$product_id]['price'] * $quantity;
|
} catch (PDOException $e) {
|
||||||
}
|
error_log("Checkout user fetch error: " . $e->getMessage());
|
||||||
}
|
// Do not block the page, guest checkout is still possible
|
||||||
|
|
||||||
// --- Database Transaction ---
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
// 1. Insert into orders table
|
|
||||||
$sql_order = "INSERT INTO orders (customer_name, customer_email, customer_address, total_amount) VALUES (?, ?, ?, ?)";
|
|
||||||
$stmt_order = $pdo->prepare($sql_order);
|
|
||||||
$stmt_order->execute([$name, $email, $address, $total_amount]);
|
|
||||||
$order_id = $pdo->lastInsertId();
|
|
||||||
|
|
||||||
// 2. Insert into order_items table
|
|
||||||
$sql_items = "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)";
|
|
||||||
$stmt_items = $pdo->prepare($sql_items);
|
|
||||||
|
|
||||||
foreach ($_SESSION['cart'] as $product_id => $quantity) {
|
|
||||||
if(isset($products_from_db[$product_id])){
|
|
||||||
$price = $products_from_db[$product_id]['price'];
|
|
||||||
$stmt_items->execute([$order_id, $product_id, $quantity, $price]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Commit the transaction
|
|
||||||
$pdo->commit();
|
|
||||||
|
|
||||||
// 4. Clear the cart and set success flag
|
|
||||||
unset($_SESSION['cart']);
|
|
||||||
$order_placed_successfully = true;
|
|
||||||
$p_title = "سفارش شما ثبت شد";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if ($pdo->inTransaction()) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
}
|
|
||||||
error_log("Checkout Error: " . $e->getMessage());
|
|
||||||
$error_message = 'مشکلی در ثبت سفارش شما به وجود آمد. لطفاً دوباره تلاش کنید.';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$shipping_cost = 50000;
|
||||||
|
$grand_total = $total_price + $shipping_cost;
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!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 $p_title; ?> - چرم آتیمه</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow">
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=Inter:wght@400;500&family=Lalezar&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
</head>
|
|
||||||
<body class="bg-dark text-white">
|
|
||||||
|
|
||||||
<!-- Header -->
|
<main class="checkout-page-wrapper">
|
||||||
<header class="p-3 mb-3 border-bottom border-secondary">
|
<section class="section-padding">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
|
<div class="text-center" data-aos="fade-down">
|
||||||
<a href="index.php" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
|
<h1 class="section-title">تکمیل سفارش و پرداخت</h1>
|
||||||
<h1 class="font-playfair fs-2" style="color: #D4AF37;">آتیمه</h1>
|
<p class="text-muted fs-5">اطلاعات خود را برای ارسال سفارش وارد کنید.</p>
|
||||||
</a>
|
</div>
|
||||||
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
|
|
||||||
<li><a href="index.php" class="nav-link px-2 text-white">خانه</a></li>
|
<div class="row g-5 mt-5">
|
||||||
<li><a href="shop.php" class="nav-link px-2 text-white">فروشگاه</a></li>
|
<!-- Billing Details Column -->
|
||||||
</ul>
|
<div class="col-lg-8" data-aos="fade-right">
|
||||||
<div class="text-end">
|
<h3 class="mb-4">جزئیات صورتحساب</h3>
|
||||||
<a href="cart.php" class="btn btn-outline-warning position-relative">
|
|
||||||
سبد خرید
|
<?php
|
||||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
if (!empty($_SESSION['checkout_errors'])) {
|
||||||
<?php echo count($_SESSION['cart'] ?? []); ?>
|
echo '<div class="alert alert-danger"><ul>';
|
||||||
</span>
|
foreach ($_SESSION['checkout_errors'] as $error) {
|
||||||
</a>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<?php require_once 'includes/footer.php'; ?>
|
||||||
<main class="container my-5">
|
|
||||||
<div class="text-center mb-5">
|
|
||||||
<h2 class="font-lalezar display-4"><?php echo $p_title; ?></h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<?php if ($order_placed_successfully): ?>
|
|
||||||
<div class="alert alert-success text-center">
|
|
||||||
<h4>از خرید شما متشکریم!</h4>
|
|
||||||
<p>سفارش شما با موفقیت ثبت شد و به زودی پردازش خواهد شد. یک ایمیل تایید برای شما ارسال گردید.</p>
|
|
||||||
<a href="shop.php" class="btn btn-warning">بازگشت به فروشگاه</a>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php if (!empty($error_message)): ?>
|
|
||||||
<div class="alert alert-danger">.<?php echo $error_message; ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="card bg-dark-2">
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<h5 class="card-title mb-4">اطلاعات ارسال</h5>
|
|
||||||
<form action="checkout.php" method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="customer_name" class="form-label">نام و نام خانوادگی</label>
|
|
||||||
<input type="text" class="form-control bg-dark text-white" id="customer_name" name="customer_name" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="customer_email" class="form-label">آدرس ایمیل</label>
|
|
||||||
<input type="email" class="form-control bg-dark text-white" id="customer_email" name="customer_email" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="customer_address" class="form-label">آدرس کامل</label>
|
|
||||||
<textarea class="form-control bg-dark text-white" id="customer_address" name="customer_address" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="d-grid mt-4">
|
|
||||||
<button type="submit" class="btn btn-warning btn-lg fw-bold">ثبت سفارش و پرداخت</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="py-5 mt-5 border-top border-secondary">
|
|
||||||
<div class="container text-center">
|
|
||||||
<p class="text-muted">© <?php echo date("Y"); ?> چرم آتیمه. تمام حقوق محفوظ است.</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
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
173
contact.php
@ -1,82 +1,135 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start(); // Ensure session is started
|
||||||
require_once __DIR__ . '/mail/MailService.php';
|
|
||||||
|
|
||||||
$page_title = 'تماس با ما';
|
$page_title = 'تماس با ما';
|
||||||
$page_description = 'با ما در تماس باشید. نظرات و پیشنهادات شما برای ما ارزشمند است.';
|
require_once 'includes/header.php';
|
||||||
|
require_once 'mail/MailService.php';
|
||||||
|
|
||||||
$message = '';
|
// Handle form submission
|
||||||
$error = '';
|
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 ($_SERVER["REQUEST_METHOD"] == "POST") {
|
if (empty($name) || empty($email) || empty($message)) {
|
||||||
$name = trim($_POST['name']);
|
$_SESSION['flash_message'] = ['type' => 'warning', 'message' => 'لطفاً تمام فیلدها را پر کنید.'];
|
||||||
$email = trim($_POST['email']);
|
} elseif (!$email) {
|
||||||
$subject = trim($_POST['subject']);
|
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'آدرس ایمیل وارد شده معتبر نیست.'];
|
||||||
$body = trim($_POST['message']);
|
|
||||||
|
|
||||||
if (empty($name) || empty($email) || empty($subject) || empty($body) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
$error = 'لطفاً تمام فیلدها را به درستی پر کنید.';
|
|
||||||
} else {
|
} else {
|
||||||
$response = MailService::sendContactMessage($name, $email, $body, null, $subject);
|
$to_email = getenv('MAIL_TO') ?: 'support@atimeh.com'; // Fallback email
|
||||||
if (!empty($response['success'])) {
|
$subject = "پیام جدید از فرم تماس وبسایت";
|
||||||
$message = 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!';
|
$email_result = MailService::sendContactMessage($name, $email, $message, $to_email, $subject);
|
||||||
|
|
||||||
|
if (!empty($email_result['success'])) {
|
||||||
|
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!'];
|
||||||
} else {
|
} else {
|
||||||
$error = 'خطایی در ارسال پیام رخ داد. لطفاً بعداً تلاش کنید. متن خطا: ' . htmlspecialchars($response['error'] ?? 'Unknown error');
|
$_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();
|
||||||
}
|
}
|
||||||
|
|
||||||
include 'includes/header.php';
|
// 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="row justify-content-center">
|
<div class="container py-5 my-5">
|
||||||
<div class="col-lg-8 text-center" data-aos="fade-up">
|
<div class="section-title text-center mb-5" data-aos="fade-down">
|
||||||
<h1 class="display-4 fw-bold"><?php echo $page_title; ?></h1>
|
<h1>ارتباط با ما</h1>
|
||||||
<p class="lead text-white-50 mt-3"><?php echo $page_description; ?></p>
|
<p class="fs-5 text-muted">نظرات، پیشنهادات و سوالات شما برای ما ارزشمند است.</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center mt-5">
|
<div class="contact-card p-4 p-lg-5" data-aos="fade-up">
|
||||||
<div class="col-lg-8">
|
<div class="row g-5">
|
||||||
<div class="card border-0" style="background-color: var(--surface-color);">
|
<div class="col-lg-5">
|
||||||
<div class="card-body p-4 p-md-5">
|
<div class="contact-info h-100 d-flex flex-column justify-content-center">
|
||||||
|
<h3 class="mb-4">راههای ارتباطی</h3>
|
||||||
<?php if ($message): ?>
|
<div class="d-flex align-items-start mb-4">
|
||||||
<div class="alert alert-success" role="alert">
|
<i class="ri-map-pin-line mt-1 me-3"></i>
|
||||||
<?php echo $message; ?>
|
<div>
|
||||||
|
<strong>آدرس:</strong>
|
||||||
|
<p class="text-muted mb-0">تهران، خیابان هنر، کوچه خلاقیت، پلاک ۱۲</p>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
<?php if ($error): ?>
|
<div class="d-flex align-items-start mb-4">
|
||||||
<div class="alert alert-danger" role="alert">
|
<i class="ri-mail-line mt-1 me-3"></i>
|
||||||
<?php echo $error; ?>
|
<div>
|
||||||
|
<strong>ایمیل:</strong>
|
||||||
|
<p class="mb-0"><a href="mailto:info@atimeh.com">info@atimeh.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
|
<div class="d-flex align-items-start mb-4">
|
||||||
<form action="contact.php" method="POST" data-aos="fade-up" data-aos-delay="200">
|
<i class="ri-phone-line mt-1 me-3"></i>
|
||||||
<div class="mb-4">
|
<div>
|
||||||
<label for="name" class="form-label fs-5">نام شما</label>
|
<strong>تلفن:</strong>
|
||||||
<input type="text" class="form-control form-control-lg bg-dark text-white" id="name" name="name" required>
|
<p class="mb-0"><a href="tel:+982112345678">۰۲۱-۱۲۳۴۵۶۷۸</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<label for="email" class="form-label fs-5">ایمیل شما</label>
|
<hr class="my-4" style="border-color: var(--luxury-border);">
|
||||||
<input type="email" class="form-control form-control-lg bg-dark text-white" id="email" name="email" required>
|
<h4 class="h5 mb-3">ما را دنبال کنید</h4>
|
||||||
</div>
|
<div class="d-flex gap-2">
|
||||||
<div class="mb-4">
|
<a href="#" class="social-btn"><i class="ri-instagram-line"></i></a>
|
||||||
<label for="subject" class="form-label fs-5">موضوع</label>
|
<a href="#" class="social-btn"><i class="ri-telegram-line"></i></a>
|
||||||
<input type="text" class="form-control form-control-lg bg-dark text-white" id="subject" name="subject" required>
|
<a href="#" class="social-btn"><i class="ri-whatsapp-line"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
|
||||||
<label for="message" class="form-label fs-5">پیام شما</label>
|
|
||||||
<textarea class="form-control form-control-lg bg-dark text-white" id="message" name="message" rows="5" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="d-grid">
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg">ارسال پیام</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-info mt-4"><b>توجه:</b> این فرم برای اهداف آزمایشی است. برای دریافت واقعی ایمیلها، باید اطلاعات سرور ایمیل (SMTP) خود را در فایل <code>.env</code> وارد کنید.</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>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<!-- 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');
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
ALTER TABLE products ADD COLUMN colors VARCHAR(255) DEFAULT NULL COMMENT 'Comma-separated list of available colors';
|
-- 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';
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
ALTER TABLE products ADD COLUMN is_featured BOOLEAN DEFAULT 0;
|
-- 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"}
|
||||||
@ -11,9 +11,10 @@
|
|||||||
<h5 class="fw-bold mb-3">دسترسی سریع</h5>
|
<h5 class="fw-bold mb-3">دسترسی سریع</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><a href="shop.php" class="text-white-50">فروشگاه</a></li>
|
<li><a href="shop.php" class="text-white-50">فروشگاه</a></li>
|
||||||
<li><a href="#" class="text-white-50">درباره ما</a></li>
|
<li><a href="track_order.php" class="text-white-50">پیگیری سفارش</a></li>
|
||||||
<li><a href="#" class="text-white-50">قوانین و مقررات</a></li>
|
<li><a href="about.php" class="text-white-50">درباره ما</a></li>
|
||||||
<li><a href="#" 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
|
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
|
||||||
@ -36,10 +37,10 @@
|
|||||||
<div class="col-lg-3 col-md-6">
|
<div class="col-lg-3 col-md-6">
|
||||||
<h5 class="fw-bold mb-3">ما را دنبال کنید</h5>
|
<h5 class="fw-bold mb-3">ما را دنبال کنید</h5>
|
||||||
<p class="text-white-50">از جدیدترین محصولات و تخفیفها باخبر شوید.</p>
|
<p class="text-white-50">از جدیدترین محصولات و تخفیفها باخبر شوید.</p>
|
||||||
<div class="d-flex mt-3">
|
<div class="d-flex mt-3 social-icons">
|
||||||
<a href="#" class="btn btn-outline-primary me-2"><i class="bi bi-instagram"></i></a>
|
<a href="#" class="social-icon me-3"><i class="bi bi-instagram"></i></a>
|
||||||
<a href="#" class="btn btn-outline-primary me-2"><i class="bi bi-telegram"></i></a>
|
<a href="#" class="social-icon me-3"><i class="bi bi-telegram"></i></a>
|
||||||
<a href="#" class="btn btn-outline-primary"><i class="bi bi-whatsapp"></i></a>
|
<a href="#" class="social-icon"><i class="bi bi-whatsapp"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -49,10 +50,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Bootstrap JS Bundle -->
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<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>
|
||||||
<!-- Custom JS -->
|
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,40 +1,82 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// Enforce session cookie settings BEFORE starting the session
|
||||||
|
require_once __DIR__ . '/session_config.php';
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
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;
|
$cart_item_count = isset($_SESSION['cart']) ? array_sum(array_column($_SESSION['cart'], 'quantity')) : 0;
|
||||||
$page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
$page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fa" dir="rtl" class="dark">
|
<html lang="fa" dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php echo htmlspecialchars($page_title); ?></title>
|
<title><?php echo htmlspecialchars($page_title); ?></title>
|
||||||
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'خرید محصولات چرمی لوکس و با کیفیت.'); ?>">
|
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'خرید محصولات چرمی لوکس و با کیفیت.'); ?>">
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<!-- IRANSans Font -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="https://font-ir.s3.ir-thr-at1.arvanstorage.com/IRANSans/css/IRANSans.css">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Bootstrap 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@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
|
||||||
<!-- Bootstrap Icons -->
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<!-- Remix Icon CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
|
||||||
|
|
||||||
<!-- AOS CSS -->
|
<!-- AOS CSS -->
|
||||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
<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 -->
|
<!-- Custom CSS -->
|
||||||
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
|
<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>
|
</head>
|
||||||
<body>
|
<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">
|
<header class="site-header sticky-top py-3">
|
||||||
<nav class="navbar navbar-expand-lg container">
|
<nav class="navbar navbar-expand-lg container">
|
||||||
<div class="container-fluid">
|
<div class="container">
|
||||||
<a class="navbar-brand fw-bold fs-4" href="index.php">آتیمه</a>
|
<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">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
@ -50,7 +92,7 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
|||||||
<a class="nav-link" href="shop.php">فروشگاه</a>
|
<a class="nav-link" href="shop.php">فروشگاه</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#">درباره ما</a>
|
<a class="nav-link" href="about.php">درباره ما</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="contact.php">تماس با ما</a>
|
<a class="nav-link" href="contact.php">تماس با ما</a>
|
||||||
@ -59,7 +101,7 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
|||||||
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<a href="cart.php" class="ms-4 position-relative">
|
<a href="cart.php" class="ms-4 position-relative">
|
||||||
<i class="bi bi-bag fs-5"></i>
|
<i class="ri-shopping-bag-line fs-5"></i>
|
||||||
<?php if ($cart_item_count > 0): ?>
|
<?php if ($cart_item_count > 0): ?>
|
||||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
||||||
<?php echo $cart_item_count; ?>
|
<?php echo $cart_item_count; ?>
|
||||||
@ -67,9 +109,23 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
|
|||||||
</span>
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
<a href="/admin/login.php" class="ms-3">
|
|
||||||
<i class="bi bi-person fs-5"></i>
|
<?php if (isset($_SESSION['user_id'])): ?>
|
||||||
</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
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
|
||||||
|
]);
|
||||||
|
?>
|
||||||
171
index.php
@ -1,88 +1,111 @@
|
|||||||
<?php
|
<?php
|
||||||
$page_title = 'صفحه اصلی';
|
$page_title = 'صفحه اصلی';
|
||||||
include 'includes/header.php';
|
include 'includes/header.php';
|
||||||
|
|
||||||
|
|
||||||
|
// 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';
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="hero-section vh-100 d-flex align-items-center text-white text-center">
|
<section class="hero-section vh-100 d-flex justify-content-center align-items-center position-relative text-white text-center">
|
||||||
<div class="video-background-wrapper">
|
<div class="hero-video-background">
|
||||||
<div class="video-overlay"></div>
|
<video playsinline autoplay muted loop poster="assets/images/pexels/about-us-34942790.jpg">
|
||||||
<video playsinline="playsinline" autoplay="autoplay" muted="muted" loop="loop">
|
<source src="https://videos.pexels.com/video-files/8065365/8065365-hd_1920_1080_25fps.mp4" type="video/mp4">
|
||||||
<source src="https://storage.googleapis.com/gemini-agent-mediabucket-prod/v-001/video_bg.mp4" type="video/mp4">
|
ویدیوی شما توسط مرورگر پشتیبانی نمیشود.
|
||||||
</video>
|
</video>
|
||||||
</div>
|
<div class="hero-video-overlay"></div>
|
||||||
<div class="container position-relative">
|
</div>
|
||||||
<h1 class="display-3 fw-bold mb-3 hero-title" data-aos="fade-up">اصالت در هر نگاه</h1>
|
<div class="container position-relative">
|
||||||
<p class="lead fs-4 mb-4 hero-subtitle" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دستدوز، آفریده برای ماندگاری.</p>
|
<h1 class="display-3 fw-bold" data-aos="fade-down">اصالت در هر نگاه</h1>
|
||||||
<a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">کاوش در مجموعه</a>
|
<p class="lead fs-4 mb-4" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دستدوز، آفریده برای ماندگاری.</p>
|
||||||
</div>
|
<a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">مشاهده محصولات</a>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
<!-- Featured Products Section -->
|
<!-- Featured Products Section -->
|
||||||
<section id="featured-products" class="py-5">
|
<section id="featured-products" class="section-padding">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="text-center mb-5" data-aos="fade-up">
|
<?php
|
||||||
<h2 class="display-5 fw-bold">مجموعه برگزیده ما</h2>
|
if (isset($_SESSION['success_message'])) {
|
||||||
<p class="text-white-50 fs-5">دستچین شده برای سلیقههای خاص.</p>
|
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>
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 g-lg-5">
|
</div>
|
||||||
<?php
|
<?php
|
||||||
require_once 'db/config.php';
|
$delay += 150;
|
||||||
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);
|
|
||||||
|
|
||||||
$animations = ['fade-up', 'zoom-in-up', 'fade-left'];
|
|
||||||
if (empty($featured_products)) {
|
|
||||||
echo '<div class="col-12"><p class="text-center text-white-50">هیچ محصولی برای نمایش وجود ندارد.</p></div>';
|
|
||||||
} else {
|
|
||||||
$delay = 0;
|
|
||||||
foreach ($featured_products as $key => $product) {
|
|
||||||
$animation = $animations[$key % count($animations)]; // Cycle through animations
|
|
||||||
echo '<div class="col" data-aos="' . $animation . '" data-aos-delay="' . $delay . '">';
|
|
||||||
echo ' <div class="product-card h-100">';
|
|
||||||
echo ' <div class="product-image">';
|
|
||||||
echo ' <a href="product.php?id=' . $product['id'] . '">';
|
|
||||||
echo ' <img src="' . htmlspecialchars($product['image_url']) . '" class="img-fluid" alt="' . htmlspecialchars($product['name']) . '">';
|
|
||||||
echo ' </a>';
|
|
||||||
echo ' </div>';
|
|
||||||
echo ' <div class="product-info text-center">';
|
|
||||||
echo ' <h3 class="product-title"><a href="product.php?id=' . $product['id'] . '" class="text-decoration-none">' . htmlspecialchars($product['name']) . '</a></h3>';
|
|
||||||
echo ' <p class="product-price">' . number_format($product['price']) . ' تومان</p>';
|
|
||||||
echo ' </div>';
|
|
||||||
echo ' </div>';
|
|
||||||
echo '</div>';
|
|
||||||
$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>
|
} catch (PDOException $e) {
|
||||||
<div class="text-center mt-5" data-aos="fade-up">
|
error_log("Database error: " . $e->getMessage());
|
||||||
<a href="shop.php" class="btn btn-outline-gold btn-lg">مشاهده تمام محصولات</a>
|
echo '<div class="col-12"><p class="text-center text-danger">خطا در بارگذاری محصولات.</p></div>';
|
||||||
</div>
|
}
|
||||||
</div>
|
?>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<!-- About Us Section -->
|
<div class="text-center mt-5" data-aos="fade-up">
|
||||||
<section id="about-us" class="py-5 my-5">
|
<a href="shop.php" class="btn btn-primary">مشاهده تمام محصولات</a>
|
||||||
<div class="container">
|
</div>
|
||||||
<div class="row align-items-center">
|
</div>
|
||||||
<div class="col-md-6" data-aos="fade-right">
|
</section>
|
||||||
<img src="https://storage.googleapis.com/gemini-agent-mediabucket-prod/v-001/about-us.jpg" alt="درباره ما" class="img-fluid rounded-4 shadow-lg">
|
|
||||||
</div>
|
<!-- About Us Section -->
|
||||||
<div class="col-md-6 mt-4 mt-md-0 ps-md-5" data-aos="fade-left">
|
<section id="about-us" class="section-padding bg-surface">
|
||||||
<h2 class="display-5 fw-bold">داستان آتیمه</h2>
|
<div class="container">
|
||||||
<p class="text-white-50 fs-5 mt-3">ما در آتیمه، به تلفیق هنر سنتی و طراحی مدرن باور داریم. هر محصول، حاصل ساعتها کار دست هنرمندان ماهر و استفاده از بهترین چرمهای طبیعی است. هدف ما خلق آثاری است که نه تنها یک وسیله، بلکه بخشی از داستان و استایل شما باشند.</p>
|
<div class="row align-items-center g-5">
|
||||||
<a href="#" class="btn btn-primary mt-3">بیشتر بدانید</a>
|
<div class="col-md-6" data-aos="fade-right">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?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';
|
||||||
187
product.php
@ -1,10 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
|
||||||
require_once 'db/config.php';
|
require_once 'db/config.php';
|
||||||
|
require_once 'includes/header.php';
|
||||||
|
|
||||||
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||||
|
$product = null;
|
||||||
|
$db_error = '';
|
||||||
|
|
||||||
if ($product_id <= 0) {
|
if (!$product_id) {
|
||||||
|
// Redirect or show error if ID is not valid
|
||||||
header("Location: shop.php");
|
header("Location: shop.php");
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -14,58 +17,144 @@ try {
|
|||||||
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
|
||||||
$stmt->execute([$product_id]);
|
$stmt->execute([$product_id]);
|
||||||
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
$product = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$product) {
|
|
||||||
header("Location: shop.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
error_log("DB Error: " . $e->getMessage());
|
error_log("Database Error: " . $e->getMessage());
|
||||||
die("An error occurred. Please try again later.");
|
$db_error = "<p>خطا در برقراری ارتباط با پایگاه داده.</p>";
|
||||||
}
|
}
|
||||||
|
|
||||||
$page_title = htmlspecialchars($product['name']);
|
// If product not found, show a message and stop
|
||||||
$available_colors = !empty($product['colors']) ? array_map('trim', explode(',', $product['colors'])) : [];
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
include 'includes/header.php';
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="row g-5">
|
<main class="container section-padding">
|
||||||
<div class="col-lg-6">
|
<div class="row g-5">
|
||||||
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid rounded-4 shadow-lg w-100" alt="<?php echo htmlspecialchars($product['name']); ?>" style="aspect-ratio: 1/1; object-fit: cover;">
|
<div class="col-lg-6" data-aos="fade-right">
|
||||||
</div>
|
<div class="card card-static p-3">
|
||||||
<div class="col-lg-6 d-flex flex-column justify-content-center">
|
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($product['name']); ?>">
|
||||||
<h1 class="display-4 fw-bold"><?php echo htmlspecialchars($product['name']); ?></h1>
|
|
||||||
<p class="lead text-white-50 my-3"><?php echo htmlspecialchars($product['description']); ?></p>
|
|
||||||
|
|
||||||
<div class="display-5 fw-bold my-4 text-gold"><?php echo number_format($product['price']); ?> <span class="fs-5 text-white-50">تومان</span></div>
|
|
||||||
|
|
||||||
<form action="cart_handler.php" method="POST">
|
|
||||||
<input type="hidden" name="product_id" value="<?php echo $product['id']; ?>">
|
|
||||||
|
|
||||||
<?php if (!empty($available_colors)): ?>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label class="form-label fw-bold fs-5 mb-3">انتخاب رنگ:</label>
|
|
||||||
<div class="d-flex flex-wrap gap-3 color-swatches">
|
|
||||||
<?php foreach ($available_colors as $index => $color): ?>
|
|
||||||
<div data-bs-toggle="tooltip" title="<?php echo htmlspecialchars($color); ?>">
|
|
||||||
<input type="radio" class="btn-check" name="color" id="color-<?php echo $index; ?>" value="<?php echo htmlspecialchars($color); ?>" autocomplete="off" <?php echo $index === 0 ? 'checked' : ''; ?>>
|
|
||||||
<label class="btn" for="color-<?php echo $index; ?>"><?php echo htmlspecialchars($color); ?></label>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center mb-4">
|
|
||||||
<label for="quantity" class="form-label ms-3 mb-0 fs-5">تعداد:</label>
|
|
||||||
<input type="number" name="quantity" id="quantity" class="form-control bg-dark text-white" value="1" min="1" max="10" style="width: 80px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" name="add_to_cart" class="btn btn-primary btn-lg w-100 py-3 fw-bold">افزودن به سبد خرید</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<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>
|
||||||
79
shop.php
@ -1,46 +1,65 @@
|
|||||||
<?php
|
<?php
|
||||||
|
$page_title = 'فروشگاه';
|
||||||
|
require_once 'includes/header.php';
|
||||||
require_once 'db/config.php';
|
require_once 'db/config.php';
|
||||||
|
|
||||||
|
// Fetch all products from the database
|
||||||
try {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->query("SELECT * FROM products ORDER BY created_at DESC");
|
$stmt = $pdo->query("SELECT * FROM products ORDER BY created_at DESC");
|
||||||
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
error_log("DB Error: " . $e->getMessage());
|
error_log("Database error: " . $e->getMessage());
|
||||||
$products = [];
|
$products = [];
|
||||||
|
$db_error = "خطا در بارگذاری محصولات. لطفا بعدا تلاش کنید.";
|
||||||
}
|
}
|
||||||
|
|
||||||
$page_title = 'فروشگاه';
|
|
||||||
include 'includes/header.php';
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="text-center mb-5">
|
<main class="container section-padding">
|
||||||
<h1 class="display-4 fw-bold">گالری محصولات</h1>
|
<div class="text-center" data-aos="fade-down">
|
||||||
<p class="lead text-muted">دستسازههایی از چرم طبیعی، با عشق و دقت</p>
|
<h1 class="section-title">مجموعه کامل محصولات</h1>
|
||||||
</div>
|
<p class="fs-5 text-muted">دستسازههایی از چرم طبیعی، با عشق و دقت.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
<?php if (!empty($db_error)): ?>
|
||||||
<?php if (!empty($products)): ?>
|
<div class="alert alert-danger">
|
||||||
<?php foreach ($products as $product): ?>
|
<?= $db_error; ?>
|
||||||
<div class="col">
|
|
||||||
<div class="product-card h-100">
|
|
||||||
<div class="product-image">
|
|
||||||
<a href="product.php?id=<?php echo $product['id']; ?>">
|
|
||||||
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid" alt="<?php echo htmlspecialchars($product['name']); ?>">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="product-info text-center">
|
|
||||||
<h3 class="product-title"><a href="product.php?id=<?php echo $product['id']; ?>" class="text-decoration-none"><?php echo htmlspecialchars($product['name']); ?></a></h3>
|
|
||||||
<p class="product-price"><?php echo number_format($product['price']); ?> تومان</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="col-12">
|
|
||||||
<p class="text-center p-5 bg-light rounded-3">محصولی برای نمایش یافت نشد.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php include 'includes/footer.php'; ?>
|
<?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'; ?>
|
||||||
25
vendor/autoload.php
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload.php @generated by Composer
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID < 50600) {
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header('HTTP/1.1 500 Internal Server Error');
|
||||||
|
}
|
||||||
|
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||||
|
if (!ini_get('display_errors')) {
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
fwrite(STDERR, $err);
|
||||||
|
} elseif (!headers_sent()) {
|
||||||
|
echo $err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trigger_error(
|
||||||
|
$err,
|
||||||
|
E_USER_ERROR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/composer/autoload_real.php';
|
||||||
|
|
||||||
|
return ComposerAutoloaderInit5b2bba47128b6bc755536cfe29b1c726::getLoader();
|
||||||
585
vendor/composer/ClassLoader.php
vendored
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Autoload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||||
|
*
|
||||||
|
* $loader = new \Composer\Autoload\ClassLoader();
|
||||||
|
*
|
||||||
|
* // register classes with namespaces
|
||||||
|
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||||
|
* $loader->add('Symfony', __DIR__.'/framework');
|
||||||
|
*
|
||||||
|
* // activate the autoloader
|
||||||
|
* $loader->register();
|
||||||
|
*
|
||||||
|
* // to enable searching the include path (eg. for PEAR packages)
|
||||||
|
* $loader->setUseIncludePath(true);
|
||||||
|
*
|
||||||
|
* In this example, if you try to use a class in the Symfony\Component
|
||||||
|
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||||
|
* the autoloader will first look for the class under the component/
|
||||||
|
* directory, and it will then fallback to the framework/ directory if not
|
||||||
|
* found before giving up.
|
||||||
|
*
|
||||||
|
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
* @see https://www.php-fig.org/psr/psr-0/
|
||||||
|
* @see https://www.php-fig.org/psr/psr-4/
|
||||||
|
*/
|
||||||
|
class ClassLoader
|
||||||
|
{
|
||||||
|
/** @var \Closure(string):void */
|
||||||
|
private static $includeFile;
|
||||||
|
|
||||||
|
/** @var ?string */
|
||||||
|
private $vendorDir;
|
||||||
|
|
||||||
|
// PSR-4
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array<string, int>>
|
||||||
|
*/
|
||||||
|
private $prefixLengthsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
private $prefixDirsPsr4 = array();
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr4 = array();
|
||||||
|
|
||||||
|
// PSR-0
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array<string, string[]>>
|
||||||
|
*/
|
||||||
|
private $prefixesPsr0 = array();
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, string>
|
||||||
|
*/
|
||||||
|
private $fallbackDirsPsr0 = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $useIncludePath = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
* @psalm-var array<string, string>
|
||||||
|
*/
|
||||||
|
private $classMap = array();
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $classMapAuthoritative = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool[]
|
||||||
|
* @psalm-var array<string, bool>
|
||||||
|
*/
|
||||||
|
private $missingClasses = array();
|
||||||
|
|
||||||
|
/** @var ?string */
|
||||||
|
private $apcuPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var self[]
|
||||||
|
*/
|
||||||
|
private static $registeredLoaders = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ?string $vendorDir
|
||||||
|
*/
|
||||||
|
public function __construct($vendorDir = null)
|
||||||
|
{
|
||||||
|
$this->vendorDir = $vendorDir;
|
||||||
|
self::initializeIncludeClosure();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getPrefixes()
|
||||||
|
{
|
||||||
|
if (!empty($this->prefixesPsr0)) {
|
||||||
|
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
public function getPrefixesPsr4()
|
||||||
|
{
|
||||||
|
return $this->prefixDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirs()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getFallbackDirsPsr4()
|
||||||
|
{
|
||||||
|
return $this->fallbackDirsPsr4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[] Array of classname => path
|
||||||
|
* @psalm-return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getClassMap()
|
||||||
|
{
|
||||||
|
return $this->classMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $classMap Class to filename map
|
||||||
|
* @psalm-param array<string, string> $classMap
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addClassMap(array $classMap)
|
||||||
|
{
|
||||||
|
if ($this->classMap) {
|
||||||
|
$this->classMap = array_merge($this->classMap, $classMap);
|
||||||
|
} else {
|
||||||
|
$this->classMap = $classMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix, either
|
||||||
|
* appending or prepending to the ones previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param string[]|string $paths The PSR-0 root directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
(array) $paths,
|
||||||
|
$this->fallbackDirsPsr0
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr0 = array_merge(
|
||||||
|
$this->fallbackDirsPsr0,
|
||||||
|
(array) $paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = $prefix[0];
|
||||||
|
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($prepend) {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
(array) $paths,
|
||||||
|
$this->prefixesPsr0[$first][$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||||
|
$this->prefixesPsr0[$first][$prefix],
|
||||||
|
(array) $paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace, either
|
||||||
|
* appending or prepending to the ones previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param string[]|string $paths The PSR-4 base directories
|
||||||
|
* @param bool $prepend Whether to prepend the directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function addPsr4($prefix, $paths, $prepend = false)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
// Register directories for the root namespace.
|
||||||
|
if ($prepend) {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
(array) $paths,
|
||||||
|
$this->fallbackDirsPsr4
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->fallbackDirsPsr4 = array_merge(
|
||||||
|
$this->fallbackDirsPsr4,
|
||||||
|
(array) $paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||||
|
// Register directories for a new namespace.
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||||
|
} elseif ($prepend) {
|
||||||
|
// Prepend directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
(array) $paths,
|
||||||
|
$this->prefixDirsPsr4[$prefix]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Append directories for an already registered namespace.
|
||||||
|
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||||
|
$this->prefixDirsPsr4[$prefix],
|
||||||
|
(array) $paths
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-0 directories for a given prefix,
|
||||||
|
* replacing any others previously set for this prefix.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix
|
||||||
|
* @param string[]|string $paths The PSR-0 base directories
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function set($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr0 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a set of PSR-4 directories for a given namespace,
|
||||||
|
* replacing any others previously set for this namespace.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||||
|
* @param string[]|string $paths The PSR-4 base directories
|
||||||
|
*
|
||||||
|
* @throws \InvalidArgumentException
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setPsr4($prefix, $paths)
|
||||||
|
{
|
||||||
|
if (!$prefix) {
|
||||||
|
$this->fallbackDirsPsr4 = (array) $paths;
|
||||||
|
} else {
|
||||||
|
$length = strlen($prefix);
|
||||||
|
if ('\\' !== $prefix[$length - 1]) {
|
||||||
|
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||||
|
}
|
||||||
|
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||||
|
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns on searching the include path for class files.
|
||||||
|
*
|
||||||
|
* @param bool $useIncludePath
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setUseIncludePath($useIncludePath)
|
||||||
|
{
|
||||||
|
$this->useIncludePath = $useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used to check if the autoloader uses the include path to check
|
||||||
|
* for classes.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function getUseIncludePath()
|
||||||
|
{
|
||||||
|
return $this->useIncludePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns off searching the prefix and fallback directories for classes
|
||||||
|
* that have not been registered with the class map.
|
||||||
|
*
|
||||||
|
* @param bool $classMapAuthoritative
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||||
|
{
|
||||||
|
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should class lookup fail if not found in the current class map?
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isClassMapAuthoritative()
|
||||||
|
{
|
||||||
|
return $this->classMapAuthoritative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||||
|
*
|
||||||
|
* @param string|null $apcuPrefix
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setApcuPrefix($apcuPrefix)
|
||||||
|
{
|
||||||
|
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getApcuPrefix()
|
||||||
|
{
|
||||||
|
return $this->apcuPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @param bool $prepend Whether to prepend the autoloader or not
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register($prepend = false)
|
||||||
|
{
|
||||||
|
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||||
|
|
||||||
|
if (null === $this->vendorDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prepend) {
|
||||||
|
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||||
|
} else {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters this instance as an autoloader.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function unregister()
|
||||||
|
{
|
||||||
|
spl_autoload_unregister(array($this, 'loadClass'));
|
||||||
|
|
||||||
|
if (null !== $this->vendorDir) {
|
||||||
|
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the given class or interface.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
* @return true|null True if loaded, null otherwise
|
||||||
|
*/
|
||||||
|
public function loadClass($class)
|
||||||
|
{
|
||||||
|
if ($file = $this->findFile($class)) {
|
||||||
|
$includeFile = self::$includeFile;
|
||||||
|
$includeFile($file);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the file where the class is defined.
|
||||||
|
*
|
||||||
|
* @param string $class The name of the class
|
||||||
|
*
|
||||||
|
* @return string|false The path if found, false otherwise
|
||||||
|
*/
|
||||||
|
public function findFile($class)
|
||||||
|
{
|
||||||
|
// class map lookup
|
||||||
|
if (isset($this->classMap[$class])) {
|
||||||
|
return $this->classMap[$class];
|
||||||
|
}
|
||||||
|
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||||
|
if ($hit) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->findFileWithExtension($class, '.php');
|
||||||
|
|
||||||
|
// Search for Hack files if we are running on HHVM
|
||||||
|
if (false === $file && defined('HHVM_VERSION')) {
|
||||||
|
$file = $this->findFileWithExtension($class, '.hh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $this->apcuPrefix) {
|
||||||
|
apcu_add($this->apcuPrefix.$class, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $file) {
|
||||||
|
// Remember that this class does not exist.
|
||||||
|
$this->missingClasses[$class] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently registered loaders indexed by their corresponding vendor directories.
|
||||||
|
*
|
||||||
|
* @return self[]
|
||||||
|
*/
|
||||||
|
public static function getRegisteredLoaders()
|
||||||
|
{
|
||||||
|
return self::$registeredLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $class
|
||||||
|
* @param string $ext
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
private function findFileWithExtension($class, $ext)
|
||||||
|
{
|
||||||
|
// PSR-4 lookup
|
||||||
|
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
|
||||||
|
$first = $class[0];
|
||||||
|
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||||
|
$subPath = $class;
|
||||||
|
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||||
|
$subPath = substr($subPath, 0, $lastPos);
|
||||||
|
$search = $subPath . '\\';
|
||||||
|
if (isset($this->prefixDirsPsr4[$search])) {
|
||||||
|
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||||
|
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||||
|
if (file_exists($file = $dir . $pathEnd)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-4 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 lookup
|
||||||
|
if (false !== $pos = strrpos($class, '\\')) {
|
||||||
|
// namespaced class name
|
||||||
|
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||||
|
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||||
|
} else {
|
||||||
|
// PEAR-like class name
|
||||||
|
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->prefixesPsr0[$first])) {
|
||||||
|
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||||
|
if (0 === strpos($class, $prefix)) {
|
||||||
|
foreach ($dirs as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 fallback dirs
|
||||||
|
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||||
|
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSR-0 include paths.
|
||||||
|
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function initializeIncludeClosure()
|
||||||
|
{
|
||||||
|
if (self::$includeFile !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope isolated include.
|
||||||
|
*
|
||||||
|
* Prevents access to $this/self from included files.
|
||||||
|
*
|
||||||
|
* @param string $file
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
self::$includeFile = \Closure::bind(static function($file) {
|
||||||
|
include $file;
|
||||||
|
}, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
359
vendor/composer/InstalledVersions.php
vendored
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer;
|
||||||
|
|
||||||
|
use Composer\Autoload\ClassLoader;
|
||||||
|
use Composer\Semver\VersionParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is copied in every Composer installed project and available to all
|
||||||
|
*
|
||||||
|
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||||
|
*
|
||||||
|
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
*/
|
||||||
|
class InstalledVersions
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var mixed[]|null
|
||||||
|
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||||
|
*/
|
||||||
|
private static $installed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool|null
|
||||||
|
*/
|
||||||
|
private static $canGetVendors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array[]
|
||||||
|
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static $installedByVendor = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackages()
|
||||||
|
{
|
||||||
|
$packages = array();
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
$packages[] = array_keys($installed['versions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 === \count($packages)) {
|
||||||
|
return $packages[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all package names with a specific type e.g. 'library'
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @return string[]
|
||||||
|
* @psalm-return list<string>
|
||||||
|
*/
|
||||||
|
public static function getInstalledPackagesByType($type)
|
||||||
|
{
|
||||||
|
$packagesByType = array();
|
||||||
|
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
foreach ($installed['versions'] as $name => $package) {
|
||||||
|
if (isset($package['type']) && $package['type'] === $type) {
|
||||||
|
$packagesByType[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packagesByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package is installed
|
||||||
|
*
|
||||||
|
* This also returns true if the package name is provided or replaced by another package
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @param bool $includeDevRequirements
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (isset($installed['versions'][$packageName])) {
|
||||||
|
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the given package satisfies a version constraint
|
||||||
|
*
|
||||||
|
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||||
|
*
|
||||||
|
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||||
|
*
|
||||||
|
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||||
|
* @param string $packageName
|
||||||
|
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||||
|
{
|
||||||
|
$constraint = $parser->parseConstraints((string) $constraint);
|
||||||
|
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||||
|
|
||||||
|
return $provided->matches($constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||||
|
*
|
||||||
|
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||||
|
* whether a given version of a package is installed, and not just whether it exists
|
||||||
|
*
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string Version constraint usable with composer/semver
|
||||||
|
*/
|
||||||
|
public static function getVersionRanges($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = array();
|
||||||
|
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||||
|
}
|
||||||
|
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||||
|
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' || ', $ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||||
|
*/
|
||||||
|
public static function getPrettyVersion($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['pretty_version'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||||
|
*/
|
||||||
|
public static function getReference($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed['versions'][$packageName]['reference'];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $packageName
|
||||||
|
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||||
|
*/
|
||||||
|
public static function getInstallPath($packageName)
|
||||||
|
{
|
||||||
|
foreach (self::getInstalled() as $installed) {
|
||||||
|
if (!isset($installed['versions'][$packageName])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||||
|
*/
|
||||||
|
public static function getRootPackage()
|
||||||
|
{
|
||||||
|
$installed = self::getInstalled();
|
||||||
|
|
||||||
|
return $installed[0]['root'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw installed.php data for custom implementations
|
||||||
|
*
|
||||||
|
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||||
|
*/
|
||||||
|
public static function getRawData()
|
||||||
|
{
|
||||||
|
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
self::$installed = include __DIR__ . '/installed.php';
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||||
|
*
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
public static function getAllRawData()
|
||||||
|
{
|
||||||
|
return self::getInstalled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lets you reload the static array from another file
|
||||||
|
*
|
||||||
|
* This is only useful for complex integrations in which a project needs to use
|
||||||
|
* this class but then also needs to execute another project's autoloader in process,
|
||||||
|
* and wants to ensure both projects have access to their version of installed.php.
|
||||||
|
*
|
||||||
|
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||||
|
* the data it needs from this class, then call reload() with
|
||||||
|
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||||
|
* the project in which it runs can then also use this class safely, without
|
||||||
|
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||||
|
*
|
||||||
|
* @param array[] $data A vendor/composer/installed.php data set
|
||||||
|
* @return void
|
||||||
|
*
|
||||||
|
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||||
|
*/
|
||||||
|
public static function reload($data)
|
||||||
|
{
|
||||||
|
self::$installed = $data;
|
||||||
|
self::$installedByVendor = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||||
|
*/
|
||||||
|
private static function getInstalled()
|
||||||
|
{
|
||||||
|
if (null === self::$canGetVendors) {
|
||||||
|
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||||
|
}
|
||||||
|
|
||||||
|
$installed = array();
|
||||||
|
|
||||||
|
if (self::$canGetVendors) {
|
||||||
|
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||||
|
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||||
|
$installed[] = self::$installedByVendor[$vendorDir];
|
||||||
|
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require $vendorDir.'/composer/installed.php';
|
||||||
|
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||||
|
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||||
|
self::$installed = $installed[count($installed) - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === self::$installed) {
|
||||||
|
// only require the installed.php file if this file is loaded from its dumped location,
|
||||||
|
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||||
|
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||||
|
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||||
|
$required = require __DIR__ . '/installed.php';
|
||||||
|
self::$installed = $required;
|
||||||
|
} else {
|
||||||
|
self::$installed = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::$installed !== array()) {
|
||||||
|
$installed[] = self::$installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $installed;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
vendor/composer/LICENSE
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
30
vendor/composer/autoload_classmap.php
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_classmap.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||||
|
'Google_AccessToken_Revoke' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_AccessToken_Verify' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_AuthHandler_AuthHandlerFactory' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_AuthHandler_Guzzle6AuthHandler' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_AuthHandler_Guzzle7AuthHandler' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Client' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Collection' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Http_Batch' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Http_MediaFileUpload' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Http_REST' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Model' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Service' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Service_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Service_Resource' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Task_Composer' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Task_Exception' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Task_Retryable' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Task_Runner' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
'Google_Utils_UriTemplate' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
);
|
||||||
15
vendor/composer/autoload_files.php
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// autoload_files.php @generated by Composer
|
||||||
|
|
||||||
|
$vendorDir = dirname(__DIR__);
|
||||||
|
$baseDir = dirname($vendorDir);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||||
|
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||||
|
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||||
|
'1f87db08236948d07391152dccb70f04' => $vendorDir . '/google/apiclient-services/autoload.php',
|
||||||
|
'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
|
||||||
|
'a8d3953fd9959404dd22d3dfcd0a79f0' => $vendorDir . '/google/apiclient/src/aliases.php',
|
||||||
|
);
|
||||||