Compare commits

..

10 Commits

Author SHA1 Message Date
Flatlogic Bot
c225ebebc1 after login 2025-12-10 00:53:11 +00:00
Flatlogic Bot
8a6a18d0c2 سسشسسسس 2025-12-08 20:42:20 +00:00
Flatlogic Bot
f28a0493a0 Aaaaaaaaa 2 2025-12-08 14:31:09 +00:00
Flatlogic Bot
41e2f42e41 Aaaaaaa 2025-12-08 14:28:21 +00:00
Flatlogic Bot
f7f8238fee Gggggggggggggggggggg 2025-12-08 11:33:59 +00:00
Flatlogic Bot
7da45b4e24 qqqqqqqqqqqqqqqqqqqqqq 2025-12-07 20:08:31 +00:00
Flatlogic Bot
6c608b6ba5 پررررررررر 2025-12-06 16:02:15 +00:00
Flatlogic Bot
3f48850ff5 راتاااااااا 2025-12-04 21:21:10 +00:00
Flatlogic Bot
20dd5c8f61 3 ver 2025-12-03 22:04:46 +00:00
Flatlogic Bot
f745cb0f27 Ver2 2025-12-01 13:59:23 +00:00
32238 changed files with 3967149 additions and 1253 deletions

View File

@ -0,0 +1 @@
{"labels":["1404-09"],"data":[2940000]}

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules/
*/node_modules/
*/build/
# Ignore environment files
.env

60
about.php Normal file
View 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
View 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"}

View File

@ -1,81 +1,90 @@
<?php
session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/header.php';
$flash_message = $_SESSION['flash_message'] ?? null;
if ($flash_message) {
unset($_SESSION['flash_message']);
}
?>
<!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="row justify-content-center">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="font-lalezar">افزودن محصول جدید</h1>
<a href="index.php" class="btn btn-outline-light">بازگشت</a>
</div>
<div class="card bg-dark-2">
<div class="card-body p-4">
<style>
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
</style>
<div class="admin-header">
<h1>افزودن محصول جدید</h1>
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
</div>
<div class="card">
<div class="card-body">
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
<div class="mb-3">
<div class="form-group">
<label for="name" class="form-label">نام محصول</label>
<input type="text" class="form-control bg-dark text-white" id="name" name="name" required>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<div class="form-group">
<label for="description" class="form-label">توضیحات</label>
<textarea class="form-control bg-dark text-white" id="description" name="description" rows="3" required></textarea>
<textarea class="form-control" id="description" name="description" rows="5" required></textarea>
</div>
<div class="mb-3">
<div class="form-grid">
<div class="form-group">
<label for="price" 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="price" name="price" required>
</div>
<div class="mb-3">
<div class="form-group">
<label for="stock" class="form-label">موجودی</label>
<input type="number" class="form-control" id="stock" name="stock" required value="0">
</div>
</div>
<div class="form-group">
<label for="image" class="form-label">تصویر محصول</label>
<input type="file" class="form-control bg-dark text-white" id="image" name="image" accept="image/*" required>
<input type="file" class="form-control" id="image" name="image" accept="image/*">
</div>
<div class="mb-3">
<div class="form-group">
<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>
<input type="text" class="form-control" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
</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 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>
<button type="submit" class="btn btn-primary w-100">افزودن محصول</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<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"]); ?>', // Use html to render <br> tags
html: '<?php echo addslashes($flash_message["message"]); ?>',
icon: '<?php echo $flash_message["type"]; ?>',
confirmButtonText: 'باشه'
confirmButtonText: 'باشه',
background: style.getPropertyValue('--admin-surface'),
color: style.getPropertyValue('--admin-text')
});
<?php endif; ?>
});
</script>
</body>
</html>
<?php require_once __DIR__ . '/footer.php'; ?>

251
admin/api.php Normal file
View 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']);

View 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;
}
}

View 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;
}

View File

@ -1,8 +1,8 @@
<?php
session_start();
require_once __DIR__ . '/auth_handler.php';
// 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');
exit;
}

8
admin/auth_handler.php Normal file
View 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
View File

@ -0,0 +1 @@
{"labels":[],"data":[]}

119
admin/dashboard.php Normal file
View 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';
?>

View File

