Compare commits

...

12 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
Flatlogic Bot
a1d3188d59 var first1 2025-11-30 18:13:07 +00:00
Flatlogic Bot
a8dff7dfb0 ver first 2025-11-30 18:12:34 +00:00
32244 changed files with 3967625 additions and 147 deletions

View File

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

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules/ node_modules/
*/node_modules/ */node_modules/
*/build/ */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"}

90
admin/add_product.php Normal file
View File

@ -0,0 +1,90 @@
<?php
session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/header.php';
$flash_message = $_SESSION['flash_message'] ?? null;
if ($flash_message) {
unset($_SESSION['flash_message']);
}
?>
<style>
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
</style>
<div class="admin-header">
<h1>افزودن محصول جدید</h1>
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
</div>
<div class="card">
<div class="card-body">
<form action="handler.php?action=add" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="name" class="form-label">نام محصول</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="form-group">
<label for="description" class="form-label">توضیحات</label>
<textarea class="form-control" id="description" name="description" rows="5" required></textarea>
</div>
<div class="form-grid">
<div class="form-group">
<label for="price" class="form-label">قیمت (تومان)</label>
<input type="number" class="form-control" id="price" name="price" required>
</div>
<div class="form-group">
<label for="stock" class="form-label">موجودی</label>
<input type="number" class="form-control" id="stock" name="stock" required value="0">
</div>
</div>
<div class="form-group">
<label for="image" class="form-label">تصویر محصول</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*">
</div>
<div class="form-group">
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
<input type="text" class="form-control" id="colors" name="colors" placeholder="مثال: #8B4513, #2C2C2C">
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="is_featured" name="is_featured" value="1" style="width: 20px; height: 20px;">
<span>این یک محصول ویژه است</span>
</label>
</div>
<div style="text-align: left; margin-top: 2rem;">
<button type="submit" class="btn btn-primary">افزودن محصول</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const style = getComputedStyle(document.body);
<?php if ($flash_message): ?>
Swal.fire({
title: '<?php echo $flash_message["type"] === "success" ? "عالی" : "خطا"; ?>',
html: '<?php echo addslashes($flash_message["message"]); ?>',
icon: '<?php echo $flash_message["type"]; ?>',
confirmButtonText: 'باشه',
background: style.getPropertyValue('--admin-surface'),
color: style.getPropertyValue('--admin-text')
});
<?php endif; ?>
});
</script>
<?php require_once __DIR__ . '/footer.php'; ?>

251
admin/api.php Normal file
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;
}

8
admin/auth_check.php Normal file
View File

@ -0,0 +1,8 @@
<?php
require_once __DIR__ . '/auth_handler.php';
// Check if the user is logged in. If not, redirect to the login page.
if (!is_admin()) {
header('Location: login.php');
exit;
}

8
admin/auth_handler.php Normal file
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';
?>

127
admin/edit_product.php Normal file
View File

@ -0,0 +1,127 @@
<?php
session_start();
require_once __DIR__ . '/auth_check.php';
require_once __DIR__ . '/../db/config.php';
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if (!$product_id) {
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'شناسه محصول نامعتبر است.'];
header('Location: products.php');
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$product) {
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'محصول مورد نظر یافت نشد.'];
header('Location: products.php');
exit;
}
} catch (PDOException $e) {
$_SESSION['flash_message'] = ['type' => 'danger', 'message' => 'خطا در اتصال به پایگاه داده.'];
header('Location: products.php');
exit;
}
require_once __DIR__ . '/header.php';
?>
<style>
.form-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
.image-preview-container {
background-color: var(--admin-bg);
border: 1px dashed var(--admin-border);
padding: 1rem;
border-radius: 8px;
text-align: center;
}
.image-preview {
max-width: 100%;
height: auto;
max-height: 200px;
border-radius: 8px;
margin-bottom: 1rem;
}
</style>
<div class="admin-header">
<h1>ویرایش محصول: <?php echo htmlspecialchars($product['name']); ?></h1>
<a href="products.php" class="btn" style="background: var(--admin-border); color: var(--admin-text);">بازگشت</a>
</div>
<form action="handler.php?action=edit" method="post" enctype="multipart/form-data">
<input type="hidden" name="id" value="<?php echo htmlspecialchars($product['id']); ?>">
<input type="hidden" name="current_image" value="<?php echo htmlspecialchars($product['image_url']); ?>">
<div class="form-grid">
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="name" class="form-label">نام محصول</label>
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
</div>
<div class="form-group">
<label for="description" class="form-label">توضیحات</label>
<textarea class="form-control" id="description" name="description" rows="5" required><?php echo htmlspecialchars($product['description']); ?></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
<div class="form-group">
<label for="price" class="form-label">قیمت (تومان)</label>
<input type="number" class="form-control" id="price" name="price" value="<?php echo htmlspecialchars($product['price']); ?>" required>
</div>
<div class="form-group">
<label for="stock" class="form-label">موجودی</label>
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
</div>
</div>
<div class="form-group">
<label for="colors" class="form-label">کدهای رنگ (اختیاری)</label>
<input type="text" class="form-control" id="colors" name="colors" value="<?php echo htmlspecialchars($product['colors'] ?? ''); ?>" placeholder="مثال: #8B4513, #2C2C2C">
<small style="color: var(--admin-text-muted);">کدهای رنگ هگزادسیمال را با کاما جدا کنید.</small>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" name="is_featured" value="1" <?php echo ($product['is_featured'] ?? 0) ? 'checked' : ''; ?> style="width: 20px; height: 20px;">
<span>این یک محصول ویژه است</span>
</label>
</div>
</div>
</div>
<div class="card">
<div class="card-header">تصویر محصول</div>
<div class="card-body">
<div class="image-preview-container">
<img src="../<?php echo htmlspecialchars($product['image_url']); ?>" alt="Current Image" id="image-preview" class="image-preview">
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(event)">
<small style="color: var(--admin-text-muted); margin-top: 0.5rem; display: block;">برای تغییر، تصویر جدیدی انتخاب کنید.</small>
</div>
</div>
</div>
</div>
<div style="text-align: left; margin-top: 2rem;">
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> ذخیره تغییرات</button>
</div>
</form>
<script>
function previewImage(event) {
const reader = new FileReader();
reader.onload = () => document.getElementById('image-preview').src = reader.result;
if (event.target.files[0]) reader.readAsDataURL(event.target.files[0]);
}
</script>
<?php require_once __DIR__ . '/footer.php'; ?>

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

77
admin/handler.php Normal file
View File

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

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

125
admin/index.php Normal file
View File

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

69
admin/login.php Normal file
View File

@ -0,0 +1,69 @@
<?php
session_start();
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$hardcoded_password = 'admin123';
if (isset($_POST['password']) && $_POST['password'] === $hardcoded_password) {
$_SESSION['is_admin'] = true;
header('Location: index.php');
exit;
} else {
$error = 'رمز عبور وارد شده اشتباه است.';
}
}
$page_title = 'ورود به پنل مدیریت';
?>
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $page_title; ?></title>
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="assets/css/admin_main.css?v=<?= time(); ?>">
</head>
<body class="admin-theme">
<div class="admin-login-wrapper">
<div class="admin-login-box">
<h2>پنل مدیریت آتیمه</h2>
<p>برای دسترسی به پنل، لطفاً وارد شوید.</p>
<?php if ($error): ?>
<div class="alert alert-danger mb-3"><?= $error; ?></div>
<p class="text-center text-muted mb-4">رمز عبور پیش‌فرض: <code>admin123</code></p>
<?php endif; ?>
<form method="POST" action="login.php">
<div class="form-group">
<label for="password" class="form-label">رمز عبور</label>
<input type="password" class="form-control" id="password" name="password" required autofocus>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary w-100">ورود <i class="ri-arrow-left-line"></i></button>
</div>
</form>
</div>
</div>
</body>
</html>
<style>
.alert-danger {
background-color: var(--admin-danger-bg, #fef2f2);
border: 1px solid var(--admin-danger-border, #fecaca);
color: var(--admin-danger-text, #991b1b);
padding: 0.8rem 1rem;
border-radius: 8px;
font-size: 0.9rem;
}
.w-100 { width: 100%; }
</style>

22
admin/logout.php Normal file
View File

@ -0,0 +1,22 @@
<?php
session_start();
// Unset all of the session variables.
$_SESSION = array();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
// Redirect to the login page.
header('Location: login.php');
exit;

45
admin/nav.php Normal file
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.']);
}
}

579
assets/css/custom.css Normal file
View File

@ -0,0 +1,579 @@
:root {
--status-default: #444;
--status-default-dark: #6c757d;
--status-processing: #ffc107;
--status-shipped: #0d6efd;
--status-completed: #198754;
--status-cancelled: #dc3545;
}
.about-us-list {
width: 80vw;
display: grid;
list-style: none;
padding: 0;
grid-template-columns: repeat(3, 1fr);
justify-items: center;
margin: 0 auto;
gap: 20px;
}
.about-us-item {
width: 20vw;
min-width: 200px;
border-radius: 20px;
text-align: center;
border: 1px solid #ebebeb;
}
.inner {
position: relative;
inset: 0px;
overflow: hidden;
transition: inherit;
}
.inner::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(-65deg, #0000 40%, #fff7 50%, #0000 70%);
background-size: 200% 100%;
background-repeat: no-repeat;
animation: thing 1.5s ease infinite;
border-radius: 20px;
}
@keyframes thing {
0% {
background-position: 130%;
opacity: 1;
}
to {
background-position: -166%;
opacity: 0;
}
}
@media (max-width: 768px) {
.about-us-list {
grid-template-columns: 1fr;
}
.about-us-item {
width: 80%;
}
}
.tracking-modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1050;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.tracking-modal-container.visible {
opacity: 1;
visibility: visible;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.modal-content {
position: relative;
background-color: #2c2c2c;
color: #f0f0f0;
border-radius: 15px;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
transform: scale(0.95);
transition: transform 0.3s ease;
}
.tracking-modal-container.visible .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-header h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 500;
}
#modal-order-id {
font-weight: bold;
color: var(--bs-primary);
}
.modal-close-btn {
background: none;
border: none;
color: #f0f0f0;
font-size: 2rem;
line-height: 1;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.modal-close-btn:hover {
opacity: 1;
}
.modal-body {
padding: 1.5rem;
display: grid;
gap: 2rem;
}
.order-summary,
.shipping-details,
.products-list,
.status-details {
background-color: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 10px;
}
.order-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.modal-body h4 {
margin-top: 0;
margin-bottom: 1rem;
font-weight: 500;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 0.5rem;
}
.detail-item {
font-size: 0.95rem;
}
.detail-item strong {
color: #a0a0a0;
margin-left: 8px;
}
.status-tracker {
position: relative;
display: flex;
justify-content: space-between;
padding: 20px 0;
margin-top: 20px;
}
.status-tracker::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
width: calc(100% - 40px);
margin: 0 20px;
height: 4px;
background-color: var(--status-default);
z-index: 1;
}
.status-progress {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 20px;
height: 4px;
z-index: 2;
transition: width 0.5s ease, background-color 0.5s ease;
}
.status-step {
position: relative;
z-index: 3;
text-align: center;
width: 100px;
}
.status-step .dot {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: var(--status-default);
border: 3px solid #2c2c2c;
margin: 0 auto;
transform: translateY(-8px);
transition: background-color 0.5s ease, box-shadow 0.3s ease;
}
.status-step .label {
display: block;
margin-top: 10px;
font-size: 0.8rem;
color: #a0a0a0;
transition: color 0.5s ease;
}
.status-step.completed .label,
.status-step.active .label {
color: #f0f0f0;
font-weight: 500;
}
.status-step.active .dot {
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
}
.status-tracker.is-cancelled .status-step {
opacity: 0.5;
}
.status-tracker.is-cancelled .label {
color: var(--status-cancelled);
}
#modal-products-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.product-item {
display: flex;
align-items: center;
gap: 1rem;
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 8px;
}
.product-item img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
}
.product-info {
flex-grow: 1;
}
.product-name {
display: block;
font-weight: 500;
}
.product-quantity {
font-size: 0.9rem;
color: #a0a0a0;
}
.product-price {
font-weight: bold;
}
.product-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.9rem;
color: #a0a0a0;
margin-top: 4px;
}
.product-color-wrapper {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.product-color-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.2);
display: inline-block;
}
@media (max-width: 768px) {
footer {
text-align: center;
}
footer .row > * {
margin-bottom: 1.5rem;
}
footer .row {
justify-content: center;
}
/* Center social icons and contact list items on mobile */
footer .social-icons,
footer .list-unstyled .d-flex {
justify-content: center;
}
}
/* --- Modern Login Page --- */
.login-page-modern {
background-color: var(--color-dark-bg);
background-image: url('../images/pexels/about-us-35056828.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
position: relative;
}
.login-page-modern::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(17, 17, 17, 0.7);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.login-container {
position: relative;
z-index: 2;
width: 100%;
max-width: 450px;
padding: 2rem;
}
.login-form-wrapper {
background-color: var(--color-surface);
padding: 2.5rem;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.login-header .logo-link {
text-decoration: none;
}
.login-header .logo-title {
color: var(--color-gold);
font-weight: 800;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.login-header .tagline {
color: var(--color-text-secondary);
font-size: 1rem;
font-weight: 300;
}
.login-form-wrapper .form-title {
color: var(--color-text-primary);
font-weight: 600;
}
.form-floating > .form-control {
background-color: var(--color-dark-bg);
border-color: var(--color-border);
}
.form-floating > .form-control:focus {
background-color: var(--color-dark-bg);
}
.form-floating > label {
color: var(--color-text-secondary);
}
.login-form-wrapper .btn-primary {
padding: 0.8rem;
font-size: 1.1rem;
letter-spacing: 0.5px;
}
.auth-footer a {
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.auth-footer a:hover {
color: var(--color-gold);
}
@media (max-width: 576px) {
.login-container {
padding: 1rem;
}
}
@media (min-width: 992px) {
.login-container {
max-width: 600px;
padding: 0;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.login-container {
max-width: 500px;
}
.login-form-wrapper {
padding: 2rem 1.5rem;
}
.login-header .logo-title {
font-size: 2rem;
}
}
/* --- Login Toggle Switch --- */
.login-toggle-container .btn-group {
border: 1px solid var(--color-border);
border-radius: var(--bs-border-radius-lg); /* Match form control radius */
overflow: hidden;
}
.login-toggle-container .btn-outline-primary {
background-color: transparent;
color: var(--color-text-secondary);
border-color: transparent; /* Remove individual button borders */
transition: background-color 0.3s ease, color 0.3s ease;
padding: 0.75rem 1rem;
font-size: 1rem;
flex-grow: 1;
}
.login-toggle-container .btn-outline-primary:hover {
background-color: rgba(var(--bs-primary-rgb), 0.1);
color: var(--color-gold);
}
.login-toggle-container .btn-check:checked + .btn-outline-primary {
background-color: var(--color-gold);
color: var(--color-dark-bg);
border-color: var(--color-gold);
box-shadow: 0 0 10px rgba(var(--bs-primary-rgb), 0.3);
}
.login-toggle-container .btn-check:focus + .btn-outline-primary {
box-shadow: none;
}
/* --- Divider --- */
.divider-with-text {
display: flex;
align-items: center;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.divider-with-text::before,
.divider-with-text::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--color-border);
}
.divider-with-text:not(:empty)::before {
margin-left: .5em;
}
.divider-with-text:not(:empty)::after {
margin-right: .5em;
}
/* --- Google Sign-in Button --- */
.btn-google {
background-color: #ffffff;
color: #424242;
border: 1px solid #dcdcdc;
transition: background-color 0.3s, border-color 0.3s;
}
.btn-google:hover {
background-color: #f5f5f5;
border-color: #c5c5c5;
color: #212121;
}
.btn-google .ri-google-fill {
color: #DB4437;
font-size: 1.3em;
}
.btn-google span {
font-weight: 500;
}
/* --- Hero Video Section --- */
.hero-video-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -1; /* Changed from 1 to -1 */
}
.hero-video-background video {
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
object-fit: cover;
}
.hero-video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.hero-section .container {
z-index: 2;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 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);
}
}
});
}
});