@ -3,15 +3,10 @@ session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/../db/config.php';
$flash_message = $_SESSION['flash_message'] ?? null;
if ($flash_message) {
unset($_SESSION['flash_message']);
}
$product_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($product_id <= 0) {
header('Location: index.php');
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$product_id) {
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'شناسه محصول نامعتبر است.'];
header('Location: products.php');
exit;
}
@ -20,90 +15,113 @@ try {
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
header('Location: index.php');
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'محصول مورد نظر یافت نشد.'];
header('Location: products.php');
exit;
}
} 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">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="font-lalezar mb-4">ویرایش محصول</h1>
<div class="card bg-dark-2">
<div class="card-body p-4">
<form action="handler.php?action=edit" method="POST" enctype="multipart/form-data">
<style>
.form-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.image-preview-container {
background-color: var(--admin-bg);
border: 1px dashed var(--admin-border);
padding: 1rem;
border-radius: 8px;
text-align: center;
}
.image-preview {
max-width: 100%;
height: auto;
max-height: 200px;
border-radius: 8px;
margin-bottom: 1rem;
}
</style>
<div class="admin-header">
<h1>ویرایش محصول: <?php echo htmlspecialchars($product['name']); ?></h1>
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
</div>
<form action="handler.php?action=edit" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
<div class="mb-3">
<div class="form-grid">
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="name" class="form-label">نام محصول</label>
<input type="text" class="form-control bg-dark text-white" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">توضیحات</label>
<textarea class="form-control bg-dark text-white" id="description" name="description" rows="3" required><?php echo htmlspecialchars($product['description']); ?></textarea>
</div>
<div class="mb-3">
<label for="price" class="form-label">قیمت (به تومان)</label>
<input type="number" class="form-control bg-dark text-white" id="price" name="price" min="0" value="<?php echo htmlspecialchars($product['price']); ?>" 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" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>">
<div class="form-text">رنگ‌های موجود را با کاما از هم جدا کنید (مثال: #FFFFFF, #000000).</div>
</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/*">
<div class="form-text mt-2">تصویر فعلی:</div>
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" class="img-thumbnail mt-2" width="100">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="is_featured" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?>>
<label class="form-check-label" for="is_featured">نمایش در محصولات ویژه</label>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a href="index.php" class="btn btn-secondary">انصراف</a>
<button type="submit" class="btn btn-primary">به‌روزرسانی محصول</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<div class="form-group">
<label for="description" class="form-label">توضیحات</label>
<textarea class="form-control" id="description" name="description" rows="5" required><?php echo htmlspecialchars($product['description']); ?></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
<div class="form-group">
<label for="price" class="form-label">قیمت (تومان)</label>
<input type="number" class="form-control" id="price" name="price" value="<?php echo htmlspecialchars($product['price']); ?>" required>
</div>
<div class="form-group">
<label for="stock" class="form-label">موجودی</label>
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
</div>
</div>
<div class="form-group">
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
<input type="text" class="form-control" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>" placeholder="مثال: #8B4513, #2C2C2C">
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?> style="width: 20px; height: 20px;">
<span>این یک محصول ویژه است</span>
</label>
</div>
</div>
</div>
<div class="card">
<div class="card-header">تصویر محصول</div>
<div class="card-body">
<div class="image-preview-container">
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" id="image-preview" class="image-preview">
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(event)">
<small style="color: var(--admin-text-muted); margin-top: 0.5rem; display: block;">برای تغییر، تصویر جدیدی انتخاب کنید.</small>
</div>
</div>
</div>
</div>
<div style="text-align: left; margin-top: 2rem;">
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> ذخیره تغییرات</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function () {
<?php if ($flash_message): ?>
Swal.fire({
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
html: '<?php echo addslashes($flash_message["message"]); ?>', // Use html to render <br> tags
icon: '<?php echo $flash_message["type"]; ?>',
confirmButtonText: 'باشه'
});
<?php endif; ?>
});
function previewImage(event) {
const reader = new FileReader();
reader.onload = () => document.getElementById('image-preview').src = reader.result;
if (event.target.files[0]) reader.readAsDataURL(event.target.files[0]);
}
</script>
</body>
</html>
<?php require_once __DIR__ . '/footer.php'; ?>

35
admin/footer.php Normal file
View 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>

View File

@ -1,169 +1,77 @@
<?php
session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/auth_check.php';
$action = $_REQUEST['action'] ?? '';
$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;
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: index.php');
exit;
}
// Redirect back after the action
header('Location: ' . $redirect_to);
$action = $_POST['action'] ?? '';
if ($action === 'update_order_status') {
$order_id = filter_input(INPUT_POST, 'order_id', FILTER_VALIDATE_INT);
$status = filter_input(INPUT_POST, 'status', FILTER_SANITIZE_STRING);
$allowed_statuses = ['Processing', 'Shipped', 'Delivered', 'Cancelled'];
if ($order_id && $status && in_array($status, $allowed_statuses)) {
try {
$pdo = db();
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$status, $order_id]);
$_SESSION['success_message'] = "وضعیت سفارش #{$order_id} با موفقیت به '{$status}' تغییر یافت.";
} catch (PDOException $e) {
error_log("Order status update failed: " . $e->getMessage());
$_SESSION['error_message'] = "خطایی در به‌روزرسانی وضعیت سفارش رخ داد.";
}
} else {
$_SESSION['error_message'] = "اطلاعات نامعتبر برای به‌روزرسانی وضعیت.";
}
}
if ($action === 'add_user') {
$first_name = filter_input(INPUT_POST, 'first_name', FILTER_SANITIZE_STRING);
$last_name = filter_input(INPUT_POST, 'last_name', FILTER_SANITIZE_STRING);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$phone = filter_input(INPUT_POST, 'phone', FILTER_SANITIZE_STRING);
$password = $_POST['password'] ?? '';
$is_admin = filter_input(INPUT_POST, 'is_admin', FILTER_VALIDATE_INT) ? 1 : 0;
if ($first_name && $last_name && $email && !empty($password)) {
try {
$pdo = db();
// Check if user already exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$stmt->execute([$email]);
if ($stmt->fetch()) {
$_SESSION['error_message'] = "کاربری با این ایمیل از قبل وجود دارد.";
header('Location: users.php');
exit;
}
// Hash password
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Insert user
$stmt = $pdo->prepare("INSERT INTO users (first_name, last_name, email, phone, password, is_admin, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())");
$stmt->execute([$first_name, $last_name, $email, $phone, $hashed_password, $is_admin]);
$_SESSION['success_message'] = "کاربر جدید با موفقیت اضافه شد.";
} catch (PDOException $e) {
error_log("Add user failed: " . $e->getMessage());
$_SESSION['error_message'] = "خطایی در افزودن کاربر جدید رخ داد.";
}
} else {
$_SESSION['error_message'] = "اطلاعات وارد شده نامعتبر است. لطفاً تمام فیلدهای ستاره‌دار را پر کنید.";
}
header('Location: users.php');
exit;
}
header('Location: orders.php');
exit;
?>

38
admin/header.php Normal file
View 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>

View File

@ -3,12 +3,35 @@ session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/../db/config.php';
$page_title = 'داشبورد';
require_once __DIR__ . '/header.php';
$dashboard_error = null;
$total_products = 0;
$total_orders = 0;
$recent_orders = [];
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, name, price FROM products ORDER BY created_at DESC");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total_products = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
$total_orders = $pdo->query("SELECT COUNT(*) FROM orders")->fetchColumn();
$recent_orders_query = "
SELECT
o.id,
COALESCE(CONCAT(u.first_name, ' ', u.last_name), o.billing_name) AS customer_name,
o.total_amount,
o.status,
o.created_at
FROM orders AS o
LEFT JOIN users AS u ON o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 5
";
$recent_orders = $pdo->query($recent_orders_query)->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die("Error fetching products: " . $e->getMessage());
$dashboard_error = "<strong>خطا در بارگذاری اطلاعات:</strong> " . $e->getMessage();
}
$flash_message = $_SESSION['flash_message'] ?? null;
@ -16,102 +39,87 @@ if ($flash_message) {
unset($_SESSION['flash_message']);
}
function get_status_badge_class($status) {
switch (strtolower($status)) {
case 'processing': return 'status-processing';
case 'shipped': return 'status-shipped';
case 'delivered': return 'status-delivered';
case 'cancelled': return 'status-cancelled';
default: return 'status-pending';
}
}
?>
<!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>
<div class="d-flex gap-2">
<a href="add_product.php" class="btn btn-success">+ افزودن محصول جدید</a>
<a href="logout.php" class="btn btn-outline-danger">خروج</a>
<?php if ($flash_message): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
Swal.fire({
title: '<?php echo $flash_message["type"] === "success" ? "موفق" : "خطا"; ?>',
html: '<?php echo addslashes($flash_message["message"]); ?>',
icon: '<?php echo $flash_message["type"]; ?>',
confirmButtonText: 'باشه',
background: 'var(--admin-surface)',
color: 'var(--admin-text)'
});
});
</script>
<?php endif; ?>
<?php if ($dashboard_error): ?>
<div class="card"><div class="card-body" style="color: var(--admin-danger);"><?php echo $dashboard_error; ?></div></div>
<?php else: ?>
<div class="stat-cards-grid">
<div class="stat-card">
<div class="icon bg-primary"><i class="fas fa-box"></i></div>
<div class="stat-info">
<p>کل محصولات</p>
<h3><?php echo htmlspecialchars($total_products); ?></h3>
</div>
</div>
<div class="stat-card">
<div class="icon bg-warning"><i class="fas fa-receipt"></i></div>
<div class="stat-info">
<p>کل سفارشات</p>
<h3><?php echo htmlspecialchars($total_orders); ?></h3>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-dark table-striped table-hover">
<div class="card">
<div class="card-header">آخرین سفارشات</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">نام محصول</th>
<th scope="col">قیمت</th>
<th scope="col">عملیات</th>
<th>شماره سفارش</th>
<th>نام مشتری</th>
<th>مبلغ کل</th>
<th>وضعیت</th>
<th>تاریخ</th>
</tr>
</thead>
<tbody>
<?php if (empty($products)): ?>
<tr>
<td colspan="4" class="text-center">هیچ محصولی یافت نشد.</td>
</tr>
<?php if (empty($recent_orders)): ?>
<tr><td colspan="5" style="text-align: center; padding: 2rem;">هیچ سفارشی یافت نشد.</td></tr>
<?php else: ?>
<?php foreach ($products as $product): ?>
<?php foreach ($recent_orders as $order): ?>
<tr>
<th scope="row"><?php echo htmlspecialchars($product['id']); ?></th>
<td><?php echo htmlspecialchars($product['name']); ?></td>
<td><?php echo number_format($product['price']); ?> تومان</td>
<td>
<a href="edit_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-primary">ویرایش</a>
<a href="handler.php?action=delete&id=<?php echo $product['id']; ?>" class="btn btn-sm btn-danger delete-btn">حذف</a>
</td>
<td>#<?php echo htmlspecialchars($order['id']); ?></td>
<td><?php echo htmlspecialchars($order['customer_name']); ?></td>
<td><?php echo number_format($order['total_amount']); ?> تومان</td>
<td><span class="status-badge <?php echo get_status_badge_class($order['status']); ?>"><?php echo htmlspecialchars($order['status']); ?></span></td>
<td><?php echo date('Y-m-d', strtotime($order['created_at'])); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<div 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>
<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; ?>
<?php endif; ?>
// Delete confirmation
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>
<?php require_once __DIR__ . '/footer.php'; ?>

View File

@ -1,7 +1,6 @@
<?php
session_start();
// If the user is already logged in, redirect them to the admin dashboard
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
header('Location: index.php');
exit;
@ -9,56 +8,62 @@ if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
$error = '';
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';
if (isset($_POST['password']) && $_POST['password'] === $hardcoded_password) {
// On successful login, set a session variable
$_SESSION['is_admin'] = true;
header('Location: index.php');
exit;
} else {
$error = 'رمز عبور اشتباه است.';
$error = 'رمز عبور وارد شده اشتباه است.';
}
}
$page_title = 'ورود به پنل مدیریت';
?>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ورود به پنل مدیریت</title>
<title><?= $page_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(); ?>">
<link rel="stylesheet" href="assets/css/admin_main.css?v=<?= time(); ?>">
</head>
<body class="bg-dark text-white">
<body class="admin-theme">
<div class="admin-login-wrapper">
<div class="admin-login-box">
<h2>پنل مدیریت آتیمه</h2>
<p>برای دسترسی به پنل، لطفاً وارد شوید.</p>
<div class="container">
<div class="row justify-content-center align-items-center" style="height: 100vh;">
<div class="col-md-4">
<div class="card bg-dark-2">
<div class="card-body p-4">
<h1 class="font-lalezar text-center mb-4">ورود به پنل</h1>
<p class="text-center text-muted mb-4">رمز عبور: admin123</p>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<div class="alert alert-danger mb-3"><?= $error; ?></div>
<p class="text-center text-muted mb-4">رمز عبور پیش‌فرض: <code>admin123</code></p>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<form method="POST" action="login.php">
<div class="form-group">
<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 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>
</div>
</body>
</html>
<style>
.alert-danger {
background-color: var(--admin-danger-bg, #fef2f2);
border: 1px solid var(--admin-danger-border, #fecaca);
color: var(--admin-danger-text, #991b1b);
padding: 0.8rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
}
.w-100 { width: 100%; }
</style>

45
admin/nav.php Normal file
View 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
View 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">&times;</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
View 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
View 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
View File

@ -0,0 +1 @@
<?php phpinfo(); ?>

136
admin/users.php Normal file
View 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
View 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
View 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.']);
}
}

View File

@ -1,291 +1,579 @@
/*
:root variables for the default Light Theme
*/
:root {
--primary-color: #8B4513; /* SaddleBrown */
--secondary-color: #D2B48C; /* Tan */
--accent-color: #C0A080;
--background-color: #FDFBF7;
--surface-color: #FFFFFF; /* For cards, headers, etc. */
--text-color: #4A4A4A;
--heading-color: #2F2F2F;
--border-color: #EAEAEA;
--footer-bg: #2C2C2C;
--white-color: #FFFFFF;
--font-family-sans-serif: 'Montserrat', sans-serif;
--body-font: 'Montserrat', sans-serif;
--status-default: #444;
--status-default-dark: #6c757d;
--status-processing: #ffc107;
--status-shipped: #0d6efd;
--status-completed: #198754;
--status-cancelled: #dc3545;
}
/*
Variables for the Dark Theme
We will apply these by adding a class="dark" to the <html> tag
*/
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;
.about-us-list {
width: 80vw;
display: grid;
list-style: none;
padding: 0;
font-size: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
grid-template-columns: repeat(3, 1fr);
justify-items: center;
margin: 0 auto;
gap: 20px;
}
.color-swatches .btn-check:checked + .btn {
transform: scale(1.15);
box-shadow: 0 0 0 3px var(--surface-color), 0 0 0 5px var(--primary-color);
.about-us-item {
width: 20vw;
min-width: 200px;
border-radius: 20px;
text-align: center;
border: 1px solid #ebebeb;
}
/* Handle specific colors that need border in light mode */
.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;
.inner {
position: relative;
background-color: var(--border-color); /* Placeholder bg */
aspect-ratio: 3 / 4; /* Enforce 3:4 aspect ratio */
inset: 0px;
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%;
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;
}
/* --- Responsive Design --- */
@media (max-width: 768px) {
.display-3 {
font-size: 2.5rem;
}
.display-4 {
.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;
}
.display-5 {
font-size: 1.75rem;
}
line-height: 1;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.hero-section {
height: auto; /* Adjust height for mobile */
padding: 100px 0;
}
.modal-close-btn:hover {
opacity: 1;
}
.product-card .product-info {
padding: 15px;
}
.modal-body {
padding: 1.5rem;
display: grid;
gap: 2rem;
}
.site-footer {
.order-summary,
.shipping-details,
.products-list,
.status-details {
background-color: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 10px;
}
.order-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.modal-body h4 {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 500;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 0.5rem;
}
.detail-item {
font-size: 0.95rem;
}
.detail-item strong {
color: #a0a0a0;
margin-left: 8px;
}
.status-tracker {
position: relative;
display: flex;
justify-content: space-between;
padding: 20px 0;
margin-top: 20px;
}
.status-tracker::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
width: calc(100% - 40px);
margin: 0 20px;
height: 4px;
background-color: var(--status-default);
z-index: 1;
}
.status-progress {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 20px;
height: 4px;
z-index: 2;
transition: width 0.5s ease, background-color 0.5s ease;
}
.status-step {
position: relative;
z-index: 3;
text-align: center;
width: 100px;
}
.status-step .dot {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: var(--status-default);
border: 3px solid #2c2c2c;
margin: 0 auto;
transform: translateY(-8px);
transition: background-color 0.5s ease, box-shadow 0.3s ease;
}
.status-step .label {
display: block;
margin-top: 10px;
font-size: 0.8rem;
color: #a0a0a0;
transition: color 0.5s ease;
}
.status-step.completed .label,
.status-step.active .label {
color: #f0f0f0;
font-weight: 500;
}
.status-step.active .dot {
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
}
.status-tracker.is-cancelled .status-step {
opacity: 0.5;
}
.status-tracker.is-cancelled .label {
color: var(--status-cancelled);
}
#modal-products-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.product-item {
display: flex;
align-items: center;
gap: 1rem;
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 8px;
}
.product-item img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
}
.product-info {
flex-grow: 1;
}
.product-name {
display: block;
font-weight: 500;
}
.product-quantity {
font-size: 0.9rem;
color: #a0a0a0;
}
.product-price {
font-weight: bold;
}
.product-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.9rem;
color: #a0a0a0;
margin-top: 4px;
}
.product-color-wrapper {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.product-color-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
display: inline-block;
}
@media (max-width: 768px) {
footer {
text-align: center;
}
.site-footer .col-lg-2, .site-footer .col-lg-3, .site-footer .col-lg-4 {
margin-bottom: 30px;
footer .row > * {
margin-bottom: 1.5rem;
}
footer .row {
justify-content: center;
}
/* Center social icons and contact list items on mobile */
footer .social-icons,
footer .list-unstyled .d-flex {
justify-content: center;
}
}
/* --- Modern Login Page --- */
.login-page-modern {
background-color: var(--color-dark-bg);
background-image: url('../images/pexels/about-us-35056828.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
position: relative;
}
.login-page-modern::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(17, 17, 17, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.login-container {
position: relative;
z-index: 2;
width: 100%;
max-width: 450px;
padding: 2rem;
}
.login-form-wrapper {
background-color: var(--color-surface);
padding: 2.5rem;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.login-header .logo-link {
text-decoration: none;
}
.login-header .logo-title {
color: var(--color-gold);
font-weight: 800;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.login-header .tagline {
color: var(--color-text-secondary);
font-size: 1rem;
font-weight: 300;
}
.login-form-wrapper .form-title {
color: var(--color-text-primary);
font-weight: 600;
}
.form-floating > .form-control {
background-color: var(--color-dark-bg);
border-color: var(--color-border);
}
.form-floating > .form-control:focus {
background-color: var(--color-dark-bg);
}
.form-floating > label {
color: var(--color-text-secondary);
}
.login-form-wrapper .btn-primary {
padding: 0.8rem;
font-size: 1.1rem;
letter-spacing: 0.5px;
}
.auth-footer a {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.auth-footer a:hover {
color: var(--color-gold);
}
@media (max-width: 576px) {
.login-container {
padding: 1rem;
}
}
@media (min-width: 992px) {
.login-container {
max-width: 600px;
padding: 0;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.login-container {
max-width: 500px;
}
.login-form-wrapper {
padding: 2rem 1.5rem;
}
.login-header .logo-title {
font-size: 2rem;
}
}
/* --- Login Toggle Switch --- */
.login-toggle-container .btn-group {
border: 1px solid var(--color-border);
border-radius: var(--bs-border-radius-lg); /* Match form control radius */
overflow: hidden;
}
.login-toggle-container .btn-outline-primary {
background-color: transparent;
color: var(--color-text-secondary);
border-color: transparent; /* Remove individual button borders */
transition: background-color 0.3s ease, color 0.3s ease;
padding: 0.75rem 1rem;
font-size: 1rem;
flex-grow: 1;
}
.login-toggle-container .btn-outline-primary:hover {
background-color: rgba(var(--bs-primary-rgb), 0.1);
color: var(--color-gold);
}
.login-toggle-container .btn-check:checked + .btn-outline-primary {
background-color: var(--color-gold);
color: var(--color-dark-bg);
border-color: var(--color-gold);
box-shadow: 0 0 10px rgba(var(--bs-primary-rgb), 0.3);
}
.login-toggle-container .btn-check:focus + .btn-outline-primary {
box-shadow: none;
}
/* --- Divider --- */
.divider-with-text {
display: flex;
align-items: center;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.divider-with-text::before,
.divider-with-text::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--color-border);
}
.divider-with-text:not(:empty)::before {
margin-left: .5em;
}
.divider-with-text:not(:empty)::after {
margin-right: .5em;
}
/* --- Google Sign-in Button --- */
.btn-google {
background-color: #ffffff;
color: #424242;
border: 1px solid #dcdcdc;
transition: background-color 0.3s, border-color 0.3s;
}
.btn-google:hover {
background-color: #f5f5f5;
border-color: #c5c5c5;
color: #212121;
}
.btn-google .ri-google-fill {
color: #DB4437;
font-size: 1.3em;
}
.btn-google span {
font-weight: 500;
}
/* --- Hero Video Section --- */
.hero-video-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -1; /* Changed from 1 to -1 */
}
.hero-video-background video {
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
object-fit: cover;
}
.hero-video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.hero-section .container {
z-index: 2;
}

325
assets/css/theme.css Normal file
View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View 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);
}
}
});
}
});