30
assets/js/main.js Normal file
View File

@ -0,0 +1,30 @@
document.addEventListener('DOMContentLoaded', () => {
// Initialize AOS (Animate on Scroll)
AOS.init({
duration: 800, // Animation duration in ms
offset: 100, // Offset (in px) from the original trigger point
once: true, // Whether animation should happen only once - while scrolling down
});
// Add a class to the header when the page is scrolled
const header = document.querySelector('.site-header');
if (header) {
const scrollThreshold = 50; // Pixels to scroll before adding the class
const handleScroll = () => {
if (window.scrollY > scrollThreshold) {
header.classList.add('header-scrolled');
} else {
header.classList.remove('header-scrolled');
}
};
// Listen for the scroll event
window.addEventListener('scroll', handleScroll);
// Initial check in case the page is already scrolled on load
handleScroll();
}
});

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

111
cart.php Normal file
View File

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

110
cart_handler.php Normal file
View File

@ -0,0 +1,110 @@
<?php
require_once __DIR__ . '/includes/session_config.php';
session_start();
require_once 'db/config.php';
// Initialize cart if it doesn't exist
if (!isset($_SESSION['cart'])) {
$_SESSION['cart'] = [];
}
// Get POST data
$product_id = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
$quantity = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);
$color = filter_input(INPUT_POST, 'product_color', FILTER_SANITIZE_STRING);
$action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_STRING);
if (!$action || !$product_id) {
header('Location: shop.php');
exit;
}
// Generate a unique ID for the cart item based on product ID and color
$cart_item_id = $product_id . ($color ? '_' . str_replace('#', '', $color) : '');
switch ($action) {
case 'add':
if ($quantity > 0) {
try {
$pdo = db();
// Fetch product details including colors
$stmt = $pdo->prepare("SELECT name, price, image_url, colors FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
if ($product) {
// --- START COLOR VALIDATION ---
$available_colors = [];
if (!empty($product['colors'])) {
$colors_raw = explode(',', $product['colors']);
foreach ($colors_raw as $c) {
$trimmed_c = trim($c);
if ($trimmed_c) $available_colors[] = $trimmed_c;
}
}
if (count($available_colors) > 1 && empty($color)) {
// For multi-color products, a color must be selected.
$_SESSION['flash_message'] = [
'type' => 'warning',
'message' => 'برای افزودن این محصول، انتخاب یکی از رنگ‌ها الزامی است.'
];
header('Location: product.php?id=' . $product_id);
exit;
}
// --- END COLOR VALIDATION ---
// If item is already in the cart (same product ID and color), just update the quantity.
if (isset($_SESSION['cart'][$cart_item_id])) {
$_SESSION['cart'][$cart_item_id]['quantity'] += $quantity;
} else {
// Otherwise, add the new item to the cart.
$_SESSION['cart'][$cart_item_id] = [
'product_id' => $product_id,
'name' => $product['name'],
'price' => $product['price'],
'image_url' => $product['image_url'],
'quantity' => $quantity,
'color' => $color
];
}
$_SESSION['flash_message'] = [
'type' => 'success',
'message' => 'محصول با موفقیت به سبد خرید اضافه شد!'
];
} else {
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'محصول یافت نشد.'];
}
} catch (PDOException $e) {
error_log("Cart Add Error: " . $e->getMessage());
$_SESSION['flash_message'] = [
'type' => 'error',
'message' => 'مشکلی در افزودن محصول به سبد خرید رخ داد.'
];
}
}
// Redirect back to the previous page (likely the product page) to show the flash message.
header('Location: ' . ($_SERVER['HTTP_REFERER'] ?? 'shop.php'));
exit;
case 'update':
if ($quantity > 0) {
if (isset($_SESSION['cart'][$cart_item_id])) {
$_SESSION['cart'][$cart_item_id]['quantity'] = $quantity;
}
} else {
// If quantity is 0 or less, remove the item.
unset($_SESSION['cart'][$cart_item_id]);
}
break;
case 'remove':
if (isset($_SESSION['cart'][$cart_item_id])) {
unset($_SESSION['cart'][$cart_item_id]);
}
break;
}
// For 'update' and 'remove' actions, redirect to the cart page to show changes.
header('Location: cart.php');
exit;