View File

@ -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();
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

232
auth_handler.php Normal file
View 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();
}
}
?>

208
cart.php
View File

@ -1,121 +1,111 @@
<?php
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 = 'سبد خرید';
include 'includes/header.php';
require_once 'includes/header.php';
$cart_items = $_SESSION['cart'] ?? [];
$total_price = 0;
?>
<div class="text-center mb-5">
<h1 class="display-4 fw-bold">سبد خرید شما</h1>
</div>
<main>
<section class="section-padding">
<div class="container">
<?php if (empty($cart_items_detailed)): ?>
<div class="text-center p-5 bg-light rounded-3">
<p class="lead">سبد خرید شما خالی است.</p>
<a href="shop.php" class="btn btn-primary">بازگشت به فروشگاه</a>
<?php if (empty($cart_items)): ?>
<div class="card card-body text-center p-4 p-md-5" data-aos="fade-up">
<div class="d-inline-block mx-auto mb-4">
<i class="ri-shopping-cart-2-line display-1 text-gold"></i>
</div>
<?php else: ?>
<form action="cart_handler.php" method="POST">
<input type="hidden" name="update_cart" value="1">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th scope="col" colspan="2">محصول</th>
<th scope="col" class="text-center">قیمت</th>
<th scope="col" class="text-center">تعداد</th>
<th scope="col" class="text-end">جمع کل</th>
<th scope="col" class="text-center">حذف</th>
</tr>
</thead>
<tbody>
<?php foreach ($cart_items_detailed as $item): ?>
<tr>
<td style="width: 100px;">
<img src="<?php echo htmlspecialchars($item['image_url']); ?>" alt="<?php echo htmlspecialchars($item['name']); ?>" class="img-fluid rounded-3">
</td>
<td>
<h5 class="mb-0"><?php echo htmlspecialchars($item['name']); ?></h5>
<?php if ($item['color']): ?>
<small class="text-muted">رنگ: <?php echo htmlspecialchars($item['color']); ?></small>
<?php endif; ?>
</td>
<td class="text-center"><strong><?php echo number_format($item['price']); ?></strong></td>
<td class="text-center" style="width: 120px;">
<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">
</td>
<td class="text-end"><strong><?php echo number_format($item['subtotal']); ?></strong></td>
<td class="text-center">
<a href="cart_handler.php?action=remove&id=<?php echo $item['cart_item_id']; ?>" class="btn btn-sm btn-outline-danger">&times;</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2 class="mb-3">سبد خرید شما خالی است</h2>
<p class="text-muted fs-5 mb-4">به نظر می‌رسد هنوز محصولی به سبد خرید خود اضافه نکرده‌اید. همین حالا گشتی در فروشگاه بزنید.</p>
<div class="d-inline-block">
<a href="shop.php" class="btn btn-primary btn-lg">
<i class="ri-store-2-line me-2"></i>
رفتن به فروشگاه
</a>
</div>
</div>
<?php else: ?>
<div class="text-center" data-aos="fade-down">
<h1 class="section-title">سبد خرید شما</h1>
<p class="text-muted fs-5">جزئیات سفارش خود را بررسی و نهایی کنید.</p>
</div>
<div class="d-flex justify-content-between align-items-center mt-4 flex-wrap gap-3">
<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>
<div class="row g-5 mt-5">
<div class="col-lg-8">
<?php foreach ($cart_items as $item_id => $item):
$item_total = $item['price'] * $item['quantity'];
$total_price += $item_total;
?>
<div class="card card-body mb-4" data-aos="fade-up">
<div class="remove-item-btn">
<form action="cart_handler.php" method="POST" class="d-inline">
<input type="hidden" name="product_id" value="<?php echo $item['product_id']; ?>">
<input type="hidden" name="product_color" value="<?php echo htmlspecialchars($item['color'] ?? ''); ?>">
<input type="hidden" name="action" value="remove">
<button type="submit" class="btn btn-link text-decoration-none p-0"><i class="ri-close-circle-line"></i></button>
</form>
<div class="text-center mt-5">
<a href="checkout.php" class="btn btn-primary btn-lg">ادامه جهت تسویه حساب</a>
</div>
<?php endif; ?>
<div class="row align-items-center g-3">
<div class="col-md-2 col-3 cart-item-image">
<a href="product.php?id=<?php echo $item['product_id']; ?>">
<img src="<?php echo htmlspecialchars($item['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($item['name']); ?>">
</a>
</div>
<div class="col-md-4 col-9 cart-item-details">
<h5><a href="product.php?id=<?php echo $item['product_id']; ?>"><?php echo htmlspecialchars($item['name']); ?></a></h5>
<?php if (!empty($item['color'])) : ?>
<div class="d-flex align-items-center">
<small class="text-muted me-2">رنگ:</small>
<span class="cart-item-color-swatch" style="background-color: <?php echo htmlspecialchars($item['color']); ?>;"></span>
</div>
<?php endif; ?>
</div>
<div class="col-md-3 col-7">
<form action="cart_handler.php" method="POST" class="quantity-selector">
<input type="hidden" name="product_id" value="<?php echo $item['product_id']; ?>">
<input type="hidden" name="product_color" value="<?php echo htmlspecialchars($item['color'] ?? ''); ?>">
<input type="hidden" name="action" value="update">
<?php include 'includes/footer.php'; ?>
<button type="submit" name="quantity" value="<?php echo $item['quantity'] + 1; ?>" class="btn">+</button>
<input type="text" value="<?php echo $item['quantity']; ?>" class="quantity-input" readonly>
<button type="submit" name="quantity" value="<?php echo $item['quantity'] - 1; ?>" class="btn" <?php echo $item['quantity'] <= 1 ? 'disabled' : ''; ?>>-</button>
</form>
</div>
<div class="col-md-3 col-5 text-end">
<span class="item-price"><?php echo number_format($item_total); ?> <small>تومان</small></span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="col-lg-4">
<div class="card card-body position-sticky" style="top: 2rem;" data-aos="fade-left">
<h4 class="card-title">خلاصه سفارش</h4>
<div class="summary-item">
<span class="label">جمع کل</span>
<span class="value"><?php echo number_format($total_price); ?> تومان</span>
</div>
<div class="summary-item">
<span class="label">هزینه ارسال</span>
<span class="value text-success">رایگان</span>
</div>
<div class="summary-total">
<div class="summary-item">
<span class="label">مبلغ نهایی</span>
<span class="value"><?php echo number_format($total_price); ?> تومان</span>
</div>
</div>
<div class="d-grid mt-4">
<a href="checkout.php" class="btn btn-primary btn-lg btn-checkout"><i class="ri-secure-payment-line me-2"></i>ادامه و پرداخت</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</section>
</main>
<?php require_once 'includes/footer.php'; ?>