194
checkout.php Normal file
View File

@ -0,0 +1,194 @@
<?php
session_start();
require_once 'db/config.php';
require_once 'includes/header.php';
// 1. Check if cart exists and is not empty. If not, redirect to shop.
if (empty($_SESSION['cart'])) {
header('Location: shop.php');
exit();
}
$cart_items = $_SESSION['cart'];
$product_details = [];
$total_price = 0;
// 2. Process cart items directly from the session
foreach ($cart_items as $cart_item_id => $item) {
// Ensure item has required data
if (!isset($item['price'], $item['quantity'], $item['name'], $item['image_url'])) {
// Skip malformed items
continue;
}
$item_total = $item['price'] * $item['quantity'];
$total_price += $item_total;
// Store details for display
$product_details[] = [
'id' => $item['product_id'],
'name' => $item['name'],
'price' => $item['price'],
'image_url' => $item['image_url'],
'quantity' => $item['quantity'],
'color' => $item['color'] ?? '', // Handle case where color might not be set
'total' => $item_total
];
}
// 3. If after all checks, product_details is empty (e.g. invalid items in cart), redirect.
if (empty($product_details)) {
// Clear the invalid cart and redirect
unset($_SESSION['cart']);
header('Location: shop.php');
exit();
}
// 4. Fetch user data if logged in
$user_id = $_SESSION['user_id'] ?? null;
$user = [];
$address = [];
if ($user_id) {
try {
$pdo = db();
$user_stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$user_stmt->execute([$user_id]);
$user = $user_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
$address_stmt = $pdo->prepare("SELECT * FROM user_addresses WHERE user_id = ? ORDER BY id DESC LIMIT 1");
$address_stmt->execute([$user_id]);
$address = $address_stmt->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $e) {
error_log("Checkout user fetch error: " . $e->getMessage());
// Do not block the page, guest checkout is still possible
}
}
$shipping_cost = 50000;
$grand_total = $total_price + $shipping_cost;
?>
<main class="checkout-page-wrapper">
<section class="section-padding">
<div class="container">
<div class="text-center" data-aos="fade-down">
<h1 class="section-title">تکمیل سفارش و پرداخت</h1>
<p class="text-muted fs-5">اطلاعات خود را برای ارسال سفارش وارد کنید.</p>
</div>
<div class="row g-5 mt-5">
<!-- Billing Details Column -->
<div class="col-lg-8" data-aos="fade-right">
<h3 class="mb-4">جزئیات صورتحساب</h3>
<?php
if (!empty($_SESSION['checkout_errors'])) {
echo '<div class="alert alert-danger"><ul>';
foreach ($_SESSION['checkout_errors'] as $error) {
echo '<li>' . htmlspecialchars($error) . '</li>';
}
echo '</ul></div>';
// Unset the session variable so it doesn't show again on refresh
unset($_SESSION['checkout_errors']);
}
?>
<form id="checkout-form" action="checkout_handler.php" method="POST">
<div class="card mb-4">
<div class="card-header">اطلاعات تماس</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="firstName" class="form-label">نام</label>
<input type="text" class="form-control" id="firstName" name="first_name" value="<?= htmlspecialchars($user['first_name'] ?? '') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="lastName" class="form-label">نام خانوادگی</label>
<input type="text" class="form-control" id="lastName" name="last_name" value="<?= htmlspecialchars($user['last_name'] ?? '') ?>" required>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">ایمیل (اختیاری)</label>
<input type="email" class="form-control" id="email" name="email" value="<?= htmlspecialchars($user['email'] ?? '') ?>">
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">تلفن همراه</label>
<input type="tel" class="form-control" id="phone" name="phone_number" value="<?= htmlspecialchars($address['phone_number'] ?? $user['phone_number'] ?? '') ?>" required>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">آدرس جهت ارسال</div>
<div class="card-body">
<div class="mb-3">
<label for="address" class="form-label">آدرس</label>
<input type="text" class="form-control" id="address" name="address_line" placeholder="خیابان اصلی، کوچه فرعی، پلاک ۱۲۳" value="<?= htmlspecialchars($address['address_line'] ?? '') ?>" required>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="city" class="form-label">شهر</label>
<input type="text" class="form-control" id="city" name="city" value="<?= htmlspecialchars($address['city'] ?? '') ?>" required>
</div>
<div class="col-md-4 mb-3">
<label for="state" class="form-label">استان</label>
<input type="text" class="form-control" id="state" name="province" value="<?= htmlspecialchars($address['province'] ?? '') ?>" required>
</div>
<div class="col-md-4 mb-3">
<label for="zip" class="form-label">کد پستی</label>
<input type="text" class="form-control" id="zip" name="postal_code" value="<?= htmlspecialchars($address['postal_code'] ?? '') ?>" required>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Order Summary Column -->
<div class="col-lg-4" data-aos="fade-left">
<div class="card card-body position-sticky" style="top: 2rem;">
<h3 class="card-title">خلاصه سفارش شما</h3>
<ul class="summary-item-list">
<?php foreach ($product_details as $item) : ?>
<li>
<span class="product-name"><?= htmlspecialchars($item['name']) ?> <span class="text-muted">(x<?= $item['quantity'] ?>)</span></span>
<span class="product-total">T <?= number_format($item['total']) ?></span>
</li>
<?php endforeach; ?>
</ul>
<div class="summary-totals">
<div class="total-row">
<span class="label">جمع کل</span>
<span class="value">T <?= number_format($total_price) ?></span>
</div>
<div class="total-row">
<span class="label">هزینه ارسال</span>
<span class="value">T <?= number_format($shipping_cost) ?></span>
</div>
<div class="total-row grand-total mt-3">
<span class="label">مبلغ قابل پرداخت</span>
<span class="value">T <?= number_format($grand_total) ?></span>
</div>
</div>
<div class="d-grid mt-4">
<button type="submit" form="checkout-form" class="btn btn-primary btn-lg">
<i class="ri-secure-payment-line me-2"></i>
پرداخت و ثبت نهایی سفارش
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<?php require_once 'includes/footer.php'; ?>

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