View File

@ -1,70 +1,110 @@
<?php
require_once __DIR__ . '/includes/session_config.php';
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'])) {
$_SESSION['cart'] = [];
}
// Check if the form was submitted and it's an add-to-cart action
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_to_cart'])) {
$product_id = isset($_POST['product_id']) ? (int)$_POST['product_id'] : 0;
$quantity = isset($_POST['quantity']) ? (int)$_POST['quantity'] : 1;
$color = isset($_POST['color']) ? trim($_POST['color']) : null;
// Get POST data
$product_id = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
$quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);
$color = filter_input(INPUT_POST, 'product_color', FILTER_SANITIZE_STRING);
$action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING);
if ($product_id > 0 && $quantity > 0) {
// Create a unique ID for the cart item based on product ID and color
$cart_item_id = $product_id . ($color ? '-' . preg_replace('/[^a-zA-Z0-9_]/ ', '-', $color) : '');
if (!$action || !$product_id) {
header('Location: shop.php');
exit;
}
// If the exact item (product + color) is already in the cart, update the quantity
// Generate a unique ID for the cart item based on product ID and color
$cart_item_id = $product_id . ($color ? '_' . str_replace('#', '', $color) : '');
switch ($action) {
case 'add':
if ($quantity > 0) {
try {
$pdo = db();
// Fetch product details including colors
$stmt = $pdo->prepare("SELECT name, price, image_url, colors FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if ($product) {
// --- START COLOR VALIDATION ---
$available_colors = [];
if (!empty($product['colors'])) {
$colors_raw = explode(',', $product['colors']);
foreach ($colors_raw as $c) {
$trimmed_c = trim($c);
if ($trimmed_c) $available_colors[] = $trimmed_c;
}
}
if (count($available_colors) > 1 && empty($color)) {
// For multi-color products, a color must be selected.
$_SESSION['flash_message'] = [
'type' => 'warning',
'message' => 'برای افزودن این محصول، انتخاب یکی از رنگ‌ها الزامی است.'
];
header('Location: product.php?id=' . $product_id);
exit;
}
// --- END COLOR VALIDATION ---
// If item is already in the cart (same product ID and color), just update the quantity.
if (isset($_SESSION['cart'][$cart_item_id])) {
$_SESSION['cart'][$cart_item_id]['quantity'] += $quantity;
} else {
// Otherwise, add it as a new item
// 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
];
}
}
// Redirect to the cart page to show the updated cart
header('Location: cart.php');
exit;
}
// Handle removing an item from the cart
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'remove') {
$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
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_cart'])){
if(!empty($_POST['quantities'])){
foreach($_POST['quantities'] as $cart_item_id => $quantity){
$quantity = (int)$quantity;
if(!empty($cart_item_id) && isset($_SESSION['cart'][$cart_item_id])){
if($quantity > 0){
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
$_SESSION['flash_message'] = [
'type' => 'success',
'message' => 'محصول با موفقیت به سبد خرید اضافه شد!'
];
} else {
// Remove item if quantity is 0 or less
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'محصول یافت نشد.'];
}
} catch (PDOException $e) {
error_log("Cart Add Error: " . $e->getMessage());
$_SESSION['flash_message'] = [
'type' => 'error',
'message' => 'مشکلی در افزودن محصول به سبد خرید رخ داد.'
];
}
}
// Redirect back to the previous page (likely the product page) to show the flash message.
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? 'shop.php'));
exit;
case 'update':
if ($quantity > 0) {
if (isset($_SESSION['cart'][$cart_item_id])) {
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
}
} else {
// If quantity is 0 or less, remove the item.
unset($_SESSION['cart'][$cart_item_id]);
}
break;
case 'remove':
if (isset($_SESSION['cart'][$cart_item_id])) {
unset($_SESSION['cart'][$cart_item_id]);
}
}
}
header('Location: cart.php');
exit;
break;
}
// If someone accesses this file directly without a valid action, redirect them to the shop.
header('Location: shop.php');
// For 'update' and 'remove' actions, redirect to the cart page to show changes.
header('Location: cart.php');
exit;

View File

@ -1,174 +1,194 @@
<?php
session_start();
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'])) {
header('Location: shop.php');
exit;
exit();
}
$p_title = "تسویه حساب";
$order_placed_successfully = false;
$error_message = '';
$cart_items = $_SESSION['cart'];
$product_details = [];
$total_price = 0;
// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// --- Data Validation ---
$name = trim($_POST['customer_name'] ?? '');
$email = trim($_POST['customer_email'] ?? '');
$address = trim($_POST['customer_address'] ?? '');
if (empty($name) || empty($email) || empty($address)) {
$error_message = 'لطفاً تمام فیلدها را پر کنید.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error_message = 'لطفاً یک آدرس ایمیل معتبر وارد کنید.';
// 2. Process cart items directly from the session
foreach ($cart_items as $cart_item_id => $item) {
// Ensure item has required data
if (!isset($item['price'], $item['quantity'], $item['name'], $item['image_url'])) {
// Skip malformed items
continue;
}
if(empty($error_message)) {
$pdo = db();
try {
// --- Server-side recalculation of total ---
$product_ids = array_keys($_SESSION['cart']);
$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);
$item_total = $item['price'] * $item['quantity'];
$total_price += $item_total;
$total_amount = 0;
foreach ($_SESSION['cart'] as $product_id => $quantity) {
if(isset($products_from_db[$product_id])){
$total_amount += $products_from_db[$product_id]['price'] * $quantity;
}
}
// 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
];
}
// --- 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
// 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']);
$order_placed_successfully = true;
$p_title = "سفارش شما ثبت شد";
header('Location: shop.php');
exit();
}
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("Checkout Error: " . $e->getMessage());
$error_message = 'مشکلی در ثبت سفارش شما به وجود آمد. لطفاً دوباره تلاش کنید.';
}
// 4. Fetch user data if logged in
$user_id = $_SESSION['user_id'] ?? null;
$user = [];
$address = [];
if ($user_id) {
try {
$pdo = db();
$user_stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$user_stmt->execute([$user_id]);
$user = $user_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$address_stmt = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY id DESC LIMIT 1");
$address_stmt->execute([$user_id]);
$address = $address_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $e) {
error_log("Checkout user fetch error: " . $e->getMessage());
// Do not block the page, guest checkout is still possible
}
}
$shipping_cost = 50000;
$grand_total = $total_price + $shipping_cost;
?>
<!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 -->
<header class="p-3 mb-3 border-bottom border-secondary">
<main class="checkout-page-wrapper">
<section class="section-padding">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="index.php" class="d-flex align-items-center mb-2 mb-lg-0 text-white text-decoration-none">
<h1 class="font-playfair fs-2" style="color: #D4AF37;">آتیمه</h1>
</a>
<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>
<li><a href="shop.php" class="nav-link px-2 text-white">فروشگاه</a></li>
</ul>
<div class="text-end">
<a href="cart.php" class="btn btn-outline-warning position-relative">
سبد خرید
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
<?php echo count($_SESSION['cart'] ?? []); ?>
</span>
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container my-5">
<div class="text-center mb-5">
<h2 class="font-lalezar display-4"><?php echo $p_title; ?></h2>
<div class="text-center" data-aos="fade-down">
<h1 class="section-title">تکمیل سفارش و پرداخت</h1>
<p class="text-muted fs-5">اطلاعات خود را برای ارسال سفارش وارد کنید.</p>
</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 class="row g-5 mt-5">
<!-- Billing Details Column -->
<div class="col-lg-8" data-aos="fade-right">
<h3 class="mb-4">جزئیات صورتحساب</h3>
<?php
if (!empty($_SESSION['checkout_errors'])) {
echo '<div class="alert alert-danger"><ul>';
foreach ($_SESSION['checkout_errors'] as $error) {
echo '<li>' . htmlspecialchars($error) . '</li>';
}
echo '</ul></div>';
// Unset the session variable so it doesn't show again on refresh
unset($_SESSION['checkout_errors']);
}
?>
<form id="checkout-form" action="checkout_handler.php" method="POST">
<div class="card mb-4">
<div class="card-header">اطلاعات تماس</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="firstName" class="form-label">نام</label>
<input type="text" class="form-control" id="firstName" name="first_name" value="<?= htmlspecialchars($user['first_name'] ?? '') ?>" required>
</div>
<?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="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="customer_name" class="form-label">نام و نام خانوادگی</label>
<input type="text" class="form-control bg-dark text-white" id="customer_name" name="customer_name" required>
<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="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 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="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 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="d-grid mt-4">
<button type="submit" class="btn btn-warning btn-lg fw-bold">ثبت سفارش و پرداخت</button>
<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>
</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">&copy; <?php echo date("Y"); ?> چرم آتیمه. تمام حقوق محفوظ است.</p>
</div>
</footer>
<!-- 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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
<div class="summary-totals">
<div class="total-row">
<span class="label">جمع کل</span>
<span class="value">T <?= number_format($total_price) ?></span>
</div>
<div class="total-row">
<span class="label">هزینه ارسال</span>
<span class="value">T <?= number_format($shipping_cost) ?></span>
</div>
<div class="total-row grand-total mt-3">
<span class="label">مبلغ قابل پرداخت</span>
<span class="value">T <?= number_format($grand_total) ?></span>
</div>
</div>
<div class="d-grid mt-4">
<button type="submit" form="checkout-form" class="btn btn-primary btn-lg">
<i class="ri-secure-payment-line me-2"></i>
پرداخت و ثبت نهایی سفارش
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<?php require_once 'includes/footer.php'; ?>