135
contact.php Normal file
View File

@ -0,0 +1,135 @@
<?php
session_start(); // Ensure session is started
$page_title = 'تماس با ما';
require_once 'includes/header.php';
require_once 'mail/MailService.php';
// Handle form submission
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['contact_form'])) {
$name = trim(filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING));
$email = trim(filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL));
$message = trim(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_STRING));
if (empty($name) || empty($email) || empty($message)) {
$_SESSION['flash_message'] = ['type' => 'warning', 'message' => 'لطفاً تمام فیلدها را پر کنید.'];
} elseif (!$email) {
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'آدرس ایمیل وارد شده معتبر نیست.'];
} else {
$to_email = getenv('MAIL_TO') ?: 'support@atimeh.com'; // Fallback email
$subject = "پیام جدید از فرم تماس وب‌سایت";
$email_result = MailService::sendContactMessage($name, $email, $message, $to_email, $subject);
if (!empty($email_result['success'])) {
$_SESSION['flash_message'] = ['type' => 'success', 'message' => 'پیام شما با موفقیت ارسال شد. سپاسگزاریم!'];
} else {
$_SESSION['flash_message'] = ['type' => 'error', 'message' => 'خطا در ارسال پیام. لطفاً بعداً دوباره تلاش کنید.'];
error_log("MailService Error: " . ($email_result['error'] ?? 'Unknown error'));
}
}
// Redirect to the same page to prevent form resubmission
header("Location: contact.php");
exit();
}
// Check for flash message
$flash_message = $_SESSION['flash_message'] ?? null;
if ($flash_message) {
// Clear the message from session so it doesn't show again
unset($_SESSION['flash_message']);
}
?>
<div class="container py-5 my-5">
<div class="section-title text-center mb-5" data-aos="fade-down">
<h1>ارتباط با ما</h1>
<p class="fs-5 text-muted">نظرات، پیشنهادات و سوالات شما برای ما ارزشمند است.</p>
</div>
<div class="contact-card p-4 p-lg-5" data-aos="fade-up">
<div class="row g-5">
<div class="col-lg-5">
<div class="contact-info h-100 d-flex flex-column justify-content-center">
<h3 class="mb-4">راه‌های ارتباطی</h3>
<div class="d-flex align-items-start mb-4">
<i class="ri-map-pin-line mt-1 me-3"></i>
<div>
<strong>آدرس:</strong>
<p class="text-muted mb-0">تهران، خیابان هنر، کوچه خلاقیت، پلاک ۱۲</p>
</div>
</div>
<div class="d-flex align-items-start mb-4">
<i class="ri-mail-line mt-1 me-3"></i>
<div>
<strong>ایمیل:</strong>
<p class="mb-0"><a href="mailto:info@atimeh.com">info@atimeh.com</a></p>
</div>
</div>
<div class="d-flex align-items-start mb-4">
<i class="ri-phone-line mt-1 me-3"></i>
<div>
<strong>تلفن:</strong>
<p class="mb-0"><a href="tel:+982112345678">۰۲۱-۱۲۳۴۵۶۷۸</a></p>
</div>
</div>
<hr class="my-4" style="border-color: var(--luxury-border);">
<h4 class="h5 mb-3">ما را دنبال کنید</h4>
<div class="d-flex gap-2">
<a href="#" class="social-btn"><i class="ri-instagram-line"></i></a>
<a href="#" class="social-btn"><i class="ri-telegram-line"></i></a>
<a href="#" class="social-btn"><i class="ri-whatsapp-line"></i></a>
</div>
</div>
</div>
<div class="col-lg-7">
<h3 class="mb-4">فرم تماس</h3>
<form action="contact.php" method="POST">
<input type="hidden" name="contact_form" value="1">
<div class="mb-4">
<label for="name" class="form-label">نام شما</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-4">
<label for="email" class="form-label">ایمیل</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-4">
<label for="message" class="form-label">پیام شما</label>
<textarea class="form-control" id="message" name="message" rows="7" required></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">ارسال پیام</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- SweetAlert for flash messages -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
<?php if ($flash_message): ?>
Swal.fire({
title: '<?php echo addslashes($flash_message["message"]); ?>',
icon: '<?php echo $flash_message["type"]; ?>',
toast: true,
position: 'top-start',
showConfirmButton: false,
timer: 5000,
timerProgressBar: true,
showCloseButton: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer);
toast.addEventListener('mouseleave', Swal.resumeTimer);
},
customClass: {
popup: 'dark-theme-toast'
}
});
<?php endif; ?>
});
</script>
<?php require_once 'includes/footer.php'; ?>