159
checkout_handler.php Normal file
View 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
View File

@ -0,0 +1,5 @@
{
"require": {
"google/apiclient": "^2.15"
}
}

1284
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,82 +1,135 @@
<?php
session_start();
require_once __DIR__ . '/mail/MailService.php';
session_start(); // Ensure session is started
$page_title = 'تماس با ما';
$page_description = 'با ما در تماس باشید. نظرات و پیشنهادات شما برای ما ارزشمند است.';
require_once 'includes/header.php';
require_once 'mail/MailService.php';
$message = '';
$error = '';
// Handle form submission
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['contact_form'])) {
$name = trim(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING));
$email = trim(filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL));
$message = trim(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING));
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$name = trim($_POST['name']);
$email = trim($_POST['email']);
$subject = trim($_POST['subject']);
$body = trim($_POST['message']);
if (empty($name) || empty($email) || empty($subject) || empty($body) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'لطفاً تمام فیلدها را به درستی پر کنید.';
if (empty($name) || empty($email) || empty($message)) {
$_SESSION['flash_message'] = ['type' => 'warning', 'message' => 'لطفاً تمام فیلدها را پر کنید.'];
} elseif (!$email) {
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'آدرس ایمیل وارد شده معتبر نیست.'];
} else {
$response = MailService::sendContactMessage($name, $email, $body, null, $subject);
if (!empty($response['success'])) {
$message = 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!';
$to_email = getenv('MAIL_TO') ?: 'support@atimeh.com'; // Fallback email
$subject = "پیام جدید از فرم تماس وب‌سایت";
$email_result = MailService::sendContactMessage($name, $email, $message, $to_email, $subject);
if (!empty($email_result['success'])) {
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!'];
} else {
$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="col-lg-8 text-center" data-aos="fade-up">
<h1 class="display-4 fw-bold"><?php echo $page_title; ?></h1>
<p class="lead text-white-50 mt-3"><?php echo $page_description; ?></p>
</div>
<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="row justify-content-center mt-5">
<div class="col-lg-8">
<div class="card border-0" style="background-color: var(--surface-color);">
<div class="card-body p-4 p-md-5">
<?php if ($message): ?>
<div class="alert alert-success" role="alert">
<?php echo $message; ?>
<div class="contact-card p-4 p-lg-5" data-aos="fade-up">
<div class="row g-5">
<div class="col-lg-5">
<div class="contact-info h-100 d-flex flex-column justify-content-center">
<h3 class="mb-4">راه‌های ارتباطی</h3>
<div class="d-flex align-items-start mb-4">
<i class="ri-map-pin-line mt-1 me-3"></i>
<div>
<strong>آدرس:</strong>
<p class="text-muted mb-0">تهران، خیابان هنر، کوچه خلاقیت، پلاک ۱۲</p>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger" role="alert">
<?php echo $error; ?>
</div>
<?php endif; ?>
<form action="contact.php" method="POST" data-aos="fade-up" data-aos-delay="200">
<div class="d-flex align-items-start mb-4">
<i class="ri-mail-line mt-1 me-3"></i>
<div>
<strong>ایمیل:</strong>
<p class="mb-0"><a href="mailto:info@atimeh.com">info@atimeh.com</a></p>
</div>
</div>
<div class="d-flex align-items-start mb-4">
<i class="ri-phone-line mt-1 me-3"></i>
<div>
<strong>تلفن:</strong>
<p class="mb-0"><a href="tel:+982112345678">۰۲۱-۱۲۳۴۵۶۷۸</a></p>
</div>
</div>
<hr class="my-4" style="border-color: var(--luxury-border);">
<h4 class="h5 mb-3">ما را دنبال کنید</h4>
<div class="d-flex gap-2">
<a href="#" class="social-btn"><i class="ri-instagram-line"></i></a>
<a href="#" class="social-btn"><i class="ri-telegram-line"></i></a>
<a href="#" class="social-btn"><i class="ri-whatsapp-line"></i></a>
</div>
</div>
</div>
<div class="col-lg-7">
<h3 class="mb-4">فرم تماس</h3>
<form action="contact.php" method="POST">
<input type="hidden" name="contact_form" value="1">
<div class="mb-4">
<label for="name" class="form-label fs-5">نام شما</label>
<input type="text" class="form-control form-control-lg bg-dark text-white" id="name" name="name" required>
<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 fs-5">ایمیل شما</label>
<input type="email" class="form-control form-control-lg bg-dark text-white" id="email" name="email" required>
<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="subject" class="form-label fs-5">موضوع</label>
<input type="text" class="form-control form-control-lg bg-dark text-white" id="subject" name="subject" required>
</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>
<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 btn-lg">ارسال پیام</button>
<button type="submit" class="btn btn-primary">ارسال پیام</button>
</div>
</form>
</div>
</div>
<div class="alert alert-info mt-4"><b>توجه:</b> این فرم برای اهداف آزمایشی است. برای دریافت واقعی ایمیل‌ها، باید اطلاعات سرور ایمیل (SMTP) خود را در فایل <code>.env</code> وارد کنید.</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'; ?>

View File

@ -15,3 +15,9 @@ function db() {
}
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');

View File

@ -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';

View File

@ -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;

View 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';

View 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;

View 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;

View 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`;

View 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;

View 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;

View File

@ -0,0 +1 @@
ALTER TABLE `orders` ADD `tracking_id` VARCHAR(255) UNIQUE NULL DEFAULT NULL AFTER `status`;

View File

@ -0,0 +1 @@
ALTER TABLE `orders` CHANGE `customer_phone` `billing_phone` VARCHAR(50) DEFAULT NULL;

View 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;

View 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;

View 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
);

View 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`);

View File

@ -0,0 +1 @@
ALTER TABLE `users` CHANGE `password` `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;

View File

@ -0,0 +1 @@
ALTER TABLE `users` ADD COLUMN `phone` VARCHAR(255) NULL AFTER `email`;

View 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;

View File

@ -0,0 +1,3 @@
ALTER TABLE `users`
MODIFY COLUMN `first_name` VARCHAR(100) NULL,
MODIFY COLUMN `last_name` VARCHAR(100) NULL;

View 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
View 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
View File

@ -0,0 +1 @@
google_callback.php was executed at 2025-12-08 19:56:56

86
faq.php Normal file
View 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
View 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
View 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"}

View File

@ -11,9 +11,10 @@
<h5 class="fw-bold mb-3">دسترسی سریع</h5>
<ul class="list-unstyled">
<li><a href="shop.php" class="text-white-50">فروشگاه</a></li>
<li><a href="#" class="text-white-50">درباره ما</a></li>
<li><a href="#" 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="about.php" class="text-white-50">درباره ما</a></li>
<li><a href="terms.php" class="text-white-50">قوانین و مقررات</a></li>
<li><a href="faq.php" class="text-white-50">سوالات متداول</a></li>
</ul>
</div>
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
@ -36,10 +37,10 @@
<div class="col-lg-3 col-md-6">
<h5 class="fw-bold mb-3">ما را دنبال کنید</h5>
<p class="text-white-50">از جدیدترین محصولات و تخفیف‌ها باخبر شوید.</p>
<div class="d-flex mt-3">
<a href="#" class="btn btn-outline-primary me-2"><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="btn btn-outline-primary"><i class="bi bi-whatsapp"></i></a>
<div class="d-flex mt-3 social-icons">
<a href="#" class="social-icon me-3"><i class="bi bi-instagram"></i></a>
<a href="#" class="social-icon me-3"><i class="bi bi-telegram"></i></a>
<a href="#" class="social-icon"><i class="bi bi-whatsapp"></i></a>
</div>
</div>
</div>
@ -49,10 +50,9 @@
</div>
</footer>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></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>
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
</div>
</body>
</html>

View File

@ -1,40 +1,82 @@
<?php
// Enforce session cookie settings BEFORE starting the session
require_once __DIR__ . '/session_config.php';
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Log page views for non-admin pages
if (strpos($_SERVER['REQUEST_URI'], '/admin/') === false) {
require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
// Check if the table exists to avoid errors before migration
$table_check = $pdo->query("SHOW TABLES LIKE 'page_views'");
if ($table_check->rowCount() > 0) {
$ip_address = $_SERVER['REMOTE_ADDR'];
$page_url = $_SERVER['REQUEST_URI'];
$stmt = $pdo->prepare("INSERT INTO page_views (page_url, ip_address) VALUES (?, ?)");
$stmt->execute([$page_url, $ip_address]);
}
} catch (PDOException $e) {
// Silently fail or log to a file to not break the page for users
error_log("Could not log page view: " . $e->getMessage());
}
}
$cart_item_count = isset($_SESSION['cart']) ? array_sum(array_column($_SESSION['cart'], 'quantity')) : 0;
$page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
?>
<!DOCTYPE html>
<html lang="fa" dir="rtl" class="dark">
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($page_title); ?></title>
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'خرید محصولات چرمی لوکس و با کیفیت.'); ?>">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- IRANSans Font -->
<link rel="stylesheet" href="https://font-ir.s3.ir-thr-at1.arvanstorage.com/IRANSans/css/IRANSans.css">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.rtl.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<!-- Remix Icon CSS -->
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.2.0/fonts/remixicon.css" rel="stylesheet" />
<!-- AOS CSS -->
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<!-- Main Theme CSS -->
<link rel="stylesheet" href="/assets/css/theme.css?v=<?php echo time(); ?>">
<!-- Custom CSS -->
<link rel="stylesheet" href="/assets/css/custom.css?v=<?php echo time(); ?>">
<script>
// Apply theme from local storage before page load to prevent flashing
(function() {
const theme = localStorage.getItem('theme') || 'dark';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
</script>
</head>
<body>
<body class="dark-luxury">
<div class="overflow-hidden">
<?php
$is_admin_page = strpos($_SERVER['REQUEST_URI'], '/admin/') !== false;
?>
<header class="site-header sticky-top py-3">
<nav class="navbar navbar-expand-lg container">
<div class="container-fluid">
<div class="container">
<a class="navbar-brand fw-bold fs-4" href="index.php">آتیمه</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
@ -50,7 +92,7 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
<a class="nav-link" href="shop.php">فروشگاه</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">درباره ما</a>
<a class="nav-link" href="about.php">درباره ما</a>
</li>
<li class="nav-item">
<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">
<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): ?>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
<?php echo $cart_item_count; ?>
@ -67,9 +109,23 @@ $page_title = $page_title ?? 'فروشگاه آتیمه'; // Default title
</span>
<?php endif; ?>
</a>
<a href="/admin/login.php" class="ms-3">
<i class="bi bi-person fs-5"></i>
<?php if (isset($_SESSION['user_id'])): ?>
<a href="profile.php" class="ms-4 d-flex align-items-center text-decoration-none" title="حساب کاربری">
<i class="ri-user-line fs-5 me-2"></i>
<span><?php echo htmlspecialchars($_SESSION['user_name']); ?></span>
</a>
<?php if (!empty($_SESSION['is_admin'])): ?>
<a href="/admin/index.php" class="ms-3" title="پنل مدیریت">
<i class="ri-shield-user-line fs-5"></i>
</a>
<?php endif; ?>
<a href="logout.php" class="ms-3" title="خروج">
<i class="ri-logout-box-r-line fs-5"></i>
</a>
<?php else: ?>
<a href="login.php" class="btn btn-primary btn-sm ms-3">ورود / ثبت‌نام</a>
<?php endif; ?>
</div>
</div>
</div>

258
includes/jdf.php Normal file
View 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);
}
?>

View 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
]);
?>

117
index.php
View File

@ -1,59 +1,77 @@
<?php
$page_title = 'صفحه اصلی';
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 -->
<section class="hero-section vh-100 d-flex align-items-center text-white text-center">
<div class="video-background-wrapper">
<div class="video-overlay"></div>
<video playsinline="playsinline" autoplay="autoplay" muted="muted" loop="loop">
<source src="https://storage.googleapis.com/gemini-agent-mediabucket-prod/v-001/video_bg.mp4" type="video/mp4">
<!-- Hero Section -->
<section class="hero-section vh-100 d-flex justify-content-center align-items-center position-relative text-white text-center">
<div class="hero-video-background">
<video playsinline autoplay muted loop poster="assets/images/pexels/about-us-34942790.jpg">
<source src="https://videos.pexels.com/video-files/8065365/8065365-hd_1920_1080_25fps.mp4" type="video/mp4">
ویدیوی شما توسط مرورگر پشتیبانی نمی‌شود.
</video>
<div class="hero-video-overlay"></div>
</div>
<div class="container position-relative">
<h1 class="display-3 fw-bold mb-3 hero-title" data-aos="fade-up">اصالت در هر نگاه</h1>
<p class="lead fs-4 mb-4 hero-subtitle" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دست‌دوز، آفریده برای ماندگاری.</p>
<a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">کاوش در مجموعه</a>
<h1 class="display-3 fw-bold" data-aos="fade-down">اصالت در هر نگاه</h1>
<p class="lead fs-4 mb-4" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دست‌دوز، آفریده برای ماندگاری.</p>
<a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">مشاهده محصولات</a>
</div>
</section>
</section>
<!-- Featured Products Section -->
<section id="featured-products" class="py-5">
<!-- Featured Products Section -->
<section id="featured-products" class="section-padding">
<div class="container">
<div class="text-center mb-5" data-aos="fade-up">
<h2 class="display-5 fw-bold">مجموعه برگزیده ما</h2>
<p class="text-white-50 fs-5">دست‌چین شده برای سلیقه‌های خاص.</p>
<?php
if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">' . $_SESSION['success_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . $_SESSION['error_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error_message']);
}
?>
<div class="section-title text-center" data-aos="fade-up">
<h1>مجموعه برگزیده ما</h1>
<p class="fs-5 text-muted">دست‌چین شده برای سلیقه‌های خاص.</p>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 g-lg-5">
<?php
require_once 'db/config.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);
$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>';
echo '<div class="col-12"><p class="text-center text-muted">هیچ محصولی برای نمایش وجود ندارد.</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>';
foreach ($featured_products as $product) {
?>
<div class="col" data-aos="fade-up" data-aos-delay="<?= $delay ?>">
<div class="product-card h-100">
<div class="product-image">
<a href="product.php?id=<?= $product['id'] ?>">
<img src="<?= htmlspecialchars($product['image_url']) ?>" alt="<?= htmlspecialchars($product['name']) ?>">
</a>
</div>
<div class="product-info text-center">
<h3 class="product-title"><a href="product.php?id=<?= $product['id'] ?>"><?= htmlspecialchars($product['name']) ?></a></h3>
<p class="product-price"><?= number_format($product['price']) ?> تومان</p>
</div>
</div>
</div>
<?php
$delay += 150;
}
}
@ -63,26 +81,31 @@ include 'includes/header.php';
}
?>
</div>
<div class="text-center mt-5" data-aos="fade-up">
<a href="shop.php" class="btn btn-outline-gold btn-lg">مشاهده تمام محصولات</a>
</div>
</div>
</section>
<!-- About Us Section -->
<section id="about-us" class="py-5 my-5">
<div class="text-center mt-5" data-aos="fade-up">
<a href="shop.php" class="btn btn-primary">مشاهده تمام محصولات</a>
</div>
</div>
</section>
<!-- About Us Section -->
<section id="about-us" class="section-padding bg-surface">
<div class="container">
<div class="row align-items-center">
<div class="row align-items-center g-5">
<div class="col-md-6" data-aos="fade-right">
<img src="https://storage.googleapis.com/gemini-agent-mediabucket-prod/v-001/about-us.jpg" alt="درباره ما" class="img-fluid rounded-4 shadow-lg">
<img src="<?= htmlspecialchars($about_us_image_url) ?>" alt="درباره ما" class="about-us-image img-fluid">
</div>
<div class="col-md-6 mt-4 mt-md-0 ps-md-5" data-aos="fade-left">
<h2 class="display-5 fw-bold">داستان آتیمه</h2>
<p class="text-white-50 fs-5 mt-3">ما در آتیمه، به تلفیق هنر سنتی و طراحی مدرن باور داریم. هر محصول، حاصل ساعت‌ها کار دست هنرمندان ماهر و استفاده از بهترین چرم‌های طبیعی است. هدف ما خلق آثاری است که نه تنها یک وسیله، بلکه بخشی از داستان و استایل شما باشند.</p>
<a href="#" class="btn btn-primary mt-3">بیشتر بدانید</a>
<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>
</section>
</div>
</section>
<?php include 'includes/footer.php'; ?>

174
login.php Normal file
View 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
View File

@ -0,0 +1,3 @@
<?php
// This file provides a simple entry point for logging out.
require_once __DIR__ . '/auth_handler.php';

View File

@ -1,10 +1,13 @@
<?php
session_start();
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");
exit;
}
@ -14,58 +17,144 @@ try {
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
header("Location: shop.php");
exit;
}
} catch (PDOException $e) {
error_log("DB Error: " . $e->getMessage());
die("An error occurred. Please try again later.");
error_log("Database Error: " . $e->getMessage());
$db_error = "<p>خطا در برقراری ارتباط با پایگاه داده.</p>";
}
$page_title = htmlspecialchars($product['name']);
$available_colors = !empty($product['colors']) ? array_map('trim', explode(',', $product['colors'])) : [];
// If product not found, show a message and stop
if (!$product) {
echo '<main class="container py-5 text-center"><div class="alert alert-danger">محصولی با این شناسه یافت نشد.</div><div><a href="shop.php" class="btn btn-primary mt-3">بازگشت به فروشگاه</a></div></main>';
require_once 'includes/footer.php';
exit;
}
// Set page title after fetching product name
$page_title = htmlspecialchars($product['name']);
// Parse comma-separated colors string
$available_colors = [];
if (!empty($product['colors'])) {
$colors_raw = explode(',', $product['colors']);
foreach ($colors_raw as $color) {
$trimmed_color = trim($color);
if (!empty($trimmed_color)) {
$available_colors[] = $trimmed_color;
}
}
}
include 'includes/header.php';
?>
<main class="container section-padding">
<div class="row g-5">
<div class="col-lg-6">
<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 class="card card-static p-3">
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($product['name']); ?>">
</div>
</div>
<div class="col-lg-6 d-flex flex-column justify-content-center">
<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>
<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>
<form action="cart_handler.php" method="POST">
<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">
<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>
<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="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 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>
<button type="submit" name="add_to_cart" class="btn btn-primary btn-lg w-100 py-3 fw-bold">افزودن به سبد خرید</button>
<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>
<?php include 'includes/footer.php'; ?>
<!-- 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
View 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>

View File

@ -1,46 +1,65 @@
<?php
$page_title = 'فروشگاه';
require_once 'includes/header.php';
require_once 'db/config.php';
// Fetch all products from the database
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM products ORDER BY created_at DESC");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("DB Error: " . $e->getMessage());
error_log("Database error: " . $e->getMessage());
$products = [];
$db_error = "خطا در بارگذاری محصولات. لطفا بعدا تلاش کنید.";
}
$page_title = 'فروشگاه';
include 'includes/header.php';
?>
<div class="text-center mb-5">
<h1 class="display-4 fw-bold">گالری محصولات</h1>
<p class="lead text-muted">دست‌سازه‌هایی از چرم طبیعی، با عشق و دقت</p>
</div>
<main class="container section-padding">
<div class="text-center" data-aos="fade-down">
<h1 class="section-title">مجموعه کامل محصولات</h1>
<p class="fs-5 text-muted">دست‌سازه‌هایی از چرم طبیعی، با عشق و دقت.</p>
</div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 g-4">
<?php if (!empty($products)): ?>
<?php foreach ($products as $product): ?>
<div class="col">
<div class="product-card h-100">
<?php if (!empty($db_error)): ?>
<div class="alert alert-danger">
<?= $db_error; ?>
</div>
<?php endif; ?>
<?php if (empty($products) && empty($db_error)): ?>
<div class="col-12">
<p class="text-center text-muted fs-4">در حال حاضر محصولی برای نمایش وجود ندارد.</p>
</div>
<?php else: ?>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4 g-lg-5 mt-5">
<?php
$delay = 0;
foreach ($products as $product):
?>
<div class="col" data-aos="fade-up" data-aos-delay="<?= $delay ?>">
<div class="card product-card h-100">
<div class="product-image">
<a href="product.php?id=<?php echo $product['id']; ?>">
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid" alt="<?php echo htmlspecialchars($product['name']); ?>">
<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=<?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>
<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 endforeach; ?>
<?php else: ?>
<div class="col-12">
<p class="text-center p-5 bg-light rounded-3">محصولی برای نمایش یافت نشد.</p>
<?php
$delay = ($delay + 100) % 400; // Stagger animation delay
endforeach;
?>
</div>
<?php endif; ?>
</div>
</main>
<?php include 'includes/footer.php'; ?>
<?php require_once 'includes/footer.php'; ?>

53
terms.php Normal file
View 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
View 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">&times;</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
View 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
View 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
View 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
View 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
View 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
View 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',
);

Some files were not shown because too many files have changed in this diff Show More