View File

@ -15,3 +15,9 @@ function db() {
} }
return $pdo; return $pdo;
} }
// Google API configuration
define('GOOGLE_CLIENT_ID', '915631311746-o6gk076l6lfvuboin99u2h8cgqilc0qk.apps.googleusercontent.com');
define('GOOGLE_CLIENT_SECRET', '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');
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
define('GOOGLE_REDIRECT_URL', $protocol . $_SERVER['HTTP_HOST'] . '/google_callback.php');

View File

@ -0,0 +1,2 @@
-- Add the colors column to the products table if it doesn't exist
ALTER TABLE `products` ADD COLUMN IF NOT EXISTS `colors` VARCHAR(255) DEFAULT NULL COMMENT 'Comma-separated list of available colors';

View File

@ -0,0 +1,2 @@
-- Add the is_featured column to the products table if it doesn't exist
ALTER TABLE `products` ADD COLUMN IF NOT EXISTS `is_featured` BOOLEAN DEFAULT 0;

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"}

58
includes/footer.php Normal file
View File

@ -0,0 +1,58 @@
</main> <!-- close main container -->
<footer class="site-footer mt-5 py-5">
<div class="container">
<div class="row">
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
<h5 class="fw-bold mb-3">آتیمه</h5>
<p class="text-white-50">تجربه اصالت و کیفیت در محصولات چرمی دست‌دوز. ما به هنر و ماندگاری اعتقاد داریم.</p>
</div>
<div class="col-lg-2 col-md-6 mb-4 mb-lg-0">
<h5 class="fw-bold mb-3">دسترسی سریع</h5>
<ul class="list-unstyled">
<li><a href="shop.php" class="text-white-50">فروشگاه</a></li>
<li><a href="track_order.php" class="text-white-50">پیگیری سفارش</a></li>
<li><a href="about.php" class="text-white-50">درباره ما</a></li>
<li><a href="terms.php" class="text-white-50">قوانین و مقررات</a></li>
<li><a href="faq.php" class="text-white-50">سوالات متداول</a></li>
</ul>
</div>
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
<h5 class="fw-bold mb-3">تماس با ما</h5>
<ul class="list-unstyled">
<li class="d-flex align-items-center mb-2">
<i class="bi bi-geo-alt-fill me-2 text-primary"></i>
<span class="text-white-50">تهران، خیابان هنر، پلاک ۱۲</span>
</li>
<li class="d-flex align-items-center mb-2">
<i class="bi bi-telephone-fill me-2 text-primary"></i>
<a href="tel:+982122334455" class="text-white-50">۰۲۱-۲۲۳۳۴۴۵۵</a>
</li>
<li class="d-flex align-items-center">
<i class="bi bi-envelope-fill me-2 text-primary"></i>
<a href="mailto:info@atimeh.com" class="text-white-50">info@atimeh.com</a>
</li>
</ul>
</div>
<div class="col-lg-3 col-md-6">
<h5 class="fw-bold mb-3">ما را دنبال کنید</h5>
<p class="text-white-50">از جدیدترین محصولات و تخفیف‌ها باخبر شوید.</p>
<div class="d-flex mt-3 social-icons">
<a href="#" class="social-icon me-3"><i class="bi bi-instagram"></i></a>
<a href="#" class="social-icon me-3"><i class="bi bi-telegram"></i></a>
<a href="#" class="social-icon"><i class="bi bi-whatsapp"></i></a>
</div>
</div>
</div>
<div class="text-center text-white-50 pt-4 mt-4 border-top border-secondary">
<p>&copy; <?php echo date("Y"); ?> تمام حقوق برای چرم آتیمه محفوظ است.</p>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="/assets/js/main.js?v=<?php echo time(); ?>"></script>
</div>
</body>
</html>

135
includes/header.php Normal file
View File

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

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

25
includes/pexels.php Normal file
View File

@ -0,0 +1,25 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}

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

253
index.php
View File

@ -1,150 +1,111 @@
<?php <?php
declare(strict_types=1); $page_title = 'صفحه اصلی';
@ini_set('display_errors', '1'); include 'includes/header.php';
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s'); // Load dynamic content
$about_us_image_data = json_decode(file_get_contents('about_us_image.json'), true);
$about_us_image_url = $about_us_image_data ? str_replace('\\/', '/', $about_us_image_data['local_path']) : 'assets/images/pexels/about-us-34942790.jpg';
require_once 'db/config.php';
?> ?>
<!doctype html>
<html lang="en"> <!-- Hero Section -->
<head> <section class="hero-section vh-100 d-flex justify-content-center align-items-center position-relative text-white text-center">
<meta charset="utf-8" /> <div class="hero-video-background">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <video playsinline autoplay muted loop poster="assets/images/pexels/about-us-34942790.jpg">
<title>New Style</title> <source src="https://videos.pexels.com/video-files/8065365/8065365-hd_1920_1080_25fps.mp4" type="video/mp4">
<?php ویدیوی شما توسط مرورگر پشتیبانی نمی‌شود.
// Read project preview data from environment </video>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <div class="hero-video-overlay"></div>
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div> </div>
</main> <div class="container position-relative">
<footer> <h1 class="display-3 fw-bold" data-aos="fade-down">اصالت در هر نگاه</h1>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <p class="lead fs-4 mb-4" data-aos="fade-up" data-aos-delay="200">محصولات چرمی دست‌دوز، آفریده برای ماندگاری.</p>
</footer> <a href="shop.php" class="btn btn-primary btn-lg" data-aos="fade-up" data-aos-delay="400">مشاهده محصولات</a>
</body> </div>
</html> </section>
<!-- Featured Products Section -->
<section id="featured-products" class="section-padding">
<div class="container">
<?php
if (isset($_SESSION['success_message'])) {
echo '<div class="alert alert-success alert-dismissible fade show" role="alert">' . $_SESSION['success_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success_message']);
}
if (isset($_SESSION['error_message'])) {
echo '<div class="alert alert-danger alert-dismissible fade show" role="alert">' . $_SESSION['error_message'] . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error_message']);
}
?>
<div class="section-title text-center" data-aos="fade-up">
<h1>مجموعه برگزیده ما</h1>
<p class="fs-5 text-muted">دست‌چین شده برای سلیقه‌های خاص.</p>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 g-lg-5">
<?php
try {
$pdo = db();
$stmt = $pdo->query("SELECT * FROM products WHERE is_featured = 1 ORDER BY created_at DESC LIMIT 3");
$featured_products = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($featured_products)) {
echo '<div class="col-12"><p class="text-center text-muted">هیچ محصولی برای نمایش وجود ندارد.</p></div>';
} else {
$delay = 0;
foreach ($featured_products as $product) {
?>
<div class="col" data-aos="fade-up" data-aos-delay="<?= $delay ?>">
<div class="product-card h-100">
<div class="product-image">
<a href="product.php?id=<?= $product['id'] ?>">
<img src="<?= htmlspecialchars($product['image_url']) ?>" alt="<?= htmlspecialchars($product['name']) ?>">
</a>
</div>
<div class="product-info text-center">
<h3 class="product-title"><a href="product.php?id=<?= $product['id'] ?>"><?= htmlspecialchars($product['name']) ?></a></h3>
<p class="product-price"><?= number_format($product['price']) ?> تومان</p>
</div>
</div>
</div>
<?php
$delay += 150;
}
}
} catch (PDOException $e) {
error_log("Database error: " . $e->getMessage());
echo '<div class="col-12"><p class="text-center text-danger">خطا در بارگذاری محصولات.</p></div>';
}
?>
</div>
<div class="text-center mt-5" data-aos="fade-up">
<a href="shop.php" class="btn btn-primary">مشاهده تمام محصولات</a>
</div>
</div>
</section>
<!-- About Us Section -->
<section id="about-us" class="section-padding bg-surface">
<div class="container">
<div class="row align-items-center g-5">
<div class="col-md-6" data-aos="fade-right">
<img src="<?= htmlspecialchars($about_us_image_url) ?>" alt="درباره ما" class="about-us-image img-fluid">
</div>
<div class="col-md-6" data-aos="fade-left">
<div class="section-title text-md-end text-start">
<h1>داستان آتیمه</h1>
</div>
<p class="text-muted fs-5 mt-3 text-md-end text-start">ما در آتیمه، به تلفیق هنر سنتی و طراحی مدرن باور داریم. هر محصول، حاصل ساعت‌ها کار دست هنرمندان ماهر و استفاده از بهترین چرم‌های طبیعی است. هدف ما خلق آثاری است که نه تنها یک وسیله، بلکه بخشی از داستان و استایل شما باشند.</p>
<div class="text-md-end text-start">
<a href="about.php" class="btn btn-primary mt-3" data-aos="fade-up" data-aos-delay="200">بیشتر بدانید</a>
</div>
</div>
</div>
</div>
</section>
<?php include 'includes/footer.php'; ?>

174
login.php Normal file
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';

54
migrate.php Normal file
View File

@ -0,0 +1,54 @@
<?php
require_once 'db/config.php';
// Simple migration runner
function run_migrations() {
$pdo = db();
$migrations_dir = __DIR__ . '/db/migrations';
// Create migrations table if it doesn't exist
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (migration VARCHAR(255) PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
// Get executed migrations
$executed_migrations_stmt = $pdo->query("SELECT migration FROM migrations");
$executed_migrations = $executed_migrations_stmt->fetchAll(PDO::FETCH_COLUMN);
$migration_files = glob($migrations_dir . '/*.sql');
sort($migration_files);
foreach ($migration_files as $file) {
$migration_name = basename($file);
if (in_array($migration_name, $executed_migrations)) {
continue; // Skip already executed migration
}
echo "Running migration: {$migration_name}...
";
$sql = file_get_contents($file);
try {
$pdo->exec($sql);
// Log the migration
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
$stmt->execute([$migration_name]);
echo "Migration {$migration_name} executed successfully.
";
} catch (PDOException $e) {
echo "Error running migration {$migration_name}: " . $e->getMessage() . "
";
// Stop on error
return false;
}
}
echo "All new migrations have been executed.
";
return true;
}
// Run it
run_migrations();

160
product.php Normal file
View File

@ -0,0 +1,160 @@
<?php
require_once 'db/config.php';
require_once 'includes/header.php';
$product_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$product = null;
$db_error = '';
if (!$product_id) {
// Redirect or show error if ID is not valid
header("Location: shop.php");
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Database Error: " . $e->getMessage());
$db_error = "<p>خطا در برقراری ارتباط با پایگاه داده.</p>";
}
// If product not found, show a message and stop
if (!$product) {
echo '<main class="container py-5 text-center"><div class="alert alert-danger">محصولی با این شناسه یافت نشد.</div><div><a href="shop.php" class="btn btn-primary mt-3">بازگشت به فروشگاه</a></div></main>';
require_once 'includes/footer.php';
exit;
}
// Set page title after fetching product name
$page_title = htmlspecialchars($product['name']);
// Parse comma-separated colors string
$available_colors = [];
if (!empty($product['colors'])) {
$colors_raw = explode(',', $product['colors']);
foreach ($colors_raw as $color) {
$trimmed_color = trim($color);
if (!empty($trimmed_color)) {
$available_colors[] = $trimmed_color;
}
}
}
?>
<main class="container section-padding">
<div class="row g-5">
<div class="col-lg-6" data-aos="fade-right">
<div class="card card-static p-3">
<img src="<?php echo htmlspecialchars($product['image_url']); ?>" class="img-fluid rounded" alt="<?php echo htmlspecialchars($product['name']); ?>">
</div>
</div>
<div class="col-lg-6" data-aos="fade-left">
<div class="card h-100">
<div class="card-body p-4 p-lg-5">
<h1 class="display-5 fw-bold mb-3"><?php echo htmlspecialchars($product['name']); ?></h1>
<div class="d-flex align-items-center mb-4">
<p class="display-6 fw-bold m-0"><?php echo number_format($product['price']); ?> تومان</p>
</div>
<p class="fs-5 mb-4 text-muted"><?php echo nl2br(htmlspecialchars($product['description'])); ?></p>
<form action="cart_handler.php" method="POST" class="mt-auto">
<input type="hidden" name="product_id" value="<?php echo $product['id']; ?>">
<input type="hidden" name="action" value="add">
<?php if (!empty($available_colors)): ?>
<div class="mb-4">
<h5 class="mb-3">انتخاب رنگ:</h5>
<div class="color-swatches">
<?php foreach ($available_colors as $index => $color_hex): ?>
<input type="radio" class="btn-check" name="product_color" id="color_<?php echo $index; ?>" value="<?php echo htmlspecialchars($color_hex); ?>" autocomplete="off" <?php echo (count($available_colors) === 1) ? 'checked' : ''; ?>/>
<label class="btn" for="color_<?php echo $index; ?>" style="background-color: <?php echo htmlspecialchars($color_hex); ?>;"></label>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="row align-items-center mb-4">
<div class="col-md-5 col-lg-4 quantity-input-wrapper">
<label for="quantity" class="form-label fw-bold">تعداد:</label>
<input type="number" name="quantity" id="quantity" class="form-control quantity-input text-center" value="1" min="1" max="10">
</div>
</div>
<div class="d-grid gap-2 add-to-cart-btn">
<button type="submit" class="btn btn-primary"><i class="ri-shopping-bag-add-line"></i> افزودن به سبد خرید</button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
<!-- SweetAlert for color validation -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- 1. Flash Message Handling (from server-side) ---
<?php
if (isset($_SESSION['flash_message'])) {
$flash_message = $_SESSION['flash_message'];
unset($_SESSION['flash_message']);
echo "Swal.fire({
title: '".addslashes($flash_message['message'])."',
icon: '". $flash_message['type'] ."',
toast: true,
position: 'top-start',
showConfirmButton: false,
timer: 4000,
timerProgressBar: true,
showCloseButton: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer);
toast.addEventListener('mouseleave', Swal.resumeTimer);
},
customClass: {
popup: 'dark-theme-toast'
}
});";
}
?>
// --- 2. Client-side Color Selection Validation ---
const form = document.querySelector('form[action="cart_handler.php"]');
if (form) {
form.addEventListener('submit', function(event) {
const availableColors = <?php echo json_encode($available_colors); ?>;
const hasMultipleColors = Array.isArray(availableColors) && availableColors.length > 1;
if (hasMultipleColors) {
const selectedColor = document.querySelector('input[name="product_color"]:checked');
if (!selectedColor) {
event.preventDefault(); // Stop form submission
Swal.fire({
title: 'لطفاً یک رنگ انتخاب کنید',
text: 'برای افزودن این محصول به سبد خرید، انتخاب رنگ الزامی است.',
icon: 'warning',
confirmButtonText: 'متوجه شدم',
customClass: {
popup: 'dark-theme-popup',
title: 'dark-theme-title',
htmlContainer: 'dark-theme-content',
confirmButton: 'dark-theme-button'
}
});
}
}
});
}
});
</script>
<?php require_once 'includes/footer.php'; ?>

428
profile.php Normal file
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>

65
shop.php Normal file
View File

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

53
terms.php Normal file
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'; ?>

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