Initial commit
This commit is contained in:
commit
dcc2289717
26
.env.example
Normal file
26
.env.example
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DB_SERVER=localhost
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=shubraveil_db
|
||||||
|
|
||||||
|
# SMTP Configuration
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USERNAME=your-email@gmail.com
|
||||||
|
SMTP_PASSWORD=your-app-password
|
||||||
|
SMTP_FROM_EMAIL=noreply@shubraveil.com
|
||||||
|
SMTP_FROM_NAME=ShubraVeil
|
||||||
|
|
||||||
|
# Site Configuration
|
||||||
|
SITE_URL=http://localhost/shubraveil
|
||||||
|
DEBUG_MODE=true
|
||||||
|
|
||||||
|
# Security
|
||||||
|
JWT_SECRET=change_this_to_a_secure_secret_key
|
||||||
|
RECAPTCHA_SITE_KEY=your_recaptcha_site_key
|
||||||
|
RECAPTCHA_SECRET_KEY=your_recaptcha_secret_key
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
CACHE_ENABLED=true
|
||||||
|
CACHE_DURATION=3600
|
39
.github/workflows/php.yml
vendored
Normal file
39
.github/workflows/php.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: PHP Composer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate composer.json and composer.lock
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
id: composer-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: vendor
|
||||||
|
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-php-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
# Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
|
||||||
|
# Docs: https://getcomposer.org/doc/articles/scripts.md
|
||||||
|
|
||||||
|
# - name: Run test suite
|
||||||
|
# run: composer run-script test
|
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Composer
|
||||||
|
/vendor/
|
||||||
|
composer.lock
|
||||||
|
composer.phar
|
||||||
|
|
||||||
|
# Cache and logs
|
||||||
|
/cache/*
|
||||||
|
/logs/*
|
||||||
|
!cache/.gitkeep
|
||||||
|
!logs/.gitkeep
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
/uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
|
||||||
|
# Backups
|
||||||
|
/backups/*
|
||||||
|
!backups/.gitkeep
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
49
.htaccess
Normal file
49
.htaccess
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Enable Rewrite Engine
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
|
||||||
|
# Force HTTPS
|
||||||
|
RewriteCond %{HTTPS} off
|
||||||
|
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
|
# Protect sensitive files
|
||||||
|
<FilesMatch "^\.env">
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Protect directories
|
||||||
|
<DirectoryMatch "^/.*/(?:logs|backups|cache)/">
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</DirectoryMatch>
|
||||||
|
|
||||||
|
# Handle PHP errors
|
||||||
|
php_flag display_errors off
|
||||||
|
php_value error_reporting E_ALL
|
||||||
|
php_value error_log logs/php_errors.log
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
Header set X-Content-Type-Options "nosniff"
|
||||||
|
Header set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header set X-XSS-Protection "1; mode=block"
|
||||||
|
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
|
|
||||||
|
# Cache Control
|
||||||
|
<FilesMatch "\.(jpg|jpeg|png|gif|ico|css|js)$">
|
||||||
|
Header set Cache-Control "max-age=31536000, public"
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Prevent directory listing
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Custom error pages
|
||||||
|
ErrorDocument 404 /404.html
|
||||||
|
ErrorDocument 403 /403.html
|
||||||
|
ErrorDocument 500 /500.html
|
||||||
|
|
||||||
|
# URL Rewriting Rules
|
||||||
|
RewriteRule ^product/([0-9]+)/?$ product.php?id=$1 [NC,L]
|
||||||
|
RewriteRule ^category/([^/]+)/?$ category.php?slug=$1 [NC,L]
|
||||||
|
RewriteRule ^blog/([^/]+)/?$ blog.php?slug=$1 [NC,L]
|
17
404.html
Normal file
17
404.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404 - الصفحة غير موجودة | ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
</head>
|
||||||
|
<body class="error-page">
|
||||||
|
<div class="error-container">
|
||||||
|
<h1>404</h1>
|
||||||
|
<h2>عذراً، الصفحة غير موجودة</h2>
|
||||||
|
<p>الصفحة التي تبحث عنها غير موجودة أو تم نقلها.</p>
|
||||||
|
<a href="/" class="btn btn-primary">العودة للصفحة الرئيسية</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
105
README.md
Normal file
105
README.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# ShubraVeil Essential Oils Website
|
||||||
|
|
||||||
|
A modern, responsive website for ShubraVeil Essential Oils company, showcasing their premium quality essential oils sourced from the fertile lands of Shubra Balloula.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Responsive design that works on all devices
|
||||||
|
- Modern and clean user interface
|
||||||
|
- Brand-consistent color scheme
|
||||||
|
- Product showcase section
|
||||||
|
- About section highlighting the company's heritage
|
||||||
|
- Contact information
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
shubraveil/
|
||||||
|
├── css/
|
||||||
|
│ └── style.css
|
||||||
|
├── images/
|
||||||
|
│ ├── logo.svg
|
||||||
|
│ ├── logo-white.svg
|
||||||
|
│ ├── hero-bg.jpg
|
||||||
|
│ ├── jasmine-absolute.jpg
|
||||||
|
│ └── rosemary.jpg
|
||||||
|
├── index.html
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Brand Colors
|
||||||
|
|
||||||
|
- Salem: #0c814a
|
||||||
|
- Spring Bud: #cddf96
|
||||||
|
- Palm Leaf: #768b46
|
||||||
|
- Pine Tree: #252a16
|
||||||
|
- Raisin Black: #231f20
|
||||||
|
- Cultured: #f5f5f5
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Add the required images to the `images` directory
|
||||||
|
3. Open `index.html` in a web browser
|
||||||
|
|
||||||
|
## التثبيت والتكوين
|
||||||
|
|
||||||
|
### المتطلبات الأساسية
|
||||||
|
- PHP 7.4 أو أحدث
|
||||||
|
- MySQL 5.7 أو أحدث
|
||||||
|
- Composer
|
||||||
|
- Node.js و npm
|
||||||
|
- خادم ويب (Apache/Nginx)
|
||||||
|
- تمكين امتدادات PHP التالية:
|
||||||
|
- GD
|
||||||
|
- MySQLi
|
||||||
|
- ZIP
|
||||||
|
- JSON
|
||||||
|
- OpenSSL
|
||||||
|
|
||||||
|
### التثبيت
|
||||||
|
1. استنساخ المستودع:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/shubraveil.git
|
||||||
|
cd shubraveil
|
||||||
|
```
|
||||||
|
|
||||||
|
2. تثبيت اعتماديات PHP:
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. إنشاء ملف .env:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
قم بتحديث المتغيرات في ملف .env بالقيم المناسبة.
|
||||||
|
|
||||||
|
4. إعداد قاعدة البيانات:
|
||||||
|
```bash
|
||||||
|
mysql -u root -p
|
||||||
|
CREATE DATABASE shubraveil_db;
|
||||||
|
```
|
||||||
|
|
||||||
|
5. تهيئة المجلدات:
|
||||||
|
```bash
|
||||||
|
mkdir -p uploads/products
|
||||||
|
mkdir -p cache
|
||||||
|
mkdir -p backups
|
||||||
|
chmod -R 755 uploads cache backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### التكوين
|
||||||
|
#### الإعدادات الأساسية
|
||||||
|
1. تكوين قاعدة البيانات في ملف .env
|
||||||
|
2. تكوين SMTP لإرسال البريد الإلكتروني
|
||||||
|
3. تكوين مفاتيح reCAPTCHA
|
||||||
|
|
||||||
|
#### الأمان
|
||||||
|
- تأكد من تعيين كلمات مرور قوية
|
||||||
|
- قم بتحديث مفتاح JWT_SECRET
|
||||||
|
- قم بتكوين HTTPS
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The website uses vanilla HTML, CSS, and JavaScript for simplicity and performance. The design follows modern web development practices and is built to be easily maintainable and extensible.
|
140
admin/backup.php
Normal file
140
admin/backup.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../includes/config.php';
|
||||||
|
|
||||||
|
class Backup {
|
||||||
|
private $backup_path;
|
||||||
|
private $db_config;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->backup_path = __DIR__ . '/../backups';
|
||||||
|
if (!file_exists($this->backup_path)) {
|
||||||
|
mkdir($this->backup_path, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db_config = [
|
||||||
|
'host' => DB_SERVER,
|
||||||
|
'user' => DB_USERNAME,
|
||||||
|
'pass' => DB_PASSWORD,
|
||||||
|
'name' => DB_NAME
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createBackup() {
|
||||||
|
$timestamp = date('Y-m-d_H-i-s');
|
||||||
|
$backup_file = $this->backup_path . '/backup_' . $timestamp;
|
||||||
|
|
||||||
|
// Backup database
|
||||||
|
$this->backupDatabase($backup_file . '.sql');
|
||||||
|
|
||||||
|
// Backup files
|
||||||
|
$this->backupFiles($backup_file . '.zip');
|
||||||
|
|
||||||
|
// Clean old backups (keep last 5)
|
||||||
|
$this->cleanOldBackups();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backupDatabase($file) {
|
||||||
|
$command = sprintf(
|
||||||
|
'mysqldump --host=%s --user=%s --password=%s %s > %s',
|
||||||
|
escapeshellarg($this->db_config['host']),
|
||||||
|
escapeshellarg($this->db_config['user']),
|
||||||
|
escapeshellarg($this->db_config['pass']),
|
||||||
|
escapeshellarg($this->db_config['name']),
|
||||||
|
escapeshellarg($file)
|
||||||
|
);
|
||||||
|
|
||||||
|
exec($command, $output, $return_var);
|
||||||
|
if ($return_var !== 0) {
|
||||||
|
throw new Exception('Database backup failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backupFiles($file) {
|
||||||
|
$root_path = __DIR__ . '/..';
|
||||||
|
$exclude_dirs = ['backups', 'cache', 'logs', 'vendor'];
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($file, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new Exception('Cannot create zip file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($root_path),
|
||||||
|
RecursiveIteratorIterator::LEAVES_ONLY
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $name => $file) {
|
||||||
|
if ($file->isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $file->getRealPath();
|
||||||
|
$relativePath = substr($filePath, strlen($root_path) + 1);
|
||||||
|
|
||||||
|
// Skip excluded directories
|
||||||
|
$skip = false;
|
||||||
|
foreach ($exclude_dirs as $exclude) {
|
||||||
|
if (strpos($relativePath, $exclude . '/') === 0) {
|
||||||
|
$skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$skip) {
|
||||||
|
$zip->addFile($filePath, $relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanOldBackups($keep = 5) {
|
||||||
|
$files = glob($this->backup_path . '/*');
|
||||||
|
usort($files, function($a, $b) {
|
||||||
|
return filemtime($b) - filemtime($a);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count($files) > $keep) {
|
||||||
|
for ($i = $keep; $i < count($files); $i++) {
|
||||||
|
unlink($files[$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restore($backup_date) {
|
||||||
|
$sql_file = $this->backup_path . '/backup_' . $backup_date . '.sql';
|
||||||
|
$zip_file = $this->backup_path . '/backup_' . $backup_date . '.zip';
|
||||||
|
|
||||||
|
if (!file_exists($sql_file) || !file_exists($zip_file)) {
|
||||||
|
throw new Exception('Backup files not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore database
|
||||||
|
$command = sprintf(
|
||||||
|
'mysql --host=%s --user=%s --password=%s %s < %s',
|
||||||
|
escapeshellarg($this->db_config['host']),
|
||||||
|
escapeshellarg($this->db_config['user']),
|
||||||
|
escapeshellarg($this->db_config['pass']),
|
||||||
|
escapeshellarg($this->db_config['name']),
|
||||||
|
escapeshellarg($sql_file)
|
||||||
|
);
|
||||||
|
|
||||||
|
exec($command, $output, $return_var);
|
||||||
|
if ($return_var !== 0) {
|
||||||
|
throw new Exception('Database restore failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore files
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zip_file) !== true) {
|
||||||
|
throw new Exception('Cannot open backup archive');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->extractTo(__DIR__ . '/..');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
61
admin/config/database.php
Normal file
61
admin/config/database.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
define('DB_SERVER', 'localhost');
|
||||||
|
define('DB_USERNAME', 'root');
|
||||||
|
define('DB_PASSWORD', '');
|
||||||
|
define('DB_NAME', 'shubraveil_db');
|
||||||
|
|
||||||
|
$conn = mysqli_connect(DB_SERVER, DB_USERNAME, DB_PASSWORD);
|
||||||
|
|
||||||
|
if (!$conn) {
|
||||||
|
die("Connection failed: " . mysqli_connect_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create database if not exists
|
||||||
|
$sql = "CREATE DATABASE IF NOT EXISTS " . DB_NAME;
|
||||||
|
if (mysqli_query($conn, $sql)) {
|
||||||
|
mysqli_select_db($conn, DB_NAME);
|
||||||
|
} else {
|
||||||
|
die("Error creating database: " . mysqli_error($conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tables
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)";
|
||||||
|
mysqli_query($conn, $sql);
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price DECIMAL(10,2),
|
||||||
|
image VARCHAR(255),
|
||||||
|
category VARCHAR(50),
|
||||||
|
product_type ENUM('essential_oils', 'fixed_oils', 'hydrosols', 'natural_cosmetics') NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)";
|
||||||
|
mysqli_query($conn, $sql);
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
customer_name VARCHAR(100),
|
||||||
|
customer_email VARCHAR(100),
|
||||||
|
customer_phone VARCHAR(20),
|
||||||
|
order_details TEXT,
|
||||||
|
status VARCHAR(20),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)";
|
||||||
|
mysqli_query($conn, $sql);
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
setting_name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
setting_value TEXT,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)";
|
||||||
|
mysqli_query($conn, $sql);
|
||||||
|
?>
|
230
admin/config/setup.sql
Normal file
230
admin/config/setup.sql
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
full_name VARCHAR(100),
|
||||||
|
phone VARCHAR(20),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
role ENUM('user', 'admin') DEFAULT 'user'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Addresses table
|
||||||
|
CREATE TABLE IF NOT EXISTS addresses (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
address_line1 VARCHAR(255) NOT NULL,
|
||||||
|
address_line2 VARCHAR(255),
|
||||||
|
city VARCHAR(100) NOT NULL,
|
||||||
|
state VARCHAR(100),
|
||||||
|
postal_code VARCHAR(20),
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Categories table
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
name_ar VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
description_ar TEXT,
|
||||||
|
parent_id INT,
|
||||||
|
image_url VARCHAR(255),
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Products table
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
category_id INT,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
description_ar TEXT,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
sale_price DECIMAL(10,2),
|
||||||
|
stock_quantity INT DEFAULT 0,
|
||||||
|
sku VARCHAR(50) UNIQUE,
|
||||||
|
weight DECIMAL(10,2),
|
||||||
|
benefits TEXT,
|
||||||
|
benefits_ar TEXT,
|
||||||
|
ingredients TEXT,
|
||||||
|
ingredients_ar TEXT,
|
||||||
|
usage_instructions TEXT,
|
||||||
|
usage_instructions_ar TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Product Images table
|
||||||
|
CREATE TABLE IF NOT EXISTS product_images (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
product_id INT,
|
||||||
|
image_url VARCHAR(255) NOT NULL,
|
||||||
|
is_primary BOOLEAN DEFAULT FALSE,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Reviews table
|
||||||
|
CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
product_id INT,
|
||||||
|
user_id INT,
|
||||||
|
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||||
|
comment TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_verified BOOLEAN DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Shopping Cart table
|
||||||
|
CREATE TABLE IF NOT EXISTS shopping_cart (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
product_id INT,
|
||||||
|
quantity INT DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Wishlist table
|
||||||
|
CREATE TABLE IF NOT EXISTS wishlist (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
product_id INT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Orders table
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
|
||||||
|
total_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
shipping_address_id INT,
|
||||||
|
payment_method VARCHAR(50),
|
||||||
|
payment_status ENUM('pending', 'paid', 'failed') DEFAULT 'pending',
|
||||||
|
shipping_method VARCHAR(50),
|
||||||
|
shipping_cost DECIMAL(10,2) DEFAULT 0,
|
||||||
|
discount_amount DECIMAL(10,2) DEFAULT 0,
|
||||||
|
coupon_code VARCHAR(50),
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (shipping_address_id) REFERENCES addresses(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Order Items table
|
||||||
|
CREATE TABLE IF NOT EXISTS order_items (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_id INT,
|
||||||
|
product_id INT,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Coupons table
|
||||||
|
CREATE TABLE IF NOT EXISTS coupons (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
code VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
type ENUM('percentage', 'fixed') NOT NULL,
|
||||||
|
value DECIMAL(10,2) NOT NULL,
|
||||||
|
min_purchase DECIMAL(10,2),
|
||||||
|
max_discount DECIMAL(10,2),
|
||||||
|
start_date TIMESTAMP,
|
||||||
|
end_date TIMESTAMP,
|
||||||
|
usage_limit INT,
|
||||||
|
used_count INT DEFAULT 0,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Blog Posts table
|
||||||
|
CREATE TABLE IF NOT EXISTS blog_posts (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
title_ar VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
content_ar TEXT NOT NULL,
|
||||||
|
author_id INT,
|
||||||
|
featured_image VARCHAR(255),
|
||||||
|
status ENUM('draft', 'published') DEFAULT 'draft',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
published_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (author_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Newsletter Subscribers table
|
||||||
|
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Chat Messages table
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
admin_id INT,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE,
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (admin_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FAQ Categories table
|
||||||
|
CREATE TABLE IF NOT EXISTS faq_categories (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
name_ar VARCHAR(100) NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FAQs table
|
||||||
|
CREATE TABLE IF NOT EXISTS faqs (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
category_id INT,
|
||||||
|
question VARCHAR(255) NOT NULL,
|
||||||
|
question_ar VARCHAR(255) NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
answer_ar TEXT NOT NULL,
|
||||||
|
sort_order INT DEFAULT 0,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES faq_categories(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- User Points table
|
||||||
|
CREATE TABLE IF NOT EXISTS user_points (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT,
|
||||||
|
points INT DEFAULT 0,
|
||||||
|
earned_from VARCHAR(50),
|
||||||
|
earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Referral System table
|
||||||
|
CREATE TABLE IF NOT EXISTS referrals (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
referrer_id INT,
|
||||||
|
referred_id INT,
|
||||||
|
status ENUM('pending', 'completed') DEFAULT 'pending',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
FOREIGN KEY (referrer_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (referred_id) REFERENCES users(id)
|
||||||
|
);
|
32
admin/includes/auth.php
Normal file
32
admin/includes/auth.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// التحقق من تسجيل الدخول
|
||||||
|
function isLoggedIn() {
|
||||||
|
return isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من صلاحيات المستخدم
|
||||||
|
function checkUserRole($required_role = 'editor') {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $_SESSION["role"] === 'admin' || $_SESSION["role"] === $required_role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// تسجيل الخروج
|
||||||
|
function logout() {
|
||||||
|
$_SESSION = array();
|
||||||
|
session_destroy();
|
||||||
|
header("location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// تأمين المدخلات
|
||||||
|
function sanitize_input($data) {
|
||||||
|
$data = trim($data);
|
||||||
|
$data = stripslashes($data);
|
||||||
|
$data = htmlspecialchars($data);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
?>
|
179
admin/index.php
Normal file
179
admin/index.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true){
|
||||||
|
header("location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
require_once "config/database.php";
|
||||||
|
|
||||||
|
// Get counts for dashboard
|
||||||
|
$products_count = mysqli_fetch_array(mysqli_query($conn, "SELECT COUNT(*) FROM products"))[0];
|
||||||
|
$orders_count = mysqli_fetch_array(mysqli_query($conn, "SELECT COUNT(*) FROM orders"))[0];
|
||||||
|
$pending_orders = mysqli_fetch_array(mysqli_query($conn, "SELECT COUNT(*) FROM orders WHERE status='pending'"))[0];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>لوحة التحكم - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Tajawal', sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #343a40;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.sidebar a {
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.sidebar a:hover {
|
||||||
|
background-color: #495057;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.dashboard-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="col-md-2 d-none d-md-block sidebar">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="../images/logo.jpg" alt="Logo" style="max-width: 120px;">
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="index.php">
|
||||||
|
<i class="fas fa-home ml-2"></i>
|
||||||
|
الرئيسية
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="products.php">
|
||||||
|
<i class="fas fa-box ml-2"></i>
|
||||||
|
المنتجات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="orders.php">
|
||||||
|
<i class="fas fa-shopping-cart ml-2"></i>
|
||||||
|
الطلبات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.php">
|
||||||
|
<i class="fas fa-cog ml-2"></i>
|
||||||
|
الإعدادات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="logout.php">
|
||||||
|
<i class="fas fa-sign-out-alt ml-2"></i>
|
||||||
|
تسجيل الخروج
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main role="main" class="col-md-10 mr-auto ml-auto col-lg-10 px-4 main-content">
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
|
<h1 class="h2">لوحة التحكم</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="text-primary card-icon">
|
||||||
|
<i class="fas fa-box"></i>
|
||||||
|
</div>
|
||||||
|
<h3>المنتجات</h3>
|
||||||
|
<p class="h2"><?php echo $products_count; ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="text-success card-icon">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
</div>
|
||||||
|
<h3>إجمالي الطلبات</h3>
|
||||||
|
<p class="h2"><?php echo $orders_count; ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<div class="text-warning card-icon">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
<h3>الطلبات المعلقة</h3>
|
||||||
|
<p class="h2"><?php echo $pending_orders; ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Orders -->
|
||||||
|
<div class="dashboard-card mt-4">
|
||||||
|
<h3>آخر الطلبات</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>اسم العميل</th>
|
||||||
|
<th>التاريخ</th>
|
||||||
|
<th>الحالة</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$sql = "SELECT * FROM orders ORDER BY created_at DESC LIMIT 5";
|
||||||
|
$result = mysqli_query($conn, $sql);
|
||||||
|
while($row = mysqli_fetch_assoc($result)) {
|
||||||
|
echo "<tr>";
|
||||||
|
echo "<td>" . $row['id'] . "</td>";
|
||||||
|
echo "<td>" . $row['customer_name'] . "</td>";
|
||||||
|
echo "<td>" . $row['created_at'] . "</td>";
|
||||||
|
echo "<td><span class='badge badge-" .
|
||||||
|
($row['status'] == 'completed' ? 'success' :
|
||||||
|
($row['status'] == 'pending' ? 'warning' : 'secondary')) .
|
||||||
|
"'>" . $row['status'] . "</span></td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
179
admin/login.php
Normal file
179
admin/login.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
require_once "includes/auth.php";
|
||||||
|
require_once "config/database.php";
|
||||||
|
|
||||||
|
// إذا كان المستخدم مسجل دخوله بالفعل، قم بتحويله إلى الصفحة الرئيسية
|
||||||
|
if(isLoggedIn()) {
|
||||||
|
header("location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $password = "";
|
||||||
|
$username_err = $password_err = $login_err = "";
|
||||||
|
|
||||||
|
if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
|
// التحقق من اسم المستخدم
|
||||||
|
if(empty(trim($_POST["username"]))) {
|
||||||
|
$username_err = "الرجاء إدخال اسم المستخدم";
|
||||||
|
} else {
|
||||||
|
$username = sanitize_input($_POST["username"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من كلمة المرور
|
||||||
|
if(empty(trim($_POST["password"]))) {
|
||||||
|
$password_err = "الرجاء إدخال كلمة المرور";
|
||||||
|
} else {
|
||||||
|
$password = trim($_POST["password"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من صحة بيانات تسجيل الدخول
|
||||||
|
if(empty($username_err) && empty($password_err)) {
|
||||||
|
$sql = "SELECT id, username, password, role, full_name FROM users WHERE username = ?";
|
||||||
|
|
||||||
|
if($stmt = mysqli_prepare($conn, $sql)) {
|
||||||
|
mysqli_stmt_bind_param($stmt, "s", $param_username);
|
||||||
|
$param_username = $username;
|
||||||
|
|
||||||
|
if(mysqli_stmt_execute($stmt)) {
|
||||||
|
mysqli_stmt_store_result($stmt);
|
||||||
|
|
||||||
|
if(mysqli_stmt_num_rows($stmt) == 1) {
|
||||||
|
mysqli_stmt_bind_result($stmt, $id, $username, $hashed_password, $role, $full_name);
|
||||||
|
if(mysqli_stmt_fetch($stmt)) {
|
||||||
|
if(password_verify($password, $hashed_password)) {
|
||||||
|
// تم تسجيل الدخول بنجاح
|
||||||
|
$_SESSION["loggedin"] = true;
|
||||||
|
$_SESSION["id"] = $id;
|
||||||
|
$_SESSION["username"] = $username;
|
||||||
|
$_SESSION["role"] = $role;
|
||||||
|
$_SESSION["full_name"] = $full_name;
|
||||||
|
|
||||||
|
// تسجيل وقت آخر تسجيل دخول
|
||||||
|
$update_sql = "UPDATE users SET last_login = NOW() WHERE id = ?";
|
||||||
|
if($update_stmt = mysqli_prepare($conn, $update_sql)) {
|
||||||
|
mysqli_stmt_bind_param($update_stmt, "i", $id);
|
||||||
|
mysqli_stmt_execute($update_stmt);
|
||||||
|
mysqli_stmt_close($update_stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
header("location: index.php");
|
||||||
|
} else {
|
||||||
|
$login_err = "اسم المستخدم أو كلمة المرور غير صحيحة";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$login_err = "اسم المستخدم أو كلمة المرور غير صحيحة";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$login_err = "حدث خطأ ما. الرجاء المحاولة لاحقاً";
|
||||||
|
}
|
||||||
|
mysqli_stmt_close($stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>تسجيل الدخول - لوحة التحكم</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Tajawal', sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.login-logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.login-logo img {
|
||||||
|
max-width: 150px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 12px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.btn-login {
|
||||||
|
background-color: #0c814a;
|
||||||
|
border: none;
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.btn-login:hover {
|
||||||
|
background-color: #096b3b;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.back-to-site {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.back-to-site a {
|
||||||
|
color: #0c814a;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.back-to-site a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-logo">
|
||||||
|
<img src="../images/logo.jpg" alt="ShubraVeil">
|
||||||
|
</div>
|
||||||
|
<h2 class="text-center mb-4">تسجيل الدخول</h2>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if(!empty($login_err)){
|
||||||
|
echo '<div class="alert alert-danger">' . $login_err . '</div>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>اسم المستخدم</label>
|
||||||
|
<input type="text" name="username" class="form-control <?php echo (!empty($username_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $username; ?>">
|
||||||
|
<span class="invalid-feedback"><?php echo $username_err; ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>كلمة المرور</label>
|
||||||
|
<input type="password" name="password" class="form-control <?php echo (!empty($password_err)) ? 'is-invalid' : ''; ?>">
|
||||||
|
<span class="invalid-feedback"><?php echo $password_err; ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary btn-block btn-login">دخول</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="back-to-site">
|
||||||
|
<a href="../index.html">العودة إلى الموقع الرئيسي</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
admin/logout.php
Normal file
7
admin/logout.php
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
$_SESSION = array();
|
||||||
|
session_destroy();
|
||||||
|
header("location: login.php");
|
||||||
|
exit;
|
||||||
|
?>
|
237
admin/products.php
Normal file
237
admin/products.php
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true){
|
||||||
|
header("location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
require_once "config/database.php";
|
||||||
|
|
||||||
|
// Handle product deletion
|
||||||
|
if(isset($_GET['delete'])) {
|
||||||
|
$id = mysqli_real_escape_string($conn, $_GET['delete']);
|
||||||
|
mysqli_query($conn, "DELETE FROM products WHERE id = $id");
|
||||||
|
header("location: products.php");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle product addition/editing
|
||||||
|
if($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||||
|
$name = mysqli_real_escape_string($conn, $_POST['name']);
|
||||||
|
$description = mysqli_real_escape_string($conn, $_POST['description']);
|
||||||
|
$price = mysqli_real_escape_string($conn, $_POST['price']);
|
||||||
|
$category = mysqli_real_escape_string($conn, $_POST['category']);
|
||||||
|
$product_type = mysqli_real_escape_string($conn, $_POST['product_type']);
|
||||||
|
|
||||||
|
if(isset($_POST['id'])) {
|
||||||
|
// Update existing product
|
||||||
|
$id = mysqli_real_escape_string($conn, $_POST['id']);
|
||||||
|
$sql = "UPDATE products SET name='$name', description='$description',
|
||||||
|
price='$price', category='$category', product_type='$product_type' WHERE id=$id";
|
||||||
|
} else {
|
||||||
|
// Add new product
|
||||||
|
if(isset($_FILES['image']) && $_FILES['image']['error'] == 0) {
|
||||||
|
$target_dir = "../images/products/";
|
||||||
|
if (!file_exists($target_dir)) {
|
||||||
|
mkdir($target_dir, 0777, true);
|
||||||
|
}
|
||||||
|
$target_file = $target_dir . basename($_FILES["image"]["name"]);
|
||||||
|
move_uploaded_file($_FILES["image"]["tmp_name"], $target_file);
|
||||||
|
$image_path = "images/products/" . basename($_FILES["image"]["name"]);
|
||||||
|
} else {
|
||||||
|
$image_path = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "INSERT INTO products (name, description, price, category, product_type, image)
|
||||||
|
VALUES ('$name', '$description', '$price', '$category', '$product_type', '$image_path')";
|
||||||
|
}
|
||||||
|
|
||||||
|
mysqli_query($conn, $sql);
|
||||||
|
header("location: products.php");
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>إدارة المنتجات - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Tajawal', sans-serif; }
|
||||||
|
.sidebar {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #343a40;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
.sidebar a {
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.sidebar a:hover {
|
||||||
|
background-color: #495057;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.product-image {
|
||||||
|
max-width: 100px;
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="col-md-2 d-none d-md-block sidebar">
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="../images/logo.jpg" alt="Logo" style="max-width: 120px;">
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-sticky">
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="index.php">
|
||||||
|
<i class="fas fa-home ml-2"></i>
|
||||||
|
الرئيسية
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="products.php">
|
||||||
|
<i class="fas fa-box ml-2"></i>
|
||||||
|
المنتجات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="orders.php">
|
||||||
|
<i class="fas fa-shopping-cart ml-2"></i>
|
||||||
|
الطلبات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="settings.php">
|
||||||
|
<i class="fas fa-cog ml-2"></i>
|
||||||
|
الإعدادات
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="logout.php">
|
||||||
|
<i class="fas fa-sign-out-alt ml-2"></i>
|
||||||
|
تسجيل الخروج
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<main role="main" class="col-md-10 mr-auto ml-auto col-lg-10 px-4 main-content">
|
||||||
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||||
|
<h1 class="h2">إدارة المنتجات</h1>
|
||||||
|
<button class="btn btn-primary" data-toggle="modal" data-target="#addProductModal">
|
||||||
|
<i class="fas fa-plus ml-2"></i>إضافة منتج جديد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Products Table -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>الصورة</th>
|
||||||
|
<th>اسم المنتج</th>
|
||||||
|
<th>السعر</th>
|
||||||
|
<th>الفئة</th>
|
||||||
|
<th>نوع المنتج</th>
|
||||||
|
<th>الإجراءات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$result = mysqli_query($conn, "SELECT * FROM products ORDER BY id DESC");
|
||||||
|
while($row = mysqli_fetch_assoc($result)) {
|
||||||
|
echo "<tr>";
|
||||||
|
echo "<td>" . $row['id'] . "</td>";
|
||||||
|
echo "<td><img src='../" . $row['image'] . "' class='product-image' alt='" . $row['name'] . "'></td>";
|
||||||
|
echo "<td>" . $row['name'] . "</td>";
|
||||||
|
echo "<td>" . $row['price'] . "</td>";
|
||||||
|
echo "<td>" . $row['category'] . "</td>";
|
||||||
|
echo "<td>" . $row['product_type'] . "</td>";
|
||||||
|
echo "<td>
|
||||||
|
<button class='btn btn-sm btn-info edit-product' data-id='" . $row['id'] . "'>
|
||||||
|
<i class='fas fa-edit'></i>
|
||||||
|
</button>
|
||||||
|
<a href='?delete=" . $row['id'] . "' class='btn btn-sm btn-danger' onclick='return confirm(\"هل أنت متأكد من حذف هذا المنتج؟\")'>
|
||||||
|
<i class='fas fa-trash'></i>
|
||||||
|
</a>
|
||||||
|
</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Product Modal -->
|
||||||
|
<div class="modal fade" id="addProductModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">إضافة منتج جديد</h5>
|
||||||
|
<button type="button" class="close ml-0" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form action="" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>اسم المنتج</label>
|
||||||
|
<input type="text" name="name" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>الوصف</label>
|
||||||
|
<textarea name="description" class="form-control" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>السعر</label>
|
||||||
|
<input type="number" name="price" class="form-control" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>الفئة</label>
|
||||||
|
<input type="text" name="category" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>نوع المنتج</label>
|
||||||
|
<select name="product_type" class="form-control" required>
|
||||||
|
<option value="">اختر نوع المنتج</option>
|
||||||
|
<option value="essential_oils">الزيوت الأساسية</option>
|
||||||
|
<option value="fixed_oils">الزيوت الثابتة</option>
|
||||||
|
<option value="hydrosols">الهيدروسولات العطرية</option>
|
||||||
|
<option value="natural_cosmetics">مستحضرات تجميل طبيعية</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>صورة المنتج</label>
|
||||||
|
<input type="file" name="image" class="form-control-file">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">إلغاء</button>
|
||||||
|
<button type="submit" class="btn btn-primary">حفظ</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
apache.conf
Normal file
13
apache.conf
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName shubraveil.local
|
||||||
|
DocumentRoot /home/momaher/Public/Websites/shubraveil
|
||||||
|
|
||||||
|
<Directory /home/momaher/Public/Websites/shubraveil>
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/shubraveil_error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/shubraveil_access.log combined
|
||||||
|
</VirtualHost>
|
30
api/products.php
Normal file
30
api/products.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
|
||||||
|
require_once '../admin/config/database.php';
|
||||||
|
|
||||||
|
$type = isset($_GET['type']) ? mysqli_real_escape_string($conn, $_GET['type']) : null;
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM products";
|
||||||
|
if ($type) {
|
||||||
|
$sql .= " WHERE product_type = '$type'";
|
||||||
|
}
|
||||||
|
$sql .= " ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
$result = mysqli_query($conn, $sql);
|
||||||
|
$products = [];
|
||||||
|
|
||||||
|
while ($row = mysqli_fetch_assoc($result)) {
|
||||||
|
$products[] = [
|
||||||
|
'id' => $row['id'],
|
||||||
|
'name' => $row['name'],
|
||||||
|
'description' => $row['description'],
|
||||||
|
'price' => $row['price'],
|
||||||
|
'image' => $row['image'],
|
||||||
|
'category' => $row['category']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($products);
|
||||||
|
?>
|
1
backups/.gitkeep
Normal file
1
backups/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
103
blog.html
Normal file
103
blog.html
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>المدونة - ShubraVeil</title>
|
||||||
|
<meta name="description" content="اكتشف مقالات حصرية عن الزيوت العطرية وفوائدها وطرق استخدامها">
|
||||||
|
<meta name="keywords" content="زيوت عطرية, مدونة, نصائح, فوائد الزيوت">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<!-- نفس الهيدر السابق -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="blog-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="section-title">المدونة</h1>
|
||||||
|
|
||||||
|
<div class="blog-filters">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" placeholder="ابحث في المقالات...">
|
||||||
|
<button><i class="fas fa-search"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="categories">
|
||||||
|
<button class="category-btn active" data-category="all">الكل</button>
|
||||||
|
<button class="category-btn" data-category="tips">نصائح واستخدامات</button>
|
||||||
|
<button class="category-btn" data-category="benefits">فوائد صحية</button>
|
||||||
|
<button class="category-btn" data-category="news">أخبار وتحديثات</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blog-grid">
|
||||||
|
<!-- مقال 1 -->
|
||||||
|
<article class="blog-card">
|
||||||
|
<div class="blog-image">
|
||||||
|
<img src="images/blog/lavender-benefits.jpg" alt="فوائد اللافندر">
|
||||||
|
<span class="category">فوائد صحية</span>
|
||||||
|
</div>
|
||||||
|
<div class="blog-content">
|
||||||
|
<h2>10 فوائد مذهلة لزيت اللافندر</h2>
|
||||||
|
<p>اكتشف الفوائد المتعددة لزيت اللافندر العطري وكيفية استخدامه للحصول على أفضل النتائج...</p>
|
||||||
|
<div class="blog-meta">
|
||||||
|
<span><i class="far fa-calendar"></i> 8 ديسمبر 2024</span>
|
||||||
|
<span><i class="far fa-clock"></i> 5 دقائق قراءة</span>
|
||||||
|
</div>
|
||||||
|
<a href="blog/lavender-benefits.html" class="read-more">اقرأ المزيد</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- مقال 2 -->
|
||||||
|
<article class="blog-card">
|
||||||
|
<div class="blog-image">
|
||||||
|
<img src="images/blog/essential-oils-guide.jpg" alt="دليل الزيوت العطرية">
|
||||||
|
<span class="category">نصائح واستخدامات</span>
|
||||||
|
</div>
|
||||||
|
<div class="blog-content">
|
||||||
|
<h2>دليلك الشامل لاستخدام الزيوت العطرية</h2>
|
||||||
|
<p>تعرف على الطرق الصحيحة لاستخدام الزيوت العطرية وكيفية مزجها للحصول على أفضل النتائج...</p>
|
||||||
|
<div class="blog-meta">
|
||||||
|
<span><i class="far fa-calendar"></i> 7 ديسمبر 2024</span>
|
||||||
|
<span><i class="far fa-clock"></i> 8 دقائق قراءة</span>
|
||||||
|
</div>
|
||||||
|
<a href="blog/essential-oils-guide.html" class="read-more">اقرأ المزيد</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<button class="prev-page" disabled><i class="fas fa-chevron-right"></i></button>
|
||||||
|
<span class="current-page">1</span>
|
||||||
|
<button class="next-page"><i class="fas fa-chevron-left"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- قسم الاشتراك في النشرة البريدية -->
|
||||||
|
<section class="blog-newsletter">
|
||||||
|
<div class="container">
|
||||||
|
<div class="newsletter-content">
|
||||||
|
<h2>اشترك في نشرتنا البريدية</h2>
|
||||||
|
<p>احصل على أحدث المقالات والنصائح مباشرة إلى بريدك الإلكتروني</p>
|
||||||
|
<form id="blog-newsletter-form" class="newsletter-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" placeholder="أدخل بريدك الإلكتروني" required>
|
||||||
|
<button type="submit" class="btn">اشترك الآن</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<!-- نفس الفوتر السابق -->
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="js/blog.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
cache/.gitkeep
vendored
Normal file
1
cache/.gitkeep
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
1748
composer-setup.php
Normal file
1748
composer-setup.php
Normal file
File diff suppressed because it is too large
Load Diff
44
composer.json
Normal file
44
composer.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "shubraveil/website",
|
||||||
|
"description": "ShubraVeil - نظام إدارة متجر الحجاب",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.4",
|
||||||
|
"phpmailer/phpmailer": "^6.8",
|
||||||
|
"firebase/php-jwt": "^6.4",
|
||||||
|
"vlucas/phpdotenv": "^5.5",
|
||||||
|
"intervention/image": "^2.7",
|
||||||
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
|
"monolog/monolog": "^2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ShubraVeil\\": "includes/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"ShubraVeil\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit",
|
||||||
|
"analyse": "phpstan analyse",
|
||||||
|
"cs": "phpcs",
|
||||||
|
"post-install-cmd": [
|
||||||
|
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
3412
css/style.css
Normal file
3412
css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
185
database/backup.sql
Normal file
185
database/backup.sql
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
-- MySQL dump 10.13 Distrib 8.0.40, for Linux (x86_64)
|
||||||
|
--
|
||||||
|
-- Host: localhost Database: shubraveil_db
|
||||||
|
-- ------------------------------------------------------
|
||||||
|
-- Server version 8.0.40-0ubuntu0.24.04.1
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!50503 SET NAMES utf8mb4 */;
|
||||||
|
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||||
|
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||||
|
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||||
|
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||||
|
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||||
|
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `order_items`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `order_items`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `order_items` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_id` int NOT NULL,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`quantity` int NOT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_order_id` (`order_id`),
|
||||||
|
KEY `idx_product_id` (`product_id`),
|
||||||
|
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`),
|
||||||
|
CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `order_items`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `order_items` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `order_items` DISABLE KEYS */;
|
||||||
|
INSERT INTO `order_items` VALUES (1,1,2,1,199.99);
|
||||||
|
/*!40000 ALTER TABLE `order_items` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `orders`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `orders`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `orders` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`total_amount` decimal(10,2) NOT NULL,
|
||||||
|
`status` enum('pending','processing','shipped','delivered','cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'pending',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_status` (`status`),
|
||||||
|
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `orders`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `orders` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `orders` DISABLE KEYS */;
|
||||||
|
INSERT INTO `orders` VALUES (1,4,199.99,'pending','2024-12-09 18:44:16','2024-12-09 18:44:16');
|
||||||
|
/*!40000 ALTER TABLE `orders` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `password_resets`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `password_resets`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `password_resets` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`expiry` timestamp NOT NULL,
|
||||||
|
`used` tinyint(1) DEFAULT '0',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_token` (`token`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
CONSTRAINT `password_resets_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `password_resets`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `password_resets` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `password_resets` DISABLE KEYS */;
|
||||||
|
/*!40000 ALTER TABLE `password_resets` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `products`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `products` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`description` text COLLATE utf8mb4_unicode_ci,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
`stock` int NOT NULL DEFAULT '0',
|
||||||
|
`category` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||||
|
`image_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_category` (`category`),
|
||||||
|
KEY `idx_price` (`price`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `products` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `products` DISABLE KEYS */;
|
||||||
|
INSERT INTO `products` VALUES (2,'زيت الورد الطبيعي','زيت ورد طبيعي 100% للعناية بالبشرة',199.99,30,NULL,NULL,'2024-12-09 18:44:16','2024-12-09 18:44:16');
|
||||||
|
/*!40000 ALTER TABLE `products` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `users`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!50503 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
`role` enum('admin','user') COLLATE utf8mb4_unicode_ci DEFAULT 'user',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `username` (`username`),
|
||||||
|
UNIQUE KEY `email` (`email`),
|
||||||
|
KEY `idx_email` (`email`),
|
||||||
|
KEY `idx_username` (`username`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Dumping data for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
LOCK TABLES `users` WRITE;
|
||||||
|
/*!40000 ALTER TABLE `users` DISABLE KEYS */;
|
||||||
|
INSERT INTO `users` VALUES (1,'admin','admin@shubraveil.com','$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi','admin','2024-12-09 18:32:26','2024-12-09 18:32:26'),(2,'test_user','test@example.com','$2y$10$gDH0ZaW06YRdGSFQL2y4h.Ee1H40ux3d5nB5RGfkA52G5coEFNkWS','user','2024-12-09 18:32:33','2024-12-09 18:32:33'),(4,'test_user_1733769856','test_1733769856@example.com','$2y$10$fgy4gUfBeVRFzELpj8v2guPMpSjcxJS.s6BoKpKHwX9cpTjT8HU5a','user','2024-12-09 18:44:16','2024-12-09 18:44:16');
|
||||||
|
/*!40000 ALTER TABLE `users` ENABLE KEYS */;
|
||||||
|
UNLOCK TABLES;
|
||||||
|
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||||
|
|
||||||
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
|
-- Dump completed on 2024-12-09 21:10:55
|
75
database/schema.sql
Normal file
75
database/schema.sql
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
-- Create database if not exists
|
||||||
|
CREATE DATABASE IF NOT EXISTS shubraveil_db;
|
||||||
|
USE shubraveil_db;
|
||||||
|
|
||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('admin', 'user') DEFAULT 'user',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_email (email),
|
||||||
|
INDEX idx_username (username)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Products table
|
||||||
|
CREATE TABLE IF NOT EXISTS products (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
stock INT NOT NULL DEFAULT 0,
|
||||||
|
category VARCHAR(50),
|
||||||
|
image_url VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_category (category),
|
||||||
|
INDEX idx_price (price)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Orders table
|
||||||
|
CREATE TABLE IF NOT EXISTS orders (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
total_amount DECIMAL(10,2) NOT NULL,
|
||||||
|
status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Order items table
|
||||||
|
CREATE TABLE IF NOT EXISTS order_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
order_id INT NOT NULL,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||||
|
INDEX idx_order_id (order_id),
|
||||||
|
INDEX idx_product_id (product_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Password resets table
|
||||||
|
CREATE TABLE IF NOT EXISTS password_resets (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
token VARCHAR(64) NOT NULL,
|
||||||
|
expiry TIMESTAMP NOT NULL,
|
||||||
|
used BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
INDEX idx_token (token),
|
||||||
|
INDEX idx_user_id (user_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create default admin user
|
||||||
|
INSERT INTO users (username, email, password, role)
|
||||||
|
VALUES ('admin', 'admin@shubraveil.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin')
|
||||||
|
ON DUPLICATE KEY UPDATE role = 'admin';
|
198
database/shubraveil_db.sql
Normal file
198
database/shubraveil_db.sql
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.0
|
||||||
|
-- Encoding: UTF-8
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+02:00";
|
||||||
|
|
||||||
|
--
|
||||||
|
-- قاعدة البيانات: `shubraveil_db`
|
||||||
|
--
|
||||||
|
CREATE DATABASE IF NOT EXISTS `shubraveil_db` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
USE `shubraveil_db`;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(50) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`email` varchar(100) NOT NULL,
|
||||||
|
`full_name` varchar(100) DEFAULT NULL,
|
||||||
|
`role` enum('admin','editor') NOT NULL DEFAULT 'editor',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `username` (`username`),
|
||||||
|
UNIQUE KEY `email` (`email`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- إدراج بيانات تجريبية للجدول `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `users` (`username`, `password`, `email`, `full_name`, `role`) VALUES
|
||||||
|
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@shubraveil.com', 'مدير النظام', 'admin');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `products` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
`image` varchar(255) DEFAULT NULL,
|
||||||
|
`category` varchar(50) DEFAULT NULL,
|
||||||
|
`product_type` enum('essential_oils','fixed_oils','hydrosols','natural_cosmetics') NOT NULL,
|
||||||
|
`stock` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`is_featured` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- إدراج بيانات تجريبية للجدول `products`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `products` (`name`, `description`, `price`, `category`, `product_type`, `stock`, `is_featured`) VALUES
|
||||||
|
-- الزيوت الأساسية
|
||||||
|
('زيت الياسمين المطلق', 'زيت عطري نقي 100% مستخلص من زهور الياسمين البلدي', 299.99, 'زيوت عطرية', 'essential_oils', 50, 1),
|
||||||
|
('زيت اللافندر', 'زيت عطري طبيعي مستخلص من زهور اللافندر', 149.99, 'زيوت عطرية', 'essential_oils', 75, 1),
|
||||||
|
('زيت النعناع', 'زيت عطري منعش مستخلص من أوراق النعناع الطازجة', 89.99, 'زيوت عطرية', 'essential_oils', 100, 0),
|
||||||
|
('زيت الورد', 'زيت عطري فاخر مستخلص من بتلات الورد البلدي', 399.99, 'زيوت عطرية', 'essential_oils', 30, 1),
|
||||||
|
|
||||||
|
-- الزيوت الثابتة
|
||||||
|
('زيت اللوز الحلو', 'زيت طبيعي غني بالفيتامينات مستخلص من اللوز', 79.99, 'زيوت ثابتة', 'fixed_oils', 80, 1),
|
||||||
|
('زيت الجوجوبا', 'زيت مغذي للبشرة والشعر', 129.99, 'زيوت ثابتة', 'fixed_oils', 60, 1),
|
||||||
|
('زيت الأرغان', 'زيت مغربي أصلي للعناية بالشعر والبشرة', 199.99, 'زيوت ثابتة', 'fixed_oils', 40, 1),
|
||||||
|
('زيت جنين القمح', 'زيت غني بفيتامين E للبشرة', 89.99, 'زيوت ثابتة', 'fixed_oils', 70, 0),
|
||||||
|
|
||||||
|
-- الهيدروسولات العطرية
|
||||||
|
('ماء الورد البلدي', 'ماء ورد طبيعي 100% للبشرة والطهي', 49.99, 'هيدروسولات', 'hydrosols', 150, 1),
|
||||||
|
('ماء الزهر', 'ماء زهر البرتقال الطبيعي', 44.99, 'هيدروسولات', 'hydrosols', 120, 1),
|
||||||
|
('ماء اللافندر', 'هيدروسول اللافندر المهدئ للبشرة', 54.99, 'هيدروسولات', 'hydrosols', 90, 0),
|
||||||
|
('ماء النعناع', 'هيدروسول منعش للبشرة والشعر', 39.99, 'هيدروسولات', 'hydrosols', 100, 0),
|
||||||
|
|
||||||
|
-- مستحضرات تجميل طبيعية
|
||||||
|
('كريم الياسمين المغذي', 'كريم طبيعي بخلاصة الياسمين للترطيب', 149.99, 'مستحضرات تجميل', 'natural_cosmetics', 45, 1),
|
||||||
|
('مقشر الورد الطبيعي', 'مقشر للوجه والجسم بالورد البلدي', 99.99, 'مستحضرات تجميل', 'natural_cosmetics', 60, 1),
|
||||||
|
('بلسم الشفاه الطبيعي', 'بلسم شفاه مرطب بالعسل والزيوت الطبيعية', 29.99, 'مستحضرات تجميل', 'natural_cosmetics', 200, 0),
|
||||||
|
('ماسك الطين المغربي', 'ماسك تنظيف عميق بالطين المغربي', 79.99, 'مستحضرات تجميل', 'natural_cosmetics', 80, 1);
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `orders`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `orders` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_number` varchar(20) NOT NULL,
|
||||||
|
`customer_name` varchar(100) NOT NULL,
|
||||||
|
`customer_email` varchar(100) NOT NULL,
|
||||||
|
`customer_phone` varchar(20) NOT NULL,
|
||||||
|
`customer_address` text NOT NULL,
|
||||||
|
`total_amount` decimal(10,2) NOT NULL,
|
||||||
|
`status` enum('pending','processing','shipped','delivered','cancelled') NOT NULL DEFAULT 'pending',
|
||||||
|
`payment_method` enum('cash_on_delivery','bank_transfer','credit_card') NOT NULL,
|
||||||
|
`payment_status` enum('pending','paid','failed') NOT NULL DEFAULT 'pending',
|
||||||
|
`notes` text DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `order_number` (`order_number`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `order_items`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `order_items` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_id` int(11) NOT NULL,
|
||||||
|
`product_id` int(11) NOT NULL,
|
||||||
|
`quantity` int(11) NOT NULL,
|
||||||
|
`price` decimal(10,2) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `order_id` (`order_id`),
|
||||||
|
KEY `product_id` (`product_id`),
|
||||||
|
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `settings`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `settings` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`setting_name` varchar(50) NOT NULL,
|
||||||
|
`setting_value` text DEFAULT NULL,
|
||||||
|
`setting_type` enum('text','number','boolean','json') NOT NULL DEFAULT 'text',
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `setting_name` (`setting_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- إدراج بيانات تجريبية للجدول `settings`
|
||||||
|
--
|
||||||
|
|
||||||
|
INSERT INTO `settings` (`setting_name`, `setting_value`, `setting_type`) VALUES
|
||||||
|
('site_name', 'ShubraVeil - زيوت طبيعية عطرية', 'text'),
|
||||||
|
('site_description', 'متجر متخصص في الزيوت الطبيعية والعطرية من شبرا بلولة', 'text'),
|
||||||
|
('contact_email', 'info@shubraveil.com', 'text'),
|
||||||
|
('contact_phone', '+20 123 456 789', 'text'),
|
||||||
|
('shipping_cost', '30.00', 'number'),
|
||||||
|
('free_shipping_minimum', '500.00', 'number'),
|
||||||
|
('social_media', '{"facebook":"https://facebook.com/shubraveil","instagram":"https://instagram.com/shubraveil","whatsapp":"+201234567890"}', 'json');
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `product_reviews`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `product_reviews` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int(11) NOT NULL,
|
||||||
|
`customer_name` varchar(100) NOT NULL,
|
||||||
|
`rating` tinyint(1) NOT NULL,
|
||||||
|
`review` text DEFAULT NULL,
|
||||||
|
`status` enum('pending','approved','rejected') NOT NULL DEFAULT 'pending',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `product_id` (`product_id`),
|
||||||
|
CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- بنية الجدول `subscribers`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `subscribers` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`email` varchar(100) NOT NULL,
|
||||||
|
`status` enum('active','unsubscribed') NOT NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `email` (`email`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
COMMIT;
|
50
deploy.sh
Normal file
50
deploy.sh
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# تعيين المتغيرات
|
||||||
|
REMOTE_HOST="" # أدخل عنوان الخادم
|
||||||
|
REMOTE_USER="" # أدخل اسم المستخدم
|
||||||
|
REMOTE_PATH="" # أدخل المسار على الخادم
|
||||||
|
LOCAL_PATH="/home/momaher/Public/Websites/shubraveil"
|
||||||
|
|
||||||
|
# إنشاء نسخة احتياطية من قاعدة البيانات
|
||||||
|
echo "Creating database backup..."
|
||||||
|
mysqldump -u momaher -pMohamed@9498# shubraveil_db > database/backup.sql
|
||||||
|
|
||||||
|
# تجهيز الملفات للرفع
|
||||||
|
echo "Preparing files for upload..."
|
||||||
|
|
||||||
|
# إنشاء مجلد مؤقت للرفع
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
cp -r * $TEMP_DIR/
|
||||||
|
|
||||||
|
# حذف الملفات غير الضرورية
|
||||||
|
cd $TEMP_DIR
|
||||||
|
rm -rf .git .gitignore deploy.sh
|
||||||
|
rm -rf node_modules vendor/*/test vendor/*/tests
|
||||||
|
find . -name "*.log" -type f -delete
|
||||||
|
find . -name "*.tmp" -type f -delete
|
||||||
|
find . -name "*.cache" -type f -delete
|
||||||
|
|
||||||
|
# ضبط صلاحيات الملفات
|
||||||
|
find . -type f -exec chmod 644 {} \;
|
||||||
|
find . -type d -exec chmod 755 {} \;
|
||||||
|
|
||||||
|
# تغيير ملف الإعدادات
|
||||||
|
mv includes/config.production.php includes/config.php
|
||||||
|
|
||||||
|
# رفع الملفات (قم بإزالة التعليق وأدخل معلومات الخادم)
|
||||||
|
# echo "Uploading files..."
|
||||||
|
# rsync -avz --delete --exclude '.git' --exclude 'deploy.sh' ./ $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH
|
||||||
|
|
||||||
|
# تنظيف
|
||||||
|
cd ..
|
||||||
|
rm -rf $TEMP_DIR
|
||||||
|
|
||||||
|
echo "Deployment preparation completed!"
|
||||||
|
echo "Please review the files and upload them to your hosting using FTP or your hosting control panel."
|
||||||
|
echo "Don't forget to:"
|
||||||
|
echo "1. Create the database on your hosting"
|
||||||
|
echo "2. Import the database backup (database/backup.sql)"
|
||||||
|
echo "3. Update includes/config.php with your hosting database credentials"
|
||||||
|
echo "4. Set up proper file permissions on your hosting"
|
||||||
|
echo "5. Test the website after deployment"
|
112
faq.html
Normal file
112
faq.html
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>الأسئلة الشائعة - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<header>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<a href="index.html" class="logo">
|
||||||
|
<img src="images/logo.jpg" alt="ShubraVeil">
|
||||||
|
</a>
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li><a href="index.html">الرئيسية</a></li>
|
||||||
|
<li><a href="index.html#about">من نحن</a></li>
|
||||||
|
<li><a href="index.html#products">منتجاتنا</a></li>
|
||||||
|
<li><a href="index.html#contact">تواصل معنا</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- قسم الأسئلة الشائعة -->
|
||||||
|
<section class="faq-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="section-title">الأسئلة الشائعة</h1>
|
||||||
|
<div class="faq-container">
|
||||||
|
<div class="faq-item">
|
||||||
|
<div class="faq-question">
|
||||||
|
<h3>ما هي الزيوت العطرية؟</h3>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<p>الزيوت العطرية هي مركبات نباتية مركزة تستخرج من النباتات والأعشاب. تتميز برائحتها القوية وفوائدها العلاجية المتعددة.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item">
|
||||||
|
<div class="faq-question">
|
||||||
|
<h3>كيف يمكنني استخدام الزيوت العطرية بأمان؟</h3>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<p>يجب تخفيف معظم الزيوت العطرية قبل الاستخدام المباشر على الجلد. استخدم زيت حامل مثل زيت جوز الهند أو زيت اللوز. قم دائماً باختبار الحساسية قبل الاستخدام الكامل.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item">
|
||||||
|
<div class="faq-question">
|
||||||
|
<h3>ما هي مدة صلاحية الزيوت العطرية؟</h3>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<p>تختلف مدة الصلاحية حسب نوع الزيت، لكن معظم الزيوت العطرية تدوم من سنة إلى سنتين عند تخزينها بشكل صحيح في مكان بارد ومظلم.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="faq-item">
|
||||||
|
<div class="faq-question">
|
||||||
|
<h3>هل منتجاتكم طبيعية 100%؟</h3>
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
<div class="faq-answer">
|
||||||
|
<p>نعم، جميع منتجاتنا طبيعية 100% ومستخلصة من مصادر عضوية موثوقة. نحن نضمن جودة منتجاتنا ونقدم شهادات الجودة عند الطلب.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-about">
|
||||||
|
<img src="images/logo.jpg" alt="ShubraVeil" class="footer-logo">
|
||||||
|
<p>نقدم أجود أنواع الزيوت العطرية الطبيعية المستخلصة من أراضي شبرا بلولة الخصبة</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<h4>روابط سريعة</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="index.html">الرئيسية</a></li>
|
||||||
|
<li><a href="index.html#about">من نحن</a></li>
|
||||||
|
<li><a href="index.html#products">منتجاتنا</a></li>
|
||||||
|
<li><a href="index.html#contact">تواصل معنا</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-social">
|
||||||
|
<h4>تابعنا</h4>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="#"><i class="fab fa-facebook"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-twitter"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>جميع الحقوق محفوظة © 2024 ShubraVeil</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
103
forgot-password.php
Normal file
103
forgot-password.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
require_once 'includes/Security.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
$email = Security::sanitizeInput($_POST['email']);
|
||||||
|
|
||||||
|
if (empty($email)) {
|
||||||
|
throw new Exception('يرجى إدخال البريد الإلكتروني');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception('فشل الاتصال بقاعدة البيانات');
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من وجود البريد الإلكتروني
|
||||||
|
$stmt = $conn->prepare("SELECT id, username FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
throw new Exception('البريد الإلكتروني غير مسجل في النظام');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
|
||||||
|
// إنشاء رمز إعادة تعيين كلمة المرور
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$expiry = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO password_resets (user_id, token, expiry) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("iss", $user['id'], $token, $expiry);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
// إرسال رابط إعادة تعيين كلمة المرور
|
||||||
|
$reset_link = "https://" . $_SERVER['HTTP_HOST'] . "/reset-password.php?token=" . $token;
|
||||||
|
|
||||||
|
// TODO: استخدام نظام البريد الإلكتروني لإرسال الرابط
|
||||||
|
|
||||||
|
$success = 'تم إرسال رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني';
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>نسيت كلمة المرور - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4">استعادة كلمة المرور</h2>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo $error; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo $success; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">البريد الإلكتروني</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">إرسال رابط إعادة التعيين</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p><a href="login.php">العودة إلى تسجيل الدخول</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
images/1.jpg
Normal file
BIN
images/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 244 KiB |
BIN
images/2.jpg
Normal file
BIN
images/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
1
images/hero-bg.jpg
Normal file
1
images/hero-bg.jpg
Normal file
@ -0,0 +1 @@
|
|||||||
|
The image from the first slide showing a person in green holding an essential oil bottle against a natural background would make an excellent hero background image. However, I cannot directly create or manipulate image files. You'll need to save that image as 'hero-bg.jpg' in the images directory.
|
BIN
images/logo.jpg
Normal file
BIN
images/logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
BIN
images/patrin .jpg
Normal file
BIN
images/patrin .jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 186 KiB |
BIN
images/products 2.jpg
Normal file
BIN
images/products 2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 247 KiB |
BIN
images/proudctus.jpg
Normal file
BIN
images/proudctus.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 KiB |
76
includes/Cache.php
Normal file
76
includes/Cache.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Cache {
|
||||||
|
private static $instance = null;
|
||||||
|
private $cache_path;
|
||||||
|
private $duration;
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->cache_path = CACHE_PATH;
|
||||||
|
$this->duration = CACHE_DURATION;
|
||||||
|
|
||||||
|
if (!file_exists($this->cache_path)) {
|
||||||
|
mkdir($this->cache_path, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set($key, $data) {
|
||||||
|
if (!CACHE_ENABLED) return false;
|
||||||
|
|
||||||
|
$cache_file = $this->getCacheFile($key);
|
||||||
|
$cache_data = [
|
||||||
|
'expires' => time() + $this->duration,
|
||||||
|
'data' => $data
|
||||||
|
];
|
||||||
|
|
||||||
|
return file_put_contents($cache_file, serialize($cache_data)) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get($key) {
|
||||||
|
if (!CACHE_ENABLED) return false;
|
||||||
|
|
||||||
|
$cache_file = $this->getCacheFile($key);
|
||||||
|
|
||||||
|
if (!file_exists($cache_file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_data = unserialize(file_get_contents($cache_file));
|
||||||
|
|
||||||
|
if ($cache_data['expires'] < time()) {
|
||||||
|
unlink($cache_file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache_data['data'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($key) {
|
||||||
|
$cache_file = $this->getCacheFile($key);
|
||||||
|
if (file_exists($cache_file)) {
|
||||||
|
return unlink($cache_file);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear() {
|
||||||
|
$files = glob($this->cache_path . '/*');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_file($file)) {
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCacheFile($key) {
|
||||||
|
return $this->cache_path . '/' . md5($key) . '.cache';
|
||||||
|
}
|
||||||
|
}
|
115
includes/ImageHandler.php
Normal file
115
includes/ImageHandler.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ImageHandler {
|
||||||
|
private $upload_path;
|
||||||
|
private $allowed_types;
|
||||||
|
private $max_size;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->upload_path = UPLOAD_PATH;
|
||||||
|
$this->allowed_types = ALLOWED_IMAGE_TYPES;
|
||||||
|
$this->max_size = MAX_IMAGE_SIZE;
|
||||||
|
|
||||||
|
if (!file_exists($this->upload_path)) {
|
||||||
|
mkdir($this->upload_path, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload($file, $optimize = true) {
|
||||||
|
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
||||||
|
throw new Exception('No file uploaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file
|
||||||
|
$this->validateFile($file);
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$filename = uniqid() . '_' . time() . '.' . $extension;
|
||||||
|
$filepath = $this->upload_path . '/' . $filename;
|
||||||
|
|
||||||
|
// Move and optimize
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $filepath)) {
|
||||||
|
if ($optimize) {
|
||||||
|
$this->optimizeImage($filepath);
|
||||||
|
}
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Failed to move uploaded file');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateFile($file) {
|
||||||
|
// Check file size
|
||||||
|
if ($file['size'] > $this->max_size) {
|
||||||
|
throw new Exception('File size exceeds limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file type
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime_type = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
if (!in_array($mime_type, $this->allowed_types)) {
|
||||||
|
throw new Exception('Invalid file type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function optimizeImage($filepath) {
|
||||||
|
$image = null;
|
||||||
|
$mime = mime_content_type($filepath);
|
||||||
|
|
||||||
|
switch ($mime) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$image = imagecreatefromjpeg($filepath);
|
||||||
|
imagejpeg($image, $filepath, 85); // 85% quality
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$image = imagecreatefrompng($filepath);
|
||||||
|
imagepng($image, $filepath, 8); // Compression level 8
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$image = imagecreatefromwebp($filepath);
|
||||||
|
imagewebp($image, $filepath, 85);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($image) {
|
||||||
|
imagedestroy($image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createThumbnail($filename, $width = 200, $height = 200) {
|
||||||
|
$source_path = $this->upload_path . '/' . $filename;
|
||||||
|
$thumb_path = $this->upload_path . '/thumbnails/' . $filename;
|
||||||
|
|
||||||
|
if (!file_exists(dirname($thumb_path))) {
|
||||||
|
mkdir(dirname($thumb_path), 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$source_image = imagecreatefromstring(file_get_contents($source_path));
|
||||||
|
$source_width = imagesx($source_image);
|
||||||
|
$source_height = imagesy($source_image);
|
||||||
|
|
||||||
|
$thumb = imagecreatetruecolor($width, $height);
|
||||||
|
imagecopyresampled($thumb, $source_image, 0, 0, 0, 0, $width, $height, $source_width, $source_height);
|
||||||
|
|
||||||
|
$mime = mime_content_type($source_path);
|
||||||
|
switch ($mime) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
imagejpeg($thumb, $thumb_path, 85);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
imagepng($thumb, $thumb_path, 8);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
imagewebp($thumb, $thumb_path, 85);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagedestroy($source_image);
|
||||||
|
imagedestroy($thumb);
|
||||||
|
|
||||||
|
return 'thumbnails/' . $filename;
|
||||||
|
}
|
||||||
|
}
|
84
includes/ImageProcessor.php
Normal file
84
includes/ImageProcessor.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Intervention\Image\ImageManager;
|
||||||
|
|
||||||
|
class ImageProcessor {
|
||||||
|
private $manager;
|
||||||
|
private $uploadPath;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->manager = new ImageManager(['driver' => 'gd']);
|
||||||
|
$this->uploadPath = __DIR__ . '/../uploads';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process($file, $type = 'product', $width = 800, $height = null) {
|
||||||
|
// Validate file
|
||||||
|
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
|
||||||
|
throw new Exception('No file uploaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create type directory if it doesn't exist
|
||||||
|
$targetDir = $this->uploadPath . '/' . $type;
|
||||||
|
if (!file_exists($targetDir)) {
|
||||||
|
mkdir($targetDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique filename
|
||||||
|
$filename = uniqid() . '_' . time() . '.' . pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$targetPath = $targetDir . '/' . $filename;
|
||||||
|
|
||||||
|
// Process image
|
||||||
|
$image = $this->manager->make($file['tmp_name']);
|
||||||
|
|
||||||
|
// Resize image
|
||||||
|
if ($height) {
|
||||||
|
$image->fit($width, $height, function ($constraint) {
|
||||||
|
$constraint->aspectRatio();
|
||||||
|
$constraint->upsize();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$image->resize($width, null, function ($constraint) {
|
||||||
|
$constraint->aspectRatio();
|
||||||
|
$constraint->upsize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize and save
|
||||||
|
$image->save($targetPath, 85);
|
||||||
|
|
||||||
|
// Create thumbnail
|
||||||
|
$this->createThumbnail($image, $type, $filename);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'filename' => $filename,
|
||||||
|
'path' => $type . '/' . $filename,
|
||||||
|
'thumbnail' => $type . '/thumbnails/' . $filename
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createThumbnail($image, $type, $filename) {
|
||||||
|
$thumbDir = $this->uploadPath . '/' . $type . '/thumbnails';
|
||||||
|
if (!file_exists($thumbDir)) {
|
||||||
|
mkdir($thumbDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$image->fit(200, 200, function ($constraint) {
|
||||||
|
$constraint->aspectRatio();
|
||||||
|
})->save($thumbDir . '/' . $filename, 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($path) {
|
||||||
|
$fullPath = $this->uploadPath . '/' . $path;
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
unlink($fullPath);
|
||||||
|
|
||||||
|
// Delete thumbnail if exists
|
||||||
|
$thumbPath = dirname($fullPath) . '/thumbnails/' . basename($fullPath);
|
||||||
|
if (file_exists($thumbPath)) {
|
||||||
|
unlink($thumbPath);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
72
includes/Logger.php
Normal file
72
includes/Logger.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
|
use Monolog\Formatter\LineFormatter;
|
||||||
|
|
||||||
|
class AppLogger {
|
||||||
|
private static $instance = null;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->logger = new Logger('shubraveil');
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
if (!file_exists(__DIR__ . '/../logs')) {
|
||||||
|
mkdir(__DIR__ . '/../logs', 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add handlers
|
||||||
|
$this->addFileHandler();
|
||||||
|
if (getenv('DEBUG_MODE') === 'true') {
|
||||||
|
$this->addDebugHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addFileHandler() {
|
||||||
|
$formatter = new LineFormatter(
|
||||||
|
"[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
|
||||||
|
"Y-m-d H:i:s"
|
||||||
|
);
|
||||||
|
|
||||||
|
$handler = new RotatingFileHandler(
|
||||||
|
__DIR__ . '/../logs/app.log',
|
||||||
|
30, // Keep last 30 days of logs
|
||||||
|
Logger::INFO
|
||||||
|
);
|
||||||
|
$handler->setFormatter($formatter);
|
||||||
|
$this->logger->pushHandler($handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addDebugHandler() {
|
||||||
|
$handler = new StreamHandler(
|
||||||
|
__DIR__ . '/../logs/debug.log',
|
||||||
|
Logger::DEBUG
|
||||||
|
);
|
||||||
|
$this->logger->pushHandler($handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function info($message, array $context = []) {
|
||||||
|
$this->logger->info($message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function error($message, array $context = []) {
|
||||||
|
$this->logger->error($message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function debug($message, array $context = []) {
|
||||||
|
$this->logger->debug($message, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warning($message, array $context = []) {
|
||||||
|
$this->logger->warning($message, $context);
|
||||||
|
}
|
||||||
|
}
|
92
includes/Mailer.php
Normal file
92
includes/Mailer.php
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
class Mailer {
|
||||||
|
private static $instance = null;
|
||||||
|
private $mailer;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->mailer = new PHPMailer(true);
|
||||||
|
$this->logger = AppLogger::getInstance();
|
||||||
|
|
||||||
|
// Server settings
|
||||||
|
$this->mailer->isSMTP();
|
||||||
|
$this->mailer->Host = SMTP_HOST;
|
||||||
|
$this->mailer->SMTPAuth = true;
|
||||||
|
$this->mailer->Username = SMTP_USERNAME;
|
||||||
|
$this->mailer->Password = SMTP_PASSWORD;
|
||||||
|
$this->mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
$this->mailer->Port = SMTP_PORT;
|
||||||
|
$this->mailer->CharSet = 'UTF-8';
|
||||||
|
|
||||||
|
// Default sender
|
||||||
|
$this->mailer->setFrom(SMTP_FROM_EMAIL, SMTP_FROM_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendOrderConfirmation($order, $user) {
|
||||||
|
try {
|
||||||
|
$this->mailer->addAddress($user['email'], $user['name']);
|
||||||
|
$this->mailer->Subject = 'تأكيد الطلب - ShubraVeil';
|
||||||
|
|
||||||
|
// Load email template
|
||||||
|
$template = file_get_contents(__DIR__ . '/../templates/emails/order-confirmation.html');
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
$template = str_replace('{ORDER_ID}', $order['id'], $template);
|
||||||
|
$template = str_replace('{USER_NAME}', $user['name'], $template);
|
||||||
|
$template = str_replace('{ORDER_TOTAL}', $order['total'], $template);
|
||||||
|
|
||||||
|
$this->mailer->isHTML(true);
|
||||||
|
$this->mailer->Body = $template;
|
||||||
|
|
||||||
|
$this->mailer->send();
|
||||||
|
$this->logger->info('Order confirmation email sent', ['order_id' => $order['id']]);
|
||||||
|
return true;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->error('Failed to send order confirmation email', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'order_id' => $order['id']
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendPasswordReset($user, $token) {
|
||||||
|
try {
|
||||||
|
$this->mailer->addAddress($user['email'], $user['name']);
|
||||||
|
$this->mailer->Subject = 'إعادة تعيين كلمة المرور - ShubraVeil';
|
||||||
|
|
||||||
|
$resetLink = SITE_URL . '/reset-password?token=' . $token;
|
||||||
|
|
||||||
|
// Load email template
|
||||||
|
$template = file_get_contents(__DIR__ . '/../templates/emails/password-reset.html');
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
$template = str_replace('{USER_NAME}', $user['name'], $template);
|
||||||
|
$template = str_replace('{RESET_LINK}', $resetLink, $template);
|
||||||
|
|
||||||
|
$this->mailer->isHTML(true);
|
||||||
|
$this->mailer->Body = $template;
|
||||||
|
|
||||||
|
$this->mailer->send();
|
||||||
|
$this->logger->info('Password reset email sent', ['user_id' => $user['id']]);
|
||||||
|
return true;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->logger->error('Failed to send password reset email', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'user_id' => $user['id']
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
116
includes/Security.php
Normal file
116
includes/Security.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Security {
|
||||||
|
private static $instance = null;
|
||||||
|
private $rate_limit_file;
|
||||||
|
private static $encryptionKey = 'your-secure-encryption-key-here';
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->rate_limit_file = __DIR__ . '/../cache/rate_limits.json';
|
||||||
|
if (!file_exists($this->rate_limit_file)) {
|
||||||
|
if (!is_dir(dirname($this->rate_limit_file))) {
|
||||||
|
mkdir(dirname($this->rate_limit_file), 0755, true);
|
||||||
|
}
|
||||||
|
file_put_contents($this->rate_limit_file, '{}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function sanitizeInput($data) {
|
||||||
|
if (is_array($data)) {
|
||||||
|
return array_map([self::class, 'sanitizeInput'], $data);
|
||||||
|
}
|
||||||
|
return htmlspecialchars(strip_tags(trim($data)), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function validateRecaptcha($response) {
|
||||||
|
$url = 'https://www.google.com/recaptcha/api/siteverify';
|
||||||
|
$data = [
|
||||||
|
'secret' => RECAPTCHA_SECRET_KEY,
|
||||||
|
'response' => $response
|
||||||
|
];
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'http' => [
|
||||||
|
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
||||||
|
'method' => 'POST',
|
||||||
|
'content' => http_build_query($data)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$context = stream_context_create($options);
|
||||||
|
$result = file_get_contents($url, false, $context);
|
||||||
|
$result_json = json_decode($result, true);
|
||||||
|
|
||||||
|
return isset($result_json['success']) && $result_json['success'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkRateLimit($ip, $endpoint, $limit = 60, $window = 3600) {
|
||||||
|
$limits = json_decode(file_get_contents($this->rate_limit_file), true);
|
||||||
|
$now = time();
|
||||||
|
$key = $ip . ':' . $endpoint;
|
||||||
|
|
||||||
|
if (!isset($limits[$key])) {
|
||||||
|
$limits[$key] = ['count' => 0, 'window_start' => $now];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($now - $limits[$key]['window_start'] > $window) {
|
||||||
|
$limits[$key] = ['count' => 0, 'window_start' => $now];
|
||||||
|
}
|
||||||
|
|
||||||
|
$limits[$key]['count']++;
|
||||||
|
file_put_contents($this->rate_limit_file, json_encode($limits));
|
||||||
|
|
||||||
|
return $limits[$key]['count'] <= $limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateCSRFToken() {
|
||||||
|
if (!isset($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verifyCSRFToken($token) {
|
||||||
|
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function encrypt($data) {
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$encrypted = openssl_encrypt(
|
||||||
|
$data,
|
||||||
|
'AES-256-CBC',
|
||||||
|
self::$encryptionKey,
|
||||||
|
0,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
return base64_encode($iv . $encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function decrypt($data) {
|
||||||
|
$data = base64_decode($data);
|
||||||
|
$iv = substr($data, 0, 16);
|
||||||
|
$encrypted = substr($data, 16);
|
||||||
|
return openssl_decrypt(
|
||||||
|
$encrypted,
|
||||||
|
'AES-256-CBC',
|
||||||
|
self::$encryptionKey,
|
||||||
|
0,
|
||||||
|
$iv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hashPassword($password) {
|
||||||
|
return password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verifyPassword($password, $hash) {
|
||||||
|
return password_verify($password, $hash);
|
||||||
|
}
|
||||||
|
}
|
120
includes/auth.php
Normal file
120
includes/auth.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
class Auth {
|
||||||
|
private $conn;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->conn = $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register($data) {
|
||||||
|
$username = sanitize_input($data['username']);
|
||||||
|
$email = sanitize_input($data['email']);
|
||||||
|
$password = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||||
|
$full_name = sanitize_input($data['full_name']);
|
||||||
|
$phone = sanitize_input($data['phone']);
|
||||||
|
|
||||||
|
// Check if email exists
|
||||||
|
$stmt = $this->conn->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($stmt->get_result()->num_rows > 0) {
|
||||||
|
return ['success' => false, 'message' => 'البريد الإلكتروني مسجل مسبقاً'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new user
|
||||||
|
$stmt = $this->conn->prepare("INSERT INTO users (username, email, password, full_name, phone) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param("sssss", $username, $email, $password, $full_name, $phone);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$user_id = $stmt->insert_id;
|
||||||
|
$this->login(['email' => $email, 'password' => $data['password']]);
|
||||||
|
return ['success' => true, 'message' => 'تم التسجيل بنجاح'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء التسجيل'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login($data) {
|
||||||
|
$email = sanitize_input($data['email']);
|
||||||
|
$password = $data['password'];
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("SELECT id, username, password, role FROM users WHERE email = ? AND is_active = 1");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 1) {
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
if (password_verify($password, $user['password'])) {
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['user_role'] = $user['role'];
|
||||||
|
return ['success' => true, 'message' => 'تم تسجيل الدخول بنجاح'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'البريد الإلكتروني أو كلمة المرور غير صحيحة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout() {
|
||||||
|
session_destroy();
|
||||||
|
return ['success' => true, 'message' => 'تم تسجيل الخروج بنجاح'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword($email) {
|
||||||
|
$email = sanitize_input($email);
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$expires = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("UPDATE users SET reset_token = ?, reset_expires = ? WHERE email = ?");
|
||||||
|
$stmt->bind_param("sss", $token, $expires, $email);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
// Send reset email
|
||||||
|
$reset_link = SITE_URL . "/reset-password.php?token=" . $token;
|
||||||
|
// TODO: Implement email sending
|
||||||
|
return ['success' => true, 'message' => 'تم إرسال رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء إعادة تعيين كلمة المرور'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProfile($user_id, $data) {
|
||||||
|
$full_name = sanitize_input($data['full_name']);
|
||||||
|
$phone = sanitize_input($data['phone']);
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("UPDATE users SET full_name = ?, phone = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("ssi", $full_name, $phone, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم تحديث الملف الشخصي بنجاح'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء تحديث الملف الشخصي'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changePassword($user_id, $old_password, $new_password) {
|
||||||
|
$stmt = $this->conn->prepare("SELECT password FROM users WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 1) {
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
if (password_verify($old_password, $user['password'])) {
|
||||||
|
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("UPDATE users SET password = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("si", $hashed_password, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم تغيير كلمة المرور بنجاح'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'كلمة المرور الحالية غير صحيحة'];
|
||||||
|
}
|
||||||
|
}
|
214
includes/cart.php
Normal file
214
includes/cart.php
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
class Cart {
|
||||||
|
private $conn;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->conn = $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addToCart($product_id, $quantity = 1) {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لإضافة منتجات إلى السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if product exists and has enough stock
|
||||||
|
$stmt = $this->conn->prepare("SELECT stock_quantity FROM products WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $product_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
return ['success' => false, 'message' => 'المنتج غير موجود'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = $result->fetch_assoc();
|
||||||
|
if ($product['stock_quantity'] < $quantity) {
|
||||||
|
return ['success' => false, 'message' => 'الكمية المطلوبة غير متوفرة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if product already in cart
|
||||||
|
$stmt = $this->conn->prepare("SELECT id, quantity FROM shopping_cart WHERE user_id = ? AND product_id = ?");
|
||||||
|
$stmt->bind_param("ii", $_SESSION['user_id'], $product_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
$cart_item = $result->fetch_assoc();
|
||||||
|
$new_quantity = $cart_item['quantity'] + $quantity;
|
||||||
|
|
||||||
|
if ($new_quantity > $product['stock_quantity']) {
|
||||||
|
return ['success' => false, 'message' => 'الكمية المطلوبة تتجاوز المخزون المتوفر'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("UPDATE shopping_cart SET quantity = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("ii", $new_quantity, $cart_item['id']);
|
||||||
|
} else {
|
||||||
|
$stmt = $this->conn->prepare("INSERT INTO shopping_cart (user_id, product_id, quantity) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("iii", $_SESSION['user_id'], $product_id, $quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تمت إضافة المنتج إلى السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء إضافة المنتج إلى السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateCartItem($cart_id, $quantity) {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لتحديث السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cart item exists and belongs to user
|
||||||
|
$stmt = $this->conn->prepare("
|
||||||
|
SELECT c.*, p.stock_quantity
|
||||||
|
FROM shopping_cart c
|
||||||
|
JOIN products p ON c.product_id = p.id
|
||||||
|
WHERE c.id = ? AND c.user_id = ?
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ii", $cart_id, $_SESSION['user_id']);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
return ['success' => false, 'message' => 'المنتج غير موجود في السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart_item = $result->fetch_assoc();
|
||||||
|
if ($quantity > $cart_item['stock_quantity']) {
|
||||||
|
return ['success' => false, 'message' => 'الكمية المطلوبة غير متوفرة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($quantity <= 0) {
|
||||||
|
return $this->removeFromCart($cart_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("UPDATE shopping_cart SET quantity = ? WHERE id = ? AND user_id = ?");
|
||||||
|
$stmt->bind_param("iii", $quantity, $cart_id, $_SESSION['user_id']);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم تحديث الكمية'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء تحديث الكمية'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeFromCart($cart_id) {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لحذف منتجات من السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("DELETE FROM shopping_cart WHERE id = ? AND user_id = ?");
|
||||||
|
$stmt->bind_param("ii", $cart_id, $_SESSION['user_id']);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم حذف المنتج من السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء حذف المنتج من السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCart() {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['items' => [], 'total' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = "
|
||||||
|
SELECT c.id as cart_id, c.quantity,
|
||||||
|
p.id as product_id, p.name, p.name_ar, p.price, p.sale_price,
|
||||||
|
(SELECT image_url FROM product_images WHERE product_id = p.id AND is_primary = 1 LIMIT 1) as image
|
||||||
|
FROM shopping_cart c
|
||||||
|
JOIN products p ON c.product_id = p.id
|
||||||
|
WHERE c.user_id = ?
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare($query);
|
||||||
|
$stmt->bind_param("i", $_SESSION['user_id']);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
$total = 0;
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$price = $row['sale_price'] ?? $row['price'];
|
||||||
|
$subtotal = $price * $row['quantity'];
|
||||||
|
$total += $subtotal;
|
||||||
|
|
||||||
|
$row['price'] = $price;
|
||||||
|
$row['subtotal'] = $subtotal;
|
||||||
|
$items[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $items,
|
||||||
|
'total' => $total
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearCart() {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لتفريغ السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("DELETE FROM shopping_cart WHERE user_id = ?");
|
||||||
|
$stmt->bind_param("i", $_SESSION['user_id']);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم تفريغ السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء تفريغ السلة'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyCoupon($code) {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لاستخدام الكوبون'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("
|
||||||
|
SELECT * FROM coupons
|
||||||
|
WHERE code = ?
|
||||||
|
AND is_active = 1
|
||||||
|
AND (start_date IS NULL OR start_date <= NOW())
|
||||||
|
AND (end_date IS NULL OR end_date >= NOW())
|
||||||
|
AND (usage_limit IS NULL OR used_count < usage_limit)
|
||||||
|
");
|
||||||
|
$stmt->bind_param("s", $code);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
return ['success' => false, 'message' => 'الكوبون غير صالح'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$coupon = $result->fetch_assoc();
|
||||||
|
$cart = $this->getCart();
|
||||||
|
|
||||||
|
if ($coupon['min_purchase'] && $cart['total'] < $coupon['min_purchase']) {
|
||||||
|
return ['success' => false, 'message' => sprintf('الحد الأدنى للطلب %s ريال', $coupon['min_purchase'])];
|
||||||
|
}
|
||||||
|
|
||||||
|
$discount = $coupon['type'] === 'percentage'
|
||||||
|
? $cart['total'] * ($coupon['value'] / 100)
|
||||||
|
: $coupon['value'];
|
||||||
|
|
||||||
|
if ($coupon['max_discount'] && $discount > $coupon['max_discount']) {
|
||||||
|
$discount = $coupon['max_discount'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['coupon'] = [
|
||||||
|
'code' => $coupon['code'],
|
||||||
|
'discount' => $discount
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'تم تطبيق الكوبون',
|
||||||
|
'discount' => $discount,
|
||||||
|
'total_after_discount' => $cart['total'] - $discount
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
103
includes/config.php
Normal file
103
includes/config.php
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Error reporting
|
||||||
|
if (getenv('DEBUG_MODE') === 'true') {
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
} else {
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
error_reporting(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
$env = parse_ini_file(__DIR__ . '/../.env');
|
||||||
|
|
||||||
|
// Database configuration
|
||||||
|
define('DB_SERVER', 'localhost');
|
||||||
|
define('DB_USERNAME', 'momaher');
|
||||||
|
define('DB_PASSWORD', 'Mohamed@9498#');
|
||||||
|
define('DB_NAME', 'shubraveil_db');
|
||||||
|
|
||||||
|
// Site configuration
|
||||||
|
define('SITE_NAME', 'ShubraVeil');
|
||||||
|
define('SITE_URL', $env['SITE_URL']);
|
||||||
|
define('UPLOAD_PATH', __DIR__ . '/../uploads');
|
||||||
|
define('ALLOWED_IMAGE_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
|
||||||
|
define('MAX_IMAGE_SIZE', 5 * 1024 * 1024); // 5MB
|
||||||
|
|
||||||
|
// Email configuration
|
||||||
|
define('SMTP_HOST', $env['SMTP_HOST']);
|
||||||
|
define('SMTP_PORT', $env['SMTP_PORT']);
|
||||||
|
define('SMTP_USERNAME', $env['SMTP_USERNAME']);
|
||||||
|
define('SMTP_PASSWORD', $env['SMTP_PASSWORD']);
|
||||||
|
define('SMTP_FROM_EMAIL', $env['SMTP_FROM_EMAIL']);
|
||||||
|
define('SMTP_FROM_NAME', $env['SMTP_FROM_NAME']);
|
||||||
|
|
||||||
|
// Security configuration
|
||||||
|
define('JWT_SECRET', $env['JWT_SECRET']);
|
||||||
|
define('RECAPTCHA_SITE_KEY', $env['RECAPTCHA_SITE_KEY']);
|
||||||
|
define('RECAPTCHA_SECRET_KEY', $env['RECAPTCHA_SECRET_KEY']);
|
||||||
|
|
||||||
|
// Cache configuration
|
||||||
|
define('CACHE_ENABLED', true);
|
||||||
|
define('CACHE_PATH', __DIR__ . '/../cache');
|
||||||
|
define('CACHE_DURATION', 3600); // 1 hour
|
||||||
|
|
||||||
|
// Time zone
|
||||||
|
date_default_timezone_set('Africa/Cairo');
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
function sanitize_input($data) {
|
||||||
|
global $conn;
|
||||||
|
return mysqli_real_escape_string($conn, htmlspecialchars(trim($data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_csrf_token() {
|
||||||
|
if (!isset($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function verify_csrf_token($token) {
|
||||||
|
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_logged_in() {
|
||||||
|
return isset($_SESSION['user_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_admin() {
|
||||||
|
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect($path) {
|
||||||
|
header("Location: " . SITE_URL . $path);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flash_message($type, $message) {
|
||||||
|
$_SESSION['flash'] = [
|
||||||
|
'type' => $type,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_flash_message() {
|
||||||
|
if (isset($_SESSION['flash'])) {
|
||||||
|
$flash = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $flash;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Response helper
|
||||||
|
function json_response($data, $status = 200) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code($status);
|
||||||
|
echo json_encode($data);
|
||||||
|
exit();
|
||||||
|
}
|
34
includes/config.production.php
Normal file
34
includes/config.production.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
// إعدادات قاعدة البيانات للاستضافة
|
||||||
|
define('DB_SERVER', 'localhost'); // عادة ما يكون localhost في Hostinger
|
||||||
|
define('DB_USERNAME', ''); // أدخل اسم المستخدم من Hostinger
|
||||||
|
define('DB_PASSWORD', ''); // أدخل كلمة المرور من Hostinger
|
||||||
|
define('DB_NAME', ''); // أدخل اسم قاعدة البيانات من Hostinger
|
||||||
|
|
||||||
|
// إعدادات الموقع
|
||||||
|
define('SITE_URL', 'https://yourdomain.com'); // أدخل نطاق موقعك
|
||||||
|
define('SITE_NAME', 'ShubraVeil');
|
||||||
|
define('ADMIN_EMAIL', ''); // بريدك الإلكتروني
|
||||||
|
|
||||||
|
// إعدادات الأمان
|
||||||
|
define('SECURE_SESSION', true);
|
||||||
|
define('SESSION_LIFETIME', 3600);
|
||||||
|
define('CSRF_TOKEN_SECRET', bin2hex(random_bytes(32))); // مفتاح عشوائي آمن
|
||||||
|
|
||||||
|
// إعدادات التحميل
|
||||||
|
define('UPLOAD_PATH', __DIR__ . '/../uploads/');
|
||||||
|
define('MAX_FILE_SIZE', 5242880); // 5 ميجابايت
|
||||||
|
define('ALLOWED_FILE_TYPES', ['jpg', 'jpeg', 'png', 'gif']);
|
||||||
|
|
||||||
|
// إعدادات النسخ الاحتياطي
|
||||||
|
define('BACKUP_PATH', __DIR__ . '/../backups/');
|
||||||
|
define('MAX_BACKUP_FILES', 5);
|
||||||
|
|
||||||
|
// تكوين التاريخ والوقت
|
||||||
|
date_default_timezone_set('Africa/Cairo');
|
||||||
|
|
||||||
|
// معالجة الأخطاء
|
||||||
|
error_reporting(0);
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
ini_set('error_log', __DIR__ . '/../logs/error.log');
|
173
includes/products.php
Normal file
173
includes/products.php
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'config.php';
|
||||||
|
|
||||||
|
class Products {
|
||||||
|
private $conn;
|
||||||
|
|
||||||
|
public function __construct($conn) {
|
||||||
|
$this->conn = $conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProducts($filters = [], $page = 1, $limit = 12) {
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
|
||||||
|
if (!empty($filters['category'])) {
|
||||||
|
$where[] = "category_id = ?";
|
||||||
|
$params[] = $filters['category'];
|
||||||
|
$types .= "i";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$search = "%" . $filters['search'] . "%";
|
||||||
|
$where[] = "(name LIKE ? OR name_ar LIKE ? OR description LIKE ? OR description_ar LIKE ?)";
|
||||||
|
$params = array_merge($params, [$search, $search, $search, $search]);
|
||||||
|
$types .= "ssss";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['min_price'])) {
|
||||||
|
$where[] = "price >= ?";
|
||||||
|
$params[] = $filters['min_price'];
|
||||||
|
$types .= "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($filters['max_price'])) {
|
||||||
|
$where[] = "price <= ?";
|
||||||
|
$params[] = $filters['max_price'];
|
||||||
|
$types .= "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereClause = !empty($where) ? "WHERE " . implode(" AND ", $where) : "";
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
$countQuery = "SELECT COUNT(*) as total FROM products $whereClause";
|
||||||
|
$stmt = $this->conn->prepare($countQuery);
|
||||||
|
if (!empty($params)) {
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$total = $stmt->get_result()->fetch_assoc()['total'];
|
||||||
|
|
||||||
|
// Get products
|
||||||
|
$query = "SELECT p.*,
|
||||||
|
c.name as category_name,
|
||||||
|
c.name_ar as category_name_ar,
|
||||||
|
(SELECT image_url FROM product_images WHERE product_id = p.id AND is_primary = 1 LIMIT 1) as primary_image,
|
||||||
|
(SELECT AVG(rating) FROM reviews WHERE product_id = p.id) as average_rating,
|
||||||
|
(SELECT COUNT(*) FROM reviews WHERE product_id = p.id) as review_count
|
||||||
|
FROM products p
|
||||||
|
LEFT JOIN categories c ON p.category_id = c.id
|
||||||
|
$whereClause
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare($query);
|
||||||
|
$params[] = $limit;
|
||||||
|
$params[] = $offset;
|
||||||
|
$types .= "ii";
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
$products = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$products[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'products' => $products,
|
||||||
|
'total' => $total,
|
||||||
|
'pages' => ceil($total / $limit),
|
||||||
|
'current_page' => $page
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProduct($id) {
|
||||||
|
$query = "SELECT p.*,
|
||||||
|
c.name as category_name,
|
||||||
|
c.name_ar as category_name_ar,
|
||||||
|
(SELECT AVG(rating) FROM reviews WHERE product_id = p.id) as average_rating,
|
||||||
|
(SELECT COUNT(*) FROM reviews WHERE product_id = p.id) as review_count
|
||||||
|
FROM products p
|
||||||
|
LEFT JOIN categories c ON p.category_id = c.id
|
||||||
|
WHERE p.id = ?";
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare($query);
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$product = $stmt->get_result()->fetch_assoc();
|
||||||
|
|
||||||
|
if ($product) {
|
||||||
|
// Get images
|
||||||
|
$stmt = $this->conn->prepare("SELECT * FROM product_images WHERE product_id = ? ORDER BY is_primary DESC, sort_order ASC");
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$product['images'] = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$product['images'][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get reviews
|
||||||
|
$stmt = $this->conn->prepare("
|
||||||
|
SELECT r.*, u.username, u.full_name
|
||||||
|
FROM reviews r
|
||||||
|
LEFT JOIN users u ON r.user_id = u.id
|
||||||
|
WHERE r.product_id = ?
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->bind_param("i", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$product['reviews'] = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$product['reviews'][] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get related products
|
||||||
|
$stmt = $this->conn->prepare("
|
||||||
|
SELECT p.*,
|
||||||
|
(SELECT image_url FROM product_images WHERE product_id = p.id AND is_primary = 1 LIMIT 1) as primary_image
|
||||||
|
FROM products p
|
||||||
|
WHERE p.category_id = ? AND p.id != ?
|
||||||
|
LIMIT 4
|
||||||
|
");
|
||||||
|
$stmt->bind_param("ii", $product['category_id'], $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$product['related_products'] = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$product['related_products'][] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addReview($data) {
|
||||||
|
if (!is_logged_in()) {
|
||||||
|
return ['success' => false, 'message' => 'يجب تسجيل الدخول لإضافة تقييم'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->conn->prepare("INSERT INTO reviews (product_id, user_id, rating, comment) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->bind_param("iiis", $data['product_id'], $_SESSION['user_id'], $data['rating'], $data['comment']);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return ['success' => true, 'message' => 'تم إضافة التقييم بنجاح'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => false, 'message' => 'حدث خطأ أثناء إضافة التقييم'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategories() {
|
||||||
|
$query = "SELECT * FROM categories ORDER BY parent_id ASC, name ASC";
|
||||||
|
$result = $this->conn->query($query);
|
||||||
|
$categories = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$categories[] = $row;
|
||||||
|
}
|
||||||
|
return $categories;
|
||||||
|
}
|
||||||
|
}
|
570
index.html
Normal file
570
index.html
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ShubraVeil - زيوت طبيعية عطرية</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="/" class="brand-name">
|
||||||
|
<span class="brand-first">Veil</span>
|
||||||
|
<span class="brand-second">Shubra</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-search">
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" class="search-input" placeholder="ابحث عن منتجاتنا الطبيعية...">
|
||||||
|
<i class="fas fa-search search-icon"></i>
|
||||||
|
<button class="voice-search-btn" title="البحث الصوتي">
|
||||||
|
<i class="fas fa-microphone"></i>
|
||||||
|
</button>
|
||||||
|
<div class="search-results">
|
||||||
|
<div class="search-filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<h3>الفئات</h3>
|
||||||
|
<div class="category-filters">
|
||||||
|
<label class="category-filter">
|
||||||
|
<input type="checkbox" name="category" value="essential-oils">
|
||||||
|
زيوت عطرية
|
||||||
|
</label>
|
||||||
|
<label class="category-filter">
|
||||||
|
<input type="checkbox" name="category" value="fixed-oils">
|
||||||
|
زيوت ثابتة
|
||||||
|
</label>
|
||||||
|
<label class="category-filter">
|
||||||
|
<input type="checkbox" name="category" value="hydrosols">
|
||||||
|
ماء الزهر
|
||||||
|
</label>
|
||||||
|
<label class="category-filter">
|
||||||
|
<input type="checkbox" name="category" value="natural-cosmetics">
|
||||||
|
مستحضرات تجميل طبيعية
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="/" class="active">الرئيسية</a>
|
||||||
|
<a href="/about">من نحن</a>
|
||||||
|
<div class="dropdown">
|
||||||
|
<a href="/products" class="dropdown-toggle">منتجاتنا</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="/products/essential-oils">الزيوت العطرية</a></li>
|
||||||
|
<li><a href="/products/fixed-oils">الزيوت الثابتة</a></li>
|
||||||
|
<li><a href="/products/hydrosols">ماء الزهر</a></li>
|
||||||
|
<li><a href="/products/natural-cosmetics">مستحضرات تجميل طبيعية</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<a href="/benefits" class="dropdown-toggle">فوائد الزيوت</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="/benefits/skin">العناية بالبشرة</a></li>
|
||||||
|
<li><a href="/benefits/hair">العناية بالشعر</a></li>
|
||||||
|
<li><a href="/benefits/health">الفوائد الصحية</a></li>
|
||||||
|
<li><a href="/benefits/aromatherapy">العلاج بالعطور</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="/contact">تواصل معنا</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="nav-actions">
|
||||||
|
<div class="user-account">
|
||||||
|
<a href="/account" class="account-btn">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
<span class="btn-text">حسابي</span>
|
||||||
|
</a>
|
||||||
|
<div class="account-dropdown">
|
||||||
|
<a href="/login">تسجيل الدخول</a>
|
||||||
|
<a href="/register">حساب جديد</a>
|
||||||
|
<a href="/profile">الملف الشخصي</a>
|
||||||
|
<a href="/orders">طلباتي</a>
|
||||||
|
<a href="/wishlist">المفضلة</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shopping-cart">
|
||||||
|
<a href="/cart" class="cart-btn">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
<span class="cart-count">0</span>
|
||||||
|
<span class="btn-text">السلة</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="hero" class="hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1>زيوت طبيعية نقية 100%</h1>
|
||||||
|
<p>نقدم لكم أجود أنواع الزيوت العطرية المستخلصة من أراضي شبرا بلولة الخصبة</p>
|
||||||
|
<div class="hero-buttons">
|
||||||
|
<a href="#products" class="btn primary">تصفح منتجاتنا</a>
|
||||||
|
<a href="#about" class="btn secondary">تعرف علينا</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="features" class="features">
|
||||||
|
<div class="container">
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<i class="fas fa-leaf"></i>
|
||||||
|
<h3>طبيعي 100%</h3>
|
||||||
|
<p>زيوت مستخلصة بطريقة طبيعية</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<i class="fas fa-certificate"></i>
|
||||||
|
<h3>جودة عالية</h3>
|
||||||
|
<p>معتمدة من أفضل المختبرات</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<i class="fas fa-shipping-fast"></i>
|
||||||
|
<h3>شحن سريع</h3>
|
||||||
|
<p>توصيل لجميع المناطق</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="featured" class="featured-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">منتجاتنا المميزة</h2>
|
||||||
|
<div class="featured-products">
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="images/products/lavender.jpg" alt="زيت اللافندر">
|
||||||
|
<span class="badge">الأكثر مبيعاً</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>زيت اللافندر العطري</h3>
|
||||||
|
<p>زيت عطري طبيعي 100% للاسترخاء والعناية بالبشرة</p>
|
||||||
|
<button class="btn">اطلب الآن</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="images/products/rosemary.jpg" alt="زيت إكليل الجبل">
|
||||||
|
<span class="badge">جديد</span>
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>زيت إكليل الجبل</h3>
|
||||||
|
<p>يساعد على تنشيط الذهن وتقوية الشعر</p>
|
||||||
|
<button class="btn">اطلب الآن</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="special-offers" class="special-offers">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>عروض خاصة</h2>
|
||||||
|
<p>اكتشف أفضل العروض والتخفيضات على منتجاتنا</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="offers-grid">
|
||||||
|
<!-- العرض الرئيسي -->
|
||||||
|
<div class="main-offer">
|
||||||
|
<div class="offer-card featured">
|
||||||
|
<div class="offer-badge">خصم 30%</div>
|
||||||
|
<img src="images/products/lavender-oil.jpg" alt="زيت اللافندر">
|
||||||
|
<div class="offer-content">
|
||||||
|
<h3>زيت اللافندر الأصلي</h3>
|
||||||
|
<p>استمتع بخصم حصري على زيت اللافندر النقي 100%</p>
|
||||||
|
<div class="price">
|
||||||
|
<span class="old-price">300 ج.م</span>
|
||||||
|
<span class="new-price">210 ج.م</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary">أضف للسلة</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- العروض الفرعية -->
|
||||||
|
<div class="offers-list">
|
||||||
|
<div class="offer-card">
|
||||||
|
<div class="offer-badge">جديد</div>
|
||||||
|
<img src="images/products/rosemary-oil.jpg" alt="زيت إكليل الجبل">
|
||||||
|
<div class="offer-content">
|
||||||
|
<h3>زيت إكليل الجبل</h3>
|
||||||
|
<p>منتج جديد - عرض خاص لفترة محدودة</p>
|
||||||
|
<div class="price">
|
||||||
|
<span class="new-price">180 ج.م</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary">أضف للسلة</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="offer-card">
|
||||||
|
<div class="offer-badge">خصم 20%</div>
|
||||||
|
<img src="images/products/tea-tree-oil.jpg" alt="زيت شجرة الشاي">
|
||||||
|
<div class="offer-content">
|
||||||
|
<h3>زيت شجرة الشاي</h3>
|
||||||
|
<p>العرض ساري حتى نفاذ الكمية</p>
|
||||||
|
<div class="price">
|
||||||
|
<span class="old-price">250 ج.م</span>
|
||||||
|
<span class="new-price">200 ج.م</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary">أضف للسلة</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="offer-card">
|
||||||
|
<div class="offer-badge">عرض خاص</div>
|
||||||
|
<img src="images/products/peppermint-oil.jpg" alt="زيت النعناع">
|
||||||
|
<div class="offer-content">
|
||||||
|
<h3>زيت النعناع الطبيعي</h3>
|
||||||
|
<p>اشترِ قارورتين واحصل على الثالثة مجاناً</p>
|
||||||
|
<div class="price">
|
||||||
|
<span class="new-price">150 ج.م</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn primary">أضف للسلة</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- شريط العروض المتحرك -->
|
||||||
|
<div class="offers-ticker">
|
||||||
|
<div class="ticker-content">
|
||||||
|
<span>🌟 خصم 15% على جميع المنتجات للأعضاء الجدد</span>
|
||||||
|
<span>🚚 شحن مجاني للطلبات أكثر من 500 ج.م</span>
|
||||||
|
<span>🎁 هدية مجانية مع كل طلب أكثر من 1000 ج.م</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="products" class="products">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>منتجاتنا</h2>
|
||||||
|
<p>مجموعة متنوعة من الزيوت العطرية الطبيعية</p>
|
||||||
|
</div>
|
||||||
|
<div class="products-grid">
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="images/products 2.jpg" alt="زيت الياسمين المطلق">
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>زيت الياسمين المطلق</h3>
|
||||||
|
<p>زيت عطري طبيعي 100% من زهور الياسمين</p>
|
||||||
|
<div class="product-features">
|
||||||
|
<span>طبيعي 100%</span>
|
||||||
|
<span>استخلاص مباشر</span>
|
||||||
|
</div>
|
||||||
|
<a href="#contact" class="btn primary">اطلب الآن</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="images/proudctus.jpg" alt="زيت إكليل الجبل">
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>زيت إكليل الجبل</h3>
|
||||||
|
<p>زيت عطري نقي مستخلص من نبات إكليل الجبل</p>
|
||||||
|
<div class="product-features">
|
||||||
|
<span>طبيعي 100%</span>
|
||||||
|
<span>استخلاص مباشر</span>
|
||||||
|
</div>
|
||||||
|
<a href="#contact" class="btn primary">اطلب الآن</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="about" class="about">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>قرية شبرا بلولة</h2>
|
||||||
|
<p>مصدر الكنوز العطرية الطبيعية</p>
|
||||||
|
</div>
|
||||||
|
<div class="about-content">
|
||||||
|
<div class="about-text">
|
||||||
|
<p>تعد قرية شبرا بلولة مصدراً للكنوز العطرية التي تنمو بين أراضيها الخصبة. من هنا، تستخلص شركتنا زيوتها، معتمدة على هذا المورد الطبيعي الثمين لتقديم منتجات عالية الجودة، تتميز برائحة الياسمين الفريدة، التي تساهم في تعزيز الصحة والجمال.</p>
|
||||||
|
<ul class="about-features">
|
||||||
|
<li>استخلاص طبيعي 100%</li>
|
||||||
|
<li>زراعة عضوية مستدامة</li>
|
||||||
|
<li>جودة عالمية</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="about-image">
|
||||||
|
<img src="images/1.jpg" alt="حقول شبرا بلولة">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="benefits" class="benefits">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>فوائد الزيوت العطرية</h2>
|
||||||
|
<p>اكتشف الفوائد الصحية والجمالية لزيوتنا الطبيعية</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefits-grid">
|
||||||
|
<div class="benefit-card">
|
||||||
|
<i class="fas fa-heart"></i>
|
||||||
|
<h3>الصحة النفسية</h3>
|
||||||
|
<p>تساعد على الاسترخاء وتحسين المزاج</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-card">
|
||||||
|
<i class="fas fa-spa"></i>
|
||||||
|
<h3>العناية بالبشرة</h3>
|
||||||
|
<p>تغذية وترطيب طبيعي للبشرة</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-card">
|
||||||
|
<i class="fas fa-wind"></i>
|
||||||
|
<h3>تعطير المنزل</h3>
|
||||||
|
<p>روائح طبيعية منعشة للمنزل</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="testimonials" class="testimonials-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">آراء عملائنا</h2>
|
||||||
|
<div class="testimonials-slider">
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<div class="testimonial-content">
|
||||||
|
<div class="rating">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
</div>
|
||||||
|
<p>"زيوت عطرية رائعة وطبيعية 100%. استخدمت زيت اللافندر للاسترخاء وكانت النتائج مذهلة!"</p>
|
||||||
|
<div class="testimonial-author">
|
||||||
|
<img src="images/testimonials/user1.jpg" alt="سارة أحمد">
|
||||||
|
<div class="author-info">
|
||||||
|
<h4>سارة أحمد</h4>
|
||||||
|
<span>عميلة منذ 2023</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="testimonial-card">
|
||||||
|
<div class="testimonial-content">
|
||||||
|
<div class="rating">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
</div>
|
||||||
|
<p>"جودة استثنائية وخدمة عملاء ممتازة. أنصح بشدة بزيت إكليل الجبل لتقوية الشعر."</p>
|
||||||
|
<div class="testimonial-author">
|
||||||
|
<img src="images/testimonials/user2.jpg" alt="محمد علي">
|
||||||
|
<div class="author-info">
|
||||||
|
<h4>محمد علي</h4>
|
||||||
|
<span>عميل منذ 2024</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="newsletter" class="newsletter-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="newsletter-content">
|
||||||
|
<h2>اشترك في نشرتنا البريدية</h2>
|
||||||
|
<p>احصل على آخر العروض والأخبار عن منتجاتنا</p>
|
||||||
|
<form id="newsletter-form" class="newsletter-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" id="email" name="email" placeholder="أدخل بريدك الإلكتروني" required>
|
||||||
|
<button type="submit" class="btn">اشترك الآن</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="contact" class="contact">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>تواصل معنا</h2>
|
||||||
|
<p>نحن هنا للإجابة على استفساراتكم</p>
|
||||||
|
</div>
|
||||||
|
<div class="contact-content">
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<i class="fas fa-phone"></i>
|
||||||
|
<h3>اتصل بنا</h3>
|
||||||
|
<p>+20 123 456 789</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<h3>راسلنا</h3>
|
||||||
|
<p>info@shubraveil.com</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
|
<h3>زورنا</h3>
|
||||||
|
<p>شبرا بلولة، مصر</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="reviews-section">
|
||||||
|
<h2>مراجعات العملاء</h2>
|
||||||
|
|
||||||
|
<div class="review-summary">
|
||||||
|
<div class="average-rating">
|
||||||
|
<h3>التقييم العام</h3>
|
||||||
|
<div class="rating-value">4.5</div>
|
||||||
|
<div class="rating-stars">
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star"></i>
|
||||||
|
<i class="fas fa-star-half-alt"></i>
|
||||||
|
</div>
|
||||||
|
<span class="total-reviews">125 مراجعة</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rating-breakdown">
|
||||||
|
<div class="rating-bar">
|
||||||
|
<span>5 نجوم</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 70%"></div>
|
||||||
|
</div>
|
||||||
|
<span>70%</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating-bar">
|
||||||
|
<span>4 نجوم</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 20%"></div>
|
||||||
|
</div>
|
||||||
|
<span>20%</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating-bar">
|
||||||
|
<span>3 نجوم</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 5%"></div>
|
||||||
|
</div>
|
||||||
|
<span>5%</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating-bar">
|
||||||
|
<span>2 نجوم</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 3%"></div>
|
||||||
|
</div>
|
||||||
|
<span>3%</span>
|
||||||
|
</div>
|
||||||
|
<div class="rating-bar">
|
||||||
|
<span>1 نجمة</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 2%"></div>
|
||||||
|
</div>
|
||||||
|
<span>2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="review-controls">
|
||||||
|
<div class="review-filters">
|
||||||
|
<button class="review-filter active" data-rating="all">الكل</button>
|
||||||
|
<button class="review-filter" data-rating="5">5 نجوم</button>
|
||||||
|
<button class="review-filter" data-rating="4">4 نجوم</button>
|
||||||
|
<button class="review-filter" data-rating="3">3 نجوم</button>
|
||||||
|
<button class="review-filter" data-rating="2">2 نجوم</button>
|
||||||
|
<button class="review-filter" data-rating="1">نجمة واحدة</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select id="review-sort" class="review-sort">
|
||||||
|
<option value="newest">الأحدث</option>
|
||||||
|
<option value="highest">الأعلى تقييماً</option>
|
||||||
|
<option value="lowest">الأقل تقييماً</option>
|
||||||
|
<option value="helpful">الأكثر إفادة</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="reviews-container">
|
||||||
|
<!-- سيتم ملء هذا القسم ديناميكياً بالمراجعات -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="review-form" class="review-form">
|
||||||
|
<h3>أضف مراجعتك</h3>
|
||||||
|
|
||||||
|
<div class="rating-input-container">
|
||||||
|
<label>تقييمك:</label>
|
||||||
|
<div class="rating-stars">
|
||||||
|
<i class="far fa-star" data-rating="1"></i>
|
||||||
|
<i class="far fa-star" data-rating="2"></i>
|
||||||
|
<i class="far fa-star" data-rating="3"></i>
|
||||||
|
<i class="far fa-star" data-rating="4"></i>
|
||||||
|
<i class="far fa-star" data-rating="5"></i>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="rating-input" name="rating" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="review-content">مراجعتك:</label>
|
||||||
|
<textarea id="review-content" name="content" placeholder="اكتب رأيك في المنتج..." required></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="review-images">إضافة صور:</label>
|
||||||
|
<input type="file" id="review-images" name="images[]" multiple accept="image/*">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="submit-review">نشر المراجعة</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-about">
|
||||||
|
<img src="images/logo.jpg" alt="ShubraVeil" class="footer-logo">
|
||||||
|
<p>نقدم أجود أنواع الزيوت العطرية الطبيعية المستخلصة من أراضي شبرا بلولة الخصبة</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<h4>روابط سريعة</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#home">الرئيسية</a></li>
|
||||||
|
<li><a href="#about">من نحن</a></li>
|
||||||
|
<li><a href="#products">منتجاتنا</a></li>
|
||||||
|
<li><a href="#contact">تواصل معنا</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-social">
|
||||||
|
<h4>تابعنا</h4>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="#"><i class="fab fa-facebook"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-twitter"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>جميع الحقوق محفوظة © 2024 ShubraVeil</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
100
index.php
Normal file
100
index.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>ShubraVeil - متجر الزيوت الطبيعية</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- شريط التنقل -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="index.php">ShubraVeil</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-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="#products">المنتجات</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#about">من نحن</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#contact">اتصل بنا</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<?php if (isset($_SESSION['user_id'])): ?>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
|
||||||
|
<?php echo htmlspecialchars($_SESSION['username']); ?>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><a class="dropdown-item" href="profile.php">الملف الشخصي</a></li>
|
||||||
|
<li><a class="dropdown-item" href="orders.php">طلباتي</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="logout.php">تسجيل الخروج</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="login.php">تسجيل الدخول</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="register.php">حساب جديد</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- القسم الرئيسي -->
|
||||||
|
<main class="container my-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 text-center">
|
||||||
|
<h1 class="display-4 mb-4">مرحباً بكم في ShubraVeil</h1>
|
||||||
|
<p class="lead">اكتشف مجموعتنا المميزة من الزيوت الطبيعية عالية الجودة</p>
|
||||||
|
<?php if (!isset($_SESSION['user_id'])): ?>
|
||||||
|
<div class="mt-4">
|
||||||
|
<a href="register.php" class="btn btn-primary mx-2">إنشاء حساب</a>
|
||||||
|
<a href="login.php" class="btn btn-outline-primary mx-2">تسجيل الدخول</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- تذييل الصفحة -->
|
||||||
|
<footer class="bg-light py-4 mt-5">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h5>روابط مهمة</h5>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><a href="login.php">تسجيل الدخول</a></li>
|
||||||
|
<li><a href="register.php">حساب جديد</a></li>
|
||||||
|
<li><a href="forgot-password.php">نسيت كلمة المرور؟</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 text-md-end">
|
||||||
|
<p>© <?php echo date('Y'); ?> ShubraVeil. جميع الحقوق محفوظة.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
174
js/cart.js
Normal file
174
js/cart.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
class ShoppingCart {
|
||||||
|
constructor() {
|
||||||
|
this.cart = JSON.parse(localStorage.getItem('cart')) || [];
|
||||||
|
this.cartIcon = document.querySelector('.cart-icon');
|
||||||
|
this.cartCount = document.querySelector('.cart-count');
|
||||||
|
this.cartTotal = document.querySelector('.cart-total');
|
||||||
|
this.cartItems = document.querySelector('.cart-items');
|
||||||
|
this.cartDropdown = document.querySelector('.cart-dropdown');
|
||||||
|
|
||||||
|
this.initializeCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCart() {
|
||||||
|
// تحديث عداد السلة
|
||||||
|
this.updateCartCount();
|
||||||
|
|
||||||
|
// إضافة مستمعي الأحداث
|
||||||
|
document.querySelectorAll('.add-to-cart').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => this.addToCart(e));
|
||||||
|
});
|
||||||
|
|
||||||
|
// تفعيل القائمة المنسدلة للسلة
|
||||||
|
this.cartIcon?.addEventListener('click', () => {
|
||||||
|
this.cartDropdown?.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// إغلاق القائمة عند النقر خارجها
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.cart-container') && this.cartDropdown?.classList.contains('active')) {
|
||||||
|
this.cartDropdown.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addToCart(e) {
|
||||||
|
const productCard = e.target.closest('.product-card');
|
||||||
|
const productId = productCard.dataset.id;
|
||||||
|
const productName = productCard.querySelector('h3').textContent;
|
||||||
|
const productPrice = parseFloat(productCard.querySelector('.product-price').textContent);
|
||||||
|
const productImage = productCard.querySelector('img').src;
|
||||||
|
|
||||||
|
const existingItem = this.cart.find(item => item.id === productId);
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem.quantity += 1;
|
||||||
|
} else {
|
||||||
|
this.cart.push({
|
||||||
|
id: productId,
|
||||||
|
name: productName,
|
||||||
|
price: productPrice,
|
||||||
|
image: productImage,
|
||||||
|
quantity: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCart();
|
||||||
|
this.showNotification('تمت إضافة المنتج إلى السلة');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromCart(productId) {
|
||||||
|
this.cart = this.cart.filter(item => item.id !== productId);
|
||||||
|
this.updateCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQuantity(productId, change) {
|
||||||
|
const item = this.cart.find(item => item.id === productId);
|
||||||
|
if (item) {
|
||||||
|
item.quantity = Math.max(1, item.quantity + change);
|
||||||
|
this.updateCart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCart() {
|
||||||
|
// تحديث Local Storage
|
||||||
|
localStorage.setItem('cart', JSON.stringify(this.cart));
|
||||||
|
|
||||||
|
// تحديث عداد السلة
|
||||||
|
this.updateCartCount();
|
||||||
|
|
||||||
|
// تحديث عرض السلة
|
||||||
|
this.updateCartDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCartCount() {
|
||||||
|
const totalItems = this.cart.reduce((total, item) => total + item.quantity, 0);
|
||||||
|
if (this.cartCount) {
|
||||||
|
this.cartCount.textContent = totalItems;
|
||||||
|
this.cartCount.style.display = totalItems > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCartDisplay() {
|
||||||
|
if (!this.cartItems) return;
|
||||||
|
|
||||||
|
if (this.cart.length === 0) {
|
||||||
|
this.cartItems.innerHTML = '<p class="empty-cart">السلة فارغة</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cartItems.innerHTML = this.cart.map(item => `
|
||||||
|
<div class="cart-item" data-id="${item.id}">
|
||||||
|
<img src="${item.image}" alt="${item.name}">
|
||||||
|
<div class="item-details">
|
||||||
|
<h4>${item.name}</h4>
|
||||||
|
<div class="item-price">${item.price} جنيه</div>
|
||||||
|
<div class="quantity-controls">
|
||||||
|
<button onclick="cart.updateQuantity('${item.id}', -1)">-</button>
|
||||||
|
<span>${item.quantity}</span>
|
||||||
|
<button onclick="cart.updateQuantity('${item.id}', 1)">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="remove-item" onclick="cart.removeFromCart('${item.id}')">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// تحديث المجموع
|
||||||
|
const total = this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||||
|
if (this.cartTotal) {
|
||||||
|
this.cartTotal.textContent = `المجموع: ${total.toFixed(2)} جنيه`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'cart-notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkout() {
|
||||||
|
if (this.cart.length === 0) {
|
||||||
|
alert('السلة فارغة');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/checkout.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: this.cart,
|
||||||
|
total: this.cart.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشلت عملية الشراء');
|
||||||
|
|
||||||
|
// تفريغ السلة بعد نجاح عملية الشراء
|
||||||
|
this.cart = [];
|
||||||
|
this.updateCart();
|
||||||
|
alert('تمت عملية الشراء بنجاح!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Checkout error:', error);
|
||||||
|
alert('حدث خطأ أثناء إتمام عملية الشراء');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة سلة المشتريات
|
||||||
|
const cart = new ShoppingCart();
|
118
js/comments.js
Normal file
118
js/comments.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// نظام التعليقات
|
||||||
|
class CommentSystem {
|
||||||
|
constructor() {
|
||||||
|
this.commentForm = document.getElementById('comment-form');
|
||||||
|
this.commentsContainer = document.getElementById('comments-container');
|
||||||
|
this.replyButtons = document.querySelectorAll('.reply-btn');
|
||||||
|
|
||||||
|
this.initializeEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEventListeners() {
|
||||||
|
// معالجة إضافة تعليق جديد
|
||||||
|
this.commentForm?.addEventListener('submit', (e) => this.handleNewComment(e));
|
||||||
|
|
||||||
|
// معالجة أزرار الرد
|
||||||
|
this.replyButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => this.handleReply(button));
|
||||||
|
});
|
||||||
|
|
||||||
|
// معالجة أزرار الإعجاب
|
||||||
|
document.querySelectorAll('.like-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', () => this.handleLike(button));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleNewComment(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/comments.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل إضافة التعليق');
|
||||||
|
|
||||||
|
const comment = await response.json();
|
||||||
|
this.addCommentToDOM(comment);
|
||||||
|
e.target.reset();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Comment error:', error);
|
||||||
|
alert('حدث خطأ أثناء إضافة التعليق');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReply(button) {
|
||||||
|
const commentId = button.dataset.commentId;
|
||||||
|
const replyForm = document.querySelector(`.reply-form[data-comment-id="${commentId}"]`);
|
||||||
|
|
||||||
|
// إظهار/إخفاء نموذج الرد
|
||||||
|
if (replyForm) {
|
||||||
|
replyForm.classList.toggle('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleLike(button) {
|
||||||
|
const commentId = button.dataset.commentId;
|
||||||
|
const likeCount = button.querySelector('.like-count');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/like-comment.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ commentId })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل تسجيل الإعجاب');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
likeCount.textContent = data.likes;
|
||||||
|
button.classList.toggle('liked');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Like error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommentToDOM(comment) {
|
||||||
|
const commentElement = document.createElement('div');
|
||||||
|
commentElement.className = 'comment';
|
||||||
|
commentElement.innerHTML = `
|
||||||
|
<div class="comment-header">
|
||||||
|
<img src="${comment.userAvatar}" alt="${comment.userName}" class="user-avatar">
|
||||||
|
<div class="comment-info">
|
||||||
|
<h4>${comment.userName}</h4>
|
||||||
|
<span class="comment-date">${comment.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="comment-content">
|
||||||
|
<p>${comment.content}</p>
|
||||||
|
</div>
|
||||||
|
<div class="comment-actions">
|
||||||
|
<button class="like-btn" data-comment-id="${comment.id}">
|
||||||
|
<i class="far fa-heart"></i>
|
||||||
|
<span class="like-count">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="reply-btn" data-comment-id="${comment.id}">
|
||||||
|
<i class="far fa-comment"></i> رد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="reply-form" data-comment-id="${comment.id}">
|
||||||
|
<form>
|
||||||
|
<textarea placeholder="اكتب ردك هنا..."></textarea>
|
||||||
|
<button type="submit" class="btn">إرسال الرد</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.commentsContainer.prepend(commentElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة نظام التعليقات
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new CommentSystem();
|
||||||
|
});
|
239
js/main.js
Normal file
239
js/main.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Mobile Menu Toggle
|
||||||
|
const mobileMenuBtn = document.querySelector('.mobile-menu');
|
||||||
|
const navLinks = document.querySelector('.nav-links');
|
||||||
|
const navbar = document.querySelector('.navbar');
|
||||||
|
|
||||||
|
mobileMenuBtn.addEventListener('click', function() {
|
||||||
|
navLinks.classList.toggle('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth Scrolling for Navigation Links
|
||||||
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
|
anchor.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = document.querySelector(this.getAttribute('href'));
|
||||||
|
if (target) {
|
||||||
|
const headerOffset = 100;
|
||||||
|
const elementPosition = target.getBoundingClientRect().top;
|
||||||
|
const offsetPosition = elementPosition - headerOffset;
|
||||||
|
|
||||||
|
window.scrollBy({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
// Close mobile menu if open
|
||||||
|
navLinks.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// تغيير الهيدر عند التمرير
|
||||||
|
window.addEventListener('scroll', function() {
|
||||||
|
if (window.scrollY > 50) {
|
||||||
|
navbar.classList.add('scrolled');
|
||||||
|
} else {
|
||||||
|
navbar.classList.remove('scrolled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// إضافة الكلاس عند تحميل الصفحة إذا كنا في منتصف الصفحة
|
||||||
|
if (window.scrollY > 50) {
|
||||||
|
navbar.classList.add('scrolled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// تفعيل الأسئلة الشائعة
|
||||||
|
document.querySelectorAll('.faq-question').forEach(question => {
|
||||||
|
question.addEventListener('click', () => {
|
||||||
|
const faqItem = question.parentElement;
|
||||||
|
faqItem.classList.toggle('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// معالجة النشرة البريدية
|
||||||
|
document.getElementById('newsletter-form')?.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const email = e.target.email.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/newsletter-subscribe.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert('شكراً لاشتراكك في نشرتنا البريدية!');
|
||||||
|
e.target.reset();
|
||||||
|
} else {
|
||||||
|
throw new Error('حدث خطأ أثناء الاشتراك');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('عذراً، حدث خطأ. يرجى المحاولة مرة أخرى.');
|
||||||
|
console.error('Newsletter subscription error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced Search Functionality
|
||||||
|
const searchBar = document.querySelector('.search-bar');
|
||||||
|
const searchIcon = document.querySelector('.search-icon');
|
||||||
|
const voiceSearchIcon = document.querySelector('.voice-search-icon');
|
||||||
|
const searchSuggestions = document.querySelector('.search-suggestions');
|
||||||
|
const filterTags = document.querySelectorAll('.filter-tag');
|
||||||
|
const searchHistory = document.querySelector('.search-history');
|
||||||
|
|
||||||
|
// Load search history from localStorage
|
||||||
|
let searchHistoryItems = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
||||||
|
updateSearchHistory();
|
||||||
|
|
||||||
|
// Search input handler
|
||||||
|
let searchTimeout;
|
||||||
|
searchBar.addEventListener('input', function() {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
const query = this.value.trim();
|
||||||
|
|
||||||
|
if (query.length > 0) {
|
||||||
|
searchIcon.classList.add('searching');
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
fetchSearchSuggestions(query);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
searchSuggestions.classList.remove('active');
|
||||||
|
searchIcon.classList.remove('searching');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Voice search handler
|
||||||
|
voiceSearchIcon.addEventListener('click', function() {
|
||||||
|
if ('webkitSpeechRecognition' in window) {
|
||||||
|
const recognition = new webkitSpeechRecognition();
|
||||||
|
recognition.lang = 'ar';
|
||||||
|
recognition.continuous = false;
|
||||||
|
recognition.interimResults = false;
|
||||||
|
|
||||||
|
recognition.onstart = function() {
|
||||||
|
voiceSearchIcon.classList.add('recording');
|
||||||
|
searchBar.placeholder = 'جاري الاستماع...';
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onresult = function(event) {
|
||||||
|
const transcript = event.results[0][0].transcript;
|
||||||
|
searchBar.value = transcript;
|
||||||
|
searchBar.dispatchEvent(new Event('input'));
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = function() {
|
||||||
|
voiceSearchIcon.classList.remove('recording');
|
||||||
|
searchBar.placeholder = 'ابحث عن المنتجات...';
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = function() {
|
||||||
|
voiceSearchIcon.classList.remove('recording');
|
||||||
|
searchBar.placeholder = 'ابحث عن المنتجات...';
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.start();
|
||||||
|
} else {
|
||||||
|
alert('عذراً، البحث الصوتي غير مدعوم في متصفحك');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter tags handler
|
||||||
|
filterTags.forEach(tag => {
|
||||||
|
tag.addEventListener('click', function() {
|
||||||
|
this.classList.toggle('active');
|
||||||
|
const activeFilters = Array.from(filterTags)
|
||||||
|
.filter(tag => tag.classList.contains('active'))
|
||||||
|
.map(tag => tag.textContent);
|
||||||
|
|
||||||
|
if (activeFilters.length > 0) {
|
||||||
|
searchBar.value = activeFilters.join(' ');
|
||||||
|
searchBar.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock function for search suggestions (replace with actual API call)
|
||||||
|
async function fetchSearchSuggestions(query) {
|
||||||
|
try {
|
||||||
|
// Simulate API call delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
// Mock suggestions (replace with actual API response)
|
||||||
|
const suggestions = [
|
||||||
|
'زيت اللافندر العطري',
|
||||||
|
'زيت الورد العطري',
|
||||||
|
'زيت اللوز الثابت',
|
||||||
|
'ماء الورد الطبيعي',
|
||||||
|
'كريم مرطب طبيعي'
|
||||||
|
].filter(item => item.includes(query));
|
||||||
|
|
||||||
|
// Update suggestions UI
|
||||||
|
searchSuggestions.innerHTML = suggestions.map(item =>
|
||||||
|
`<div class="suggestion-item">${item}</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
if (suggestions.length > 0) {
|
||||||
|
searchSuggestions.classList.add('active');
|
||||||
|
} else {
|
||||||
|
searchSuggestions.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click handlers to suggestions
|
||||||
|
document.querySelectorAll('.suggestion-item').forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
searchBar.value = this.textContent;
|
||||||
|
addToSearchHistory(this.textContent);
|
||||||
|
searchSuggestions.classList.remove('active');
|
||||||
|
// Trigger search with selected suggestion
|
||||||
|
performSearch(this.textContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching suggestions:', error);
|
||||||
|
} finally {
|
||||||
|
searchIcon.classList.remove('searching');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to search history
|
||||||
|
function addToSearchHistory(query) {
|
||||||
|
if (!searchHistoryItems.includes(query)) {
|
||||||
|
searchHistoryItems.unshift(query);
|
||||||
|
if (searchHistoryItems.length > 5) {
|
||||||
|
searchHistoryItems.pop();
|
||||||
|
}
|
||||||
|
localStorage.setItem('searchHistory', JSON.stringify(searchHistoryItems));
|
||||||
|
updateSearchHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update search history UI
|
||||||
|
function updateSearchHistory() {
|
||||||
|
if (searchHistoryItems.length > 0) {
|
||||||
|
const historyHtml = searchHistoryItems
|
||||||
|
.map(item => `<span class="history-item">${item}</span>`)
|
||||||
|
.join('');
|
||||||
|
searchHistory.innerHTML = 'عمليات البحث السابقة: ' + historyHtml;
|
||||||
|
|
||||||
|
// Add click handlers to history items
|
||||||
|
document.querySelectorAll('.history-item').forEach(item => {
|
||||||
|
item.addEventListener('click', function() {
|
||||||
|
searchBar.value = this.textContent;
|
||||||
|
searchBar.dispatchEvent(new Event('input'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
searchHistory.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform search (replace with actual search implementation)
|
||||||
|
function performSearch(query) {
|
||||||
|
console.log('Performing search for:', query);
|
||||||
|
// Implement actual search functionality here
|
||||||
|
}
|
||||||
|
});
|
278
js/reviews.js
Normal file
278
js/reviews.js
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
class ReviewSystem {
|
||||||
|
constructor() {
|
||||||
|
this.reviewForm = document.getElementById('review-form');
|
||||||
|
this.reviewsContainer = document.getElementById('reviews-container');
|
||||||
|
this.ratingInput = document.getElementById('rating-input');
|
||||||
|
|
||||||
|
this.initializeReviews();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeReviews() {
|
||||||
|
// تهيئة نظام التقييم بالنجوم
|
||||||
|
this.initializeStarRating();
|
||||||
|
|
||||||
|
// معالجة إرسال المراجعة
|
||||||
|
this.reviewForm?.addEventListener('submit', (e) => this.handleReviewSubmit(e));
|
||||||
|
|
||||||
|
// تهيئة فلترة المراجعات
|
||||||
|
this.initializeReviewFilters();
|
||||||
|
|
||||||
|
// تحميل المراجعات الحالية
|
||||||
|
this.loadReviews();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeStarRating() {
|
||||||
|
const ratingStars = document.querySelectorAll('.rating-stars i');
|
||||||
|
|
||||||
|
ratingStars.forEach((star, index) => {
|
||||||
|
star.addEventListener('mouseover', () => {
|
||||||
|
this.updateStars(ratingStars, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
star.addEventListener('click', () => {
|
||||||
|
this.ratingInput.value = index + 1;
|
||||||
|
star.classList.add('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// إعادة تعيين النجوم عند إزالة المؤشر
|
||||||
|
document.querySelector('.rating-stars')?.addEventListener('mouseleave', () => {
|
||||||
|
this.updateStars(ratingStars, this.ratingInput.value - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStars(stars, activeIndex) {
|
||||||
|
stars.forEach((star, index) => {
|
||||||
|
star.classList.toggle('active', index <= activeIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleReviewSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/reviews.php', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل إرسال المراجعة');
|
||||||
|
|
||||||
|
const review = await response.json();
|
||||||
|
this.addReviewToDOM(review);
|
||||||
|
this.showNotification('تم إضافة مراجعتك بنجاح');
|
||||||
|
e.target.reset();
|
||||||
|
this.updateStars(document.querySelectorAll('.rating-stars i'), -1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Review submission error:', error);
|
||||||
|
this.showNotification('حدث خطأ أثناء إرسال المراجعة', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeReviewFilters() {
|
||||||
|
const filterButtons = document.querySelectorAll('.review-filter');
|
||||||
|
filterButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
button.classList.add('active');
|
||||||
|
this.filterReviews(button.dataset.rating);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// تهيئة الترتيب
|
||||||
|
document.getElementById('review-sort')?.addEventListener('change', (e) => {
|
||||||
|
this.sortReviews(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadReviews() {
|
||||||
|
try {
|
||||||
|
const productId = this.reviewsContainer?.dataset.productId;
|
||||||
|
const response = await fetch(`/api/reviews.php?product_id=${productId}`);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل تحميل المراجعات');
|
||||||
|
|
||||||
|
const reviews = await response.json();
|
||||||
|
this.displayReviews(reviews);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reviews loading error:', error);
|
||||||
|
this.showNotification('حدث خطأ أثناء تحميل المراجعات', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayReviews(reviews) {
|
||||||
|
if (!this.reviewsContainer) return;
|
||||||
|
|
||||||
|
if (reviews.length === 0) {
|
||||||
|
this.reviewsContainer.innerHTML = '<p class="no-reviews">لا توجد مراجعات بعد</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reviewsContainer.innerHTML = reviews.map(review => `
|
||||||
|
<div class="review-card" data-rating="${review.rating}">
|
||||||
|
<div class="review-header">
|
||||||
|
<div class="reviewer-info">
|
||||||
|
<img src="${review.userAvatar}" alt="${review.userName}" class="reviewer-avatar">
|
||||||
|
<div>
|
||||||
|
<h4>${review.userName}</h4>
|
||||||
|
<div class="review-rating">
|
||||||
|
${this.generateStars(review.rating)}
|
||||||
|
</div>
|
||||||
|
<span class="review-date">${review.date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="review-verification">
|
||||||
|
${review.verified ? '<span class="verified-badge"><i class="fas fa-check-circle"></i> شراء موثق</span>' : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="review-content">
|
||||||
|
<p>${review.content}</p>
|
||||||
|
${review.images ? this.generateImageGallery(review.images) : ''}
|
||||||
|
</div>
|
||||||
|
<div class="review-actions">
|
||||||
|
<button class="helpful-btn" onclick="reviewSystem.markHelpful('${review.id}')">
|
||||||
|
<i class="far fa-thumbs-up"></i>
|
||||||
|
<span class="helpful-count">${review.helpfulCount}</span>
|
||||||
|
</button>
|
||||||
|
<button class="report-btn" onclick="reviewSystem.reportReview('${review.id}')">
|
||||||
|
<i class="far fa-flag"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
generateStars(rating) {
|
||||||
|
return Array(5).fill().map((_, index) => `
|
||||||
|
<i class="fas fa-star ${index < rating ? 'filled' : ''}"></i>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
generateImageGallery(images) {
|
||||||
|
return `
|
||||||
|
<div class="review-images">
|
||||||
|
${images.map(image => `
|
||||||
|
<img src="${image}" alt="صورة المراجعة" onclick="reviewSystem.showImageModal('${image}')">
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterReviews(rating) {
|
||||||
|
const reviews = document.querySelectorAll('.review-card');
|
||||||
|
|
||||||
|
reviews.forEach(review => {
|
||||||
|
if (rating === 'all' || review.dataset.rating === rating) {
|
||||||
|
review.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
review.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sortReviews(criteria) {
|
||||||
|
const reviews = Array.from(document.querySelectorAll('.review-card'));
|
||||||
|
const container = this.reviewsContainer;
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
reviews.sort((a, b) => {
|
||||||
|
switch (criteria) {
|
||||||
|
case 'newest':
|
||||||
|
return new Date(b.querySelector('.review-date').textContent) -
|
||||||
|
new Date(a.querySelector('.review-date').textContent);
|
||||||
|
case 'highest':
|
||||||
|
return b.dataset.rating - a.dataset.rating;
|
||||||
|
case 'lowest':
|
||||||
|
return a.dataset.rating - b.dataset.rating;
|
||||||
|
case 'helpful':
|
||||||
|
return parseInt(b.querySelector('.helpful-count').textContent) -
|
||||||
|
parseInt(a.querySelector('.helpful-count').textContent);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
reviews.forEach(review => container.appendChild(review));
|
||||||
|
}
|
||||||
|
|
||||||
|
async markHelpful(reviewId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/helpful-review.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ reviewId })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل تسجيل المساعدة');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
document.querySelector(`[data-review-id="${reviewId}"] .helpful-count`).textContent = data.helpfulCount;
|
||||||
|
this.showNotification('شكراً لتقييمك');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Helpful marking error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reportReview(reviewId) {
|
||||||
|
const reason = prompt('يرجى ذكر سبب الإبلاغ عن هذه المراجعة:');
|
||||||
|
if (!reason) return;
|
||||||
|
|
||||||
|
fetch('/api/report-review.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ reviewId, reason })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
this.showNotification('شكراً لإبلاغك. سنراجع المحتوى.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Review report error:', error);
|
||||||
|
this.showNotification('حدث خطأ أثناء الإبلاغ', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showImageModal(imageSrc) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'image-modal';
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-content">
|
||||||
|
<img src="${imageSrc}" alt="صورة المراجعة">
|
||||||
|
<button class="close-modal">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
modal.querySelector('.close-modal').addEventListener('click', () => modal.remove());
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) modal.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = 'success') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `review-notification ${type}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة نظام المراجعات
|
||||||
|
const reviewSystem = new ReviewSystem();
|
483
js/search.js
Normal file
483
js/search.js
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
// متغيرات عامة
|
||||||
|
let selectedProducts = [];
|
||||||
|
const maxCompareItems = 3;
|
||||||
|
|
||||||
|
// تهيئة صفحة البحث
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initializeSearch();
|
||||||
|
initializeComparison();
|
||||||
|
});
|
||||||
|
|
||||||
|
// تهيئة نظام البحث
|
||||||
|
function initializeSearch() {
|
||||||
|
const searchForm = document.getElementById('advanced-search');
|
||||||
|
const filters = document.querySelectorAll('select, input');
|
||||||
|
|
||||||
|
// معالجة نموذج البحث
|
||||||
|
searchForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await performSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// تحديث النتائج عند تغيير الفلاتر
|
||||||
|
filters.forEach(filter => {
|
||||||
|
filter.addEventListener('change', debounce(performSearch, 500));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// تنفيذ البحث
|
||||||
|
async function performSearch() {
|
||||||
|
const searchQuery = document.getElementById('search-query').value;
|
||||||
|
const productType = document.getElementById('product-type').value;
|
||||||
|
const minPrice = document.getElementById('min-price').value;
|
||||||
|
const maxPrice = document.getElementById('max-price').value;
|
||||||
|
const sortBy = document.getElementById('sort-by').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/search.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: searchQuery,
|
||||||
|
type: productType,
|
||||||
|
minPrice,
|
||||||
|
maxPrice,
|
||||||
|
sortBy
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل البحث');
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
displayResults(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
showError('حدث خطأ أثناء البحث. يرجى المحاولة مرة أخرى.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// عرض نتائج البحث
|
||||||
|
function displayResults(results) {
|
||||||
|
const resultsContainer = document.getElementById('search-results');
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
resultsContainer.innerHTML = '<div class="no-results">لم يتم العثور على نتائج</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultsContainer.innerHTML = results.map(product => `
|
||||||
|
<div class="product-card" data-id="${product.id}">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="${product.image}" alt="${product.name}">
|
||||||
|
${product.badge ? `<span class="badge">${product.badge}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>${product.name}</h3>
|
||||||
|
<p>${product.description}</p>
|
||||||
|
<div class="product-price">${product.price} جنيه</div>
|
||||||
|
<div class="product-actions">
|
||||||
|
<button class="btn add-to-cart">أضف للسلة</button>
|
||||||
|
<button class="btn-secondary add-to-compare" onclick="toggleCompare(${product.id})">
|
||||||
|
<i class="fas fa-exchange-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// نظام المقارنة
|
||||||
|
function initializeComparison() {
|
||||||
|
document.getElementById('compare-btn').addEventListener('click', () => {
|
||||||
|
if (selectedProducts.length > 1) {
|
||||||
|
window.location.href = `/compare.html?products=${selectedProducts.join(',')}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// إضافة/إزالة منتج من المقارنة
|
||||||
|
function toggleCompare(productId) {
|
||||||
|
const index = selectedProducts.indexOf(productId);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
if (selectedProducts.length >= maxCompareItems) {
|
||||||
|
alert(`يمكنك مقارنة ${maxCompareItems} منتجات كحد أقصى`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedProducts.push(productId);
|
||||||
|
} else {
|
||||||
|
selectedProducts.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComparisonUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// تحديث واجهة المقارنة
|
||||||
|
function updateComparisonUI() {
|
||||||
|
const compareBtn = document.getElementById('compare-btn');
|
||||||
|
const comparisonList = document.getElementById('comparison-list');
|
||||||
|
|
||||||
|
compareBtn.disabled = selectedProducts.length < 2;
|
||||||
|
|
||||||
|
// تحديث قائمة المنتجات المحددة للمقارنة
|
||||||
|
if (selectedProducts.length > 0) {
|
||||||
|
fetchProductsDetails(selectedProducts).then(products => {
|
||||||
|
comparisonList.innerHTML = products.map(product => `
|
||||||
|
<div class="comparison-item">
|
||||||
|
<img src="${product.image}" alt="${product.name}">
|
||||||
|
<span>${product.name}</span>
|
||||||
|
<button onclick="toggleCompare(${product.id})">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
comparisonList.innerHTML = '<p>اختر منتجات للمقارنة</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة مساعدة للحد من تكرار الطلبات
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// عرض رسائل الخطأ
|
||||||
|
function showError(message) {
|
||||||
|
const resultsContainer = document.getElementById('search-results');
|
||||||
|
resultsContainer.innerHTML = `<div class="error-message">${message}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// جلب تفاصيل المنتجات للمقارنة
|
||||||
|
async function fetchProductsDetails(productIds) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/products.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids: productIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل جلب تفاصيل المنتجات');
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching product details:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const searchInput = document.querySelector('.search-input');
|
||||||
|
const searchResults = document.querySelector('.search-results');
|
||||||
|
const searchIcon = document.querySelector('.search-icon');
|
||||||
|
const voiceSearchBtn = document.querySelector('.voice-search-btn');
|
||||||
|
const categoryFilters = document.querySelectorAll('input[name="category"]');
|
||||||
|
const minPrice = document.getElementById('min-price');
|
||||||
|
const maxPrice = document.getElementById('max-price');
|
||||||
|
const sortBy = document.getElementById('sort-by');
|
||||||
|
|
||||||
|
let searchTimeout;
|
||||||
|
const debounceDelay = 300;
|
||||||
|
|
||||||
|
// تفعيل البحث عند الكتابة
|
||||||
|
searchInput.addEventListener('input', function() {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
if (this.value.length > 0) {
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
performSearch();
|
||||||
|
searchResults.classList.add('active');
|
||||||
|
}, debounceDelay);
|
||||||
|
} else {
|
||||||
|
searchResults.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// إخفاء نتائج البحث عند النقر خارجها
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (!searchResults.contains(e.target) && !searchInput.contains(e.target)) {
|
||||||
|
searchResults.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// تفعيل البحث الصوتي
|
||||||
|
voiceSearchBtn.addEventListener('click', function() {
|
||||||
|
if ('webkitSpeechRecognition' in window) {
|
||||||
|
const recognition = new webkitSpeechRecognition();
|
||||||
|
recognition.lang = 'ar-SA';
|
||||||
|
recognition.continuous = false;
|
||||||
|
recognition.interimResults = false;
|
||||||
|
|
||||||
|
recognition.onstart = function() {
|
||||||
|
voiceSearchBtn.classList.add('listening');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onresult = function(event) {
|
||||||
|
const transcript = event.results[0][0].transcript;
|
||||||
|
searchInput.value = transcript;
|
||||||
|
performSearch();
|
||||||
|
searchResults.classList.add('active');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onerror = function(event) {
|
||||||
|
console.error('Speech recognition error:', event.error);
|
||||||
|
voiceSearchBtn.classList.remove('listening');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.onend = function() {
|
||||||
|
voiceSearchBtn.classList.remove('listening');
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.start();
|
||||||
|
} else {
|
||||||
|
alert('عذراً، البحث الصوتي غير مدعوم في متصفحك');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// تحديث نتائج البحث عند تغيير الفلاتر
|
||||||
|
categoryFilters.forEach(filter => {
|
||||||
|
filter.addEventListener('change', performSearch);
|
||||||
|
});
|
||||||
|
minPrice.addEventListener('input', performSearch);
|
||||||
|
maxPrice.addEventListener('input', performSearch);
|
||||||
|
sortBy.addEventListener('change', performSearch);
|
||||||
|
|
||||||
|
// دالة البحث الرئيسية
|
||||||
|
function performSearch() {
|
||||||
|
const query = searchInput.value;
|
||||||
|
const selectedCategories = Array.from(categoryFilters)
|
||||||
|
.filter(checkbox => checkbox.checked)
|
||||||
|
.map(checkbox => checkbox.value);
|
||||||
|
const priceRange = {
|
||||||
|
min: minPrice.value ? parseInt(minPrice.value) : null,
|
||||||
|
max: maxPrice.value ? parseInt(maxPrice.value) : null
|
||||||
|
};
|
||||||
|
const sortValue = sortBy.value;
|
||||||
|
|
||||||
|
// محاكاة طلب API
|
||||||
|
fetchSearchResults(query, selectedCategories, priceRange, sortValue)
|
||||||
|
.then(displayResults)
|
||||||
|
.catch(error => console.error('Search error:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة جلب نتائج البحث من الخادم
|
||||||
|
async function fetchSearchResults(query, categories, priceRange, sort) {
|
||||||
|
// هنا يمكنك استبدال هذا بطلب API حقيقي
|
||||||
|
const mockResults = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'زيت اللافندر العطري',
|
||||||
|
category: 'زيوت عطرية',
|
||||||
|
price: 120,
|
||||||
|
image: 'images/products/lavender.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'زيت الجوجوبا',
|
||||||
|
category: 'زيوت ثابتة',
|
||||||
|
price: 85,
|
||||||
|
image: 'images/products/jojoba.jpg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => resolve(mockResults), 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة عرض نتائج البحث
|
||||||
|
function displayResults(results) {
|
||||||
|
const resultsContainer = document.querySelector('.search-results-list');
|
||||||
|
resultsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
resultsContainer.innerHTML = '<div class="no-results">لا توجد نتائج</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
const resultItem = document.createElement('div');
|
||||||
|
resultItem.className = 'result-item';
|
||||||
|
resultItem.innerHTML = `
|
||||||
|
<img src="${result.image}" alt="${result.title}" class="result-image">
|
||||||
|
<div class="result-info">
|
||||||
|
<div class="result-title">${result.title}</div>
|
||||||
|
<div class="result-category">${result.category}</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-price">${result.price} ر.س</div>
|
||||||
|
`;
|
||||||
|
resultItem.addEventListener('click', () => {
|
||||||
|
window.location.href = `/product/${result.id}`;
|
||||||
|
});
|
||||||
|
resultsContainer.appendChild(resultItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class ProductSearch {
|
||||||
|
constructor() {
|
||||||
|
this.searchInput = document.querySelector('.search-input');
|
||||||
|
this.searchResults = document.querySelector('.search-results');
|
||||||
|
this.filterForm = document.querySelector('.search-filters');
|
||||||
|
this.initializeSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSearch() {
|
||||||
|
// إضافة مستمعي الأحداث
|
||||||
|
this.searchInput?.addEventListener('input', debounce(() => this.handleSearch(), 300));
|
||||||
|
this.filterForm?.addEventListener('change', () => this.handleSearch());
|
||||||
|
|
||||||
|
// إضافة مستمع لإغلاق نتائج البحث عند النقر خارجها
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!e.target.closest('.search-container')) {
|
||||||
|
this.hideResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSearch() {
|
||||||
|
const query = this.searchInput?.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
this.hideResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// جمع معايير التصفية
|
||||||
|
const filters = this.getFilters();
|
||||||
|
|
||||||
|
// إجراء البحث
|
||||||
|
const response = await fetch('/api/search.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
...filters
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('فشل البحث');
|
||||||
|
|
||||||
|
const results = await response.json();
|
||||||
|
this.displayResults(results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
this.showError('حدث خطأ أثناء البحث');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilters() {
|
||||||
|
const filters = {
|
||||||
|
category: [],
|
||||||
|
priceRange: {
|
||||||
|
min: null,
|
||||||
|
max: null
|
||||||
|
},
|
||||||
|
sortBy: 'relevance'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.filterForm) {
|
||||||
|
// جمع الفئات المحددة
|
||||||
|
const categoryInputs = this.filterForm.querySelectorAll('input[name="category"]:checked');
|
||||||
|
filters.category = Array.from(categoryInputs).map(input => input.value);
|
||||||
|
|
||||||
|
// جمع نطاق السعر
|
||||||
|
const minPrice = this.filterForm.querySelector('#min-price')?.value;
|
||||||
|
const maxPrice = this.filterForm.querySelector('#max-price')?.value;
|
||||||
|
if (minPrice) filters.priceRange.min = parseFloat(minPrice);
|
||||||
|
if (maxPrice) filters.priceRange.max = parseFloat(maxPrice);
|
||||||
|
|
||||||
|
// جمع معيار الترتيب
|
||||||
|
filters.sortBy = this.filterForm.querySelector('#sort-by')?.value || 'relevance';
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(results) {
|
||||||
|
if (!this.searchResults) return;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="no-results">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<p>لم يتم العثور على نتائج</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="results-list">
|
||||||
|
${results.map(product => `
|
||||||
|
<a href="${product.url}" class="result-item">
|
||||||
|
<img src="${product.image}" alt="${product.name}">
|
||||||
|
<div class="result-info">
|
||||||
|
<h4>${this.highlightQuery(product.name)}</h4>
|
||||||
|
<p>${this.highlightQuery(product.description)}</p>
|
||||||
|
<span class="price">${product.price} ريال</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightQuery(text) {
|
||||||
|
if (!this.searchInput?.value.trim()) return text;
|
||||||
|
|
||||||
|
const query = this.searchInput.value.trim();
|
||||||
|
const regex = new RegExp(`(${query})`, 'gi');
|
||||||
|
return text.replace(regex, '<mark>$1</mark>');
|
||||||
|
}
|
||||||
|
|
||||||
|
showResults() {
|
||||||
|
this.searchResults?.classList.add('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideResults() {
|
||||||
|
this.searchResults?.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (!this.searchResults) return;
|
||||||
|
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="search-error">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.showResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة مساعدة للحد من تكرار استدعاء البحث
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة نظام البحث
|
||||||
|
const productSearch = new ProductSearch();
|
179
js/voice-search.js
Normal file
179
js/voice-search.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
class VoiceSearch {
|
||||||
|
constructor() {
|
||||||
|
this.recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
||||||
|
this.searchInput = document.querySelector('.search-input input');
|
||||||
|
this.voiceButton = document.querySelector('.voice-search-btn');
|
||||||
|
|
||||||
|
this.initializeVoiceSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeVoiceSearch() {
|
||||||
|
// إعداد خصائص التعرف على الصوت
|
||||||
|
this.recognition.lang = 'ar';
|
||||||
|
this.recognition.continuous = false;
|
||||||
|
this.recognition.interimResults = false;
|
||||||
|
|
||||||
|
// إضافة مستمعي الأحداث
|
||||||
|
this.voiceButton?.addEventListener('click', () => this.startVoiceSearch());
|
||||||
|
|
||||||
|
// معالجة نتائج التعرف على الصوت
|
||||||
|
this.recognition.onresult = (event) => {
|
||||||
|
const result = event.results[0][0].transcript;
|
||||||
|
if (this.searchInput) {
|
||||||
|
this.searchInput.value = result;
|
||||||
|
// تشغيل البحث تلقائياً
|
||||||
|
this.searchInput.form?.dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// معالجة الأخطاء
|
||||||
|
this.recognition.onerror = (event) => {
|
||||||
|
console.error('Voice recognition error:', event.error);
|
||||||
|
this.showNotification('حدث خطأ في التعرف على الصوت', 'error');
|
||||||
|
};
|
||||||
|
|
||||||
|
// عند انتهاء التسجيل
|
||||||
|
this.recognition.onend = () => {
|
||||||
|
this.voiceButton?.classList.remove('listening');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
startVoiceSearch() {
|
||||||
|
try {
|
||||||
|
this.recognition.start();
|
||||||
|
this.voiceButton?.classList.add('listening');
|
||||||
|
this.showNotification('جارٍ الاستماع... تحدث الآن');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Voice recognition error:', error);
|
||||||
|
this.showNotification('لا يمكن استخدام البحث الصوتي', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = 'info') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `voice-notification ${type}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// نظام مشاركة المنتجات
|
||||||
|
class ShareSystem {
|
||||||
|
constructor() {
|
||||||
|
this.initializeShare();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeShare() {
|
||||||
|
document.querySelectorAll('.share-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => this.shareProduct(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareProduct(e) {
|
||||||
|
const productCard = e.target.closest('.product-card');
|
||||||
|
const productData = {
|
||||||
|
title: productCard.querySelector('h3').textContent,
|
||||||
|
text: productCard.querySelector('p').textContent,
|
||||||
|
url: window.location.href
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (navigator.share) {
|
||||||
|
// استخدام Web Share API إذا كان متوفراً
|
||||||
|
await navigator.share(productData);
|
||||||
|
} else {
|
||||||
|
// عرض قائمة مشاركة مخصصة
|
||||||
|
this.showCustomShareMenu(e.target, productData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Share error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showCustomShareMenu(target, data) {
|
||||||
|
const shareMenu = document.createElement('div');
|
||||||
|
shareMenu.className = 'share-menu';
|
||||||
|
|
||||||
|
const platforms = [
|
||||||
|
{ name: 'Facebook', icon: 'fab fa-facebook', url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(data.url)}` },
|
||||||
|
{ name: 'Twitter', icon: 'fab fa-twitter', url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(data.title)}&url=${encodeURIComponent(data.url)}` },
|
||||||
|
{ name: 'WhatsApp', icon: 'fab fa-whatsapp', url: `https://wa.me/?text=${encodeURIComponent(data.title + ' ' + data.url)}` },
|
||||||
|
{ name: 'نسخ الرابط', icon: 'fas fa-link', action: () => this.copyToClipboard(data.url) }
|
||||||
|
];
|
||||||
|
|
||||||
|
shareMenu.innerHTML = platforms.map(platform => `
|
||||||
|
<button class="share-option" data-url="${platform.url || ''}" data-action="${platform.action ? 'copy' : 'share'}">
|
||||||
|
<i class="${platform.icon}"></i>
|
||||||
|
<span>${platform.name}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// إضافة مستمعي الأحداث للأزرار
|
||||||
|
shareMenu.querySelectorAll('.share-option').forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
if (button.dataset.action === 'copy') {
|
||||||
|
this.copyToClipboard(data.url);
|
||||||
|
} else {
|
||||||
|
window.open(button.dataset.url, '_blank');
|
||||||
|
}
|
||||||
|
shareMenu.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// إضافة القائمة للصفحة
|
||||||
|
document.body.appendChild(shareMenu);
|
||||||
|
|
||||||
|
// تحديد موقع القائمة
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
shareMenu.style.top = rect.bottom + window.scrollY + 'px';
|
||||||
|
shareMenu.style.left = rect.left + window.scrollX + 'px';
|
||||||
|
|
||||||
|
// إغلاق القائمة عند النقر خارجها
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!shareMenu.contains(e.target) && e.target !== target) {
|
||||||
|
shareMenu.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
this.showNotification('تم نسخ الرابط بنجاح');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Clipboard error:', error);
|
||||||
|
this.showNotification('فشل نسخ الرابط', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = 'success') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `share-notification ${type}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة الأنظمة
|
||||||
|
const voiceSearch = new VoiceSearch();
|
||||||
|
const shareSystem = new ShareSystem();
|
129
js/wishlist.js
Normal file
129
js/wishlist.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
class Wishlist {
|
||||||
|
constructor() {
|
||||||
|
this.wishlist = JSON.parse(localStorage.getItem('wishlist')) || [];
|
||||||
|
this.wishlistIcon = document.querySelector('.wishlist-icon');
|
||||||
|
this.wishlistCount = document.querySelector('.wishlist-count');
|
||||||
|
|
||||||
|
this.initializeWishlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeWishlist() {
|
||||||
|
// تحديث عداد المفضلة
|
||||||
|
this.updateWishlistCount();
|
||||||
|
|
||||||
|
// إضافة مستمعي الأحداث لأزرار المفضلة
|
||||||
|
document.querySelectorAll('.add-to-wishlist').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => this.toggleWishlist(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleWishlist(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const productCard = e.target.closest('.product-card');
|
||||||
|
const productId = productCard.dataset.id;
|
||||||
|
const productName = productCard.querySelector('h3').textContent;
|
||||||
|
const productPrice = parseFloat(productCard.querySelector('.product-price').textContent);
|
||||||
|
const productImage = productCard.querySelector('img').src;
|
||||||
|
|
||||||
|
const index = this.wishlist.findIndex(item => item.id === productId);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
// إضافة المنتج للمفضلة
|
||||||
|
this.wishlist.push({
|
||||||
|
id: productId,
|
||||||
|
name: productName,
|
||||||
|
price: productPrice,
|
||||||
|
image: productImage
|
||||||
|
});
|
||||||
|
this.showNotification('تمت إضافة المنتج إلى المفضلة');
|
||||||
|
e.target.classList.add('active');
|
||||||
|
} else {
|
||||||
|
// إزالة المنتج من المفضلة
|
||||||
|
this.wishlist.splice(index, 1);
|
||||||
|
this.showNotification('تمت إزالة المنتج من المفضلة');
|
||||||
|
e.target.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateWishlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWishlist() {
|
||||||
|
// تحديث Local Storage
|
||||||
|
localStorage.setItem('wishlist', JSON.stringify(this.wishlist));
|
||||||
|
|
||||||
|
// تحديث عداد المفضلة
|
||||||
|
this.updateWishlistCount();
|
||||||
|
|
||||||
|
// تحديث عرض المفضلة في الصفحة
|
||||||
|
this.updateWishlistDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWishlistCount() {
|
||||||
|
if (this.wishlistCount) {
|
||||||
|
this.wishlistCount.textContent = this.wishlist.length;
|
||||||
|
this.wishlistCount.style.display = this.wishlist.length > 0 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWishlistDisplay() {
|
||||||
|
const wishlistContainer = document.querySelector('.wishlist-items');
|
||||||
|
if (!wishlistContainer) return;
|
||||||
|
|
||||||
|
if (this.wishlist.length === 0) {
|
||||||
|
wishlistContainer.innerHTML = '<p class="empty-wishlist">لا توجد منتجات في المفضلة</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wishlistContainer.innerHTML = this.wishlist.map(item => `
|
||||||
|
<div class="wishlist-item" data-id="${item.id}">
|
||||||
|
<img src="${item.image}" alt="${item.name}">
|
||||||
|
<div class="item-details">
|
||||||
|
<h4>${item.name}</h4>
|
||||||
|
<div class="item-price">${item.price} جنيه</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button class="add-to-cart" onclick="cart.addToCart(event)">
|
||||||
|
<i class="fas fa-shopping-cart"></i>
|
||||||
|
</button>
|
||||||
|
<button class="remove-from-wishlist" onclick="wishlist.removeFromWishlist('${item.id}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromWishlist(productId) {
|
||||||
|
const index = this.wishlist.findIndex(item => item.id === productId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.wishlist.splice(index, 1);
|
||||||
|
this.updateWishlist();
|
||||||
|
this.showNotification('تمت إزالة المنتج من المفضلة');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'wishlist-notification';
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من وجود منتج في المفضلة
|
||||||
|
isInWishlist(productId) {
|
||||||
|
return this.wishlist.some(item => item.id === productId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تهيئة نظام المفضلة
|
||||||
|
const wishlist = new Wishlist();
|
124
login.php
Normal file
124
login.php
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
require_once 'includes/Security.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// إذا كان المستخدم مسجل دخوله بالفعل، قم بتحويله إلى الصفحة الرئيسية
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
$email = Security::sanitizeInput($_POST['email']);
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
if (empty($email) || empty($password)) {
|
||||||
|
throw new Exception('جميع الحقول مطلوبة');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception('فشل الاتصال بقاعدة البيانات');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("SELECT id, username, password, role FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
throw new Exception('البريد الإلكتروني أو كلمة المرور غير صحيحة');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
|
||||||
|
if (!Security::verifyPassword($password, $user['password'])) {
|
||||||
|
throw new Exception('البريد الإلكتروني أو كلمة المرور غير صحيحة');
|
||||||
|
}
|
||||||
|
|
||||||
|
// تسجيل الدخول بنجاح
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
$_SESSION['role'] = $user['role'];
|
||||||
|
|
||||||
|
// تحديث وقت آخر تسجيل دخول
|
||||||
|
$stmt = $conn->prepare("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $user['id']);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>تسجيل الدخول - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4">تسجيل الدخول</h2>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo $error; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?php echo $success; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">البريد الإلكتروني</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">كلمة المرور</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||||
|
<label class="form-check-label" for="remember">تذكرني</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">تسجيل الدخول</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p>ليس لديك حساب؟ <a href="register.php">إنشاء حساب جديد</a></p>
|
||||||
|
<p><a href="forgot-password.php">نسيت كلمة المرور؟</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
logout.php
Normal file
17
logout.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// حذف جميع متغيرات الجلسة
|
||||||
|
$_SESSION = array();
|
||||||
|
|
||||||
|
// حذف ملف الجلسة
|
||||||
|
if (isset($_COOKIE[session_name()])) {
|
||||||
|
setcookie(session_name(), '', time() - 3600, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// إنهاء الجلسة
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
// إعادة التوجيه إلى صفحة تسجيل الدخول
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
169
products/essential-oils.html
Normal file
169
products/essential-oils.html
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>الزيوت الأساسية - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="../css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="../index.html">
|
||||||
|
<img src="../images/logo.jpg" alt="ShubraVeil" class="logo-img">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul class="nav-links">
|
||||||
|
<li><a href="../index.html">الرئيسية</a></li>
|
||||||
|
<li><a href="../index.html#about">من نحن</a></li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle">منتجاتنا <i class="fas fa-chevron-down"></i></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="essential-oils.html" class="active">الزيوت الأساسية</a></li>
|
||||||
|
<li><a href="fixed-oils.html">الزيوت الثابتة</a></li>
|
||||||
|
<li><a href="hydrosols.html">الهيدروسولات العطرية</a></li>
|
||||||
|
<li><a href="natural-cosmetics.html">مستحضرات تجميل طبيعية</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a href="../index.html#benefits">فوائد الزيوت</a></li>
|
||||||
|
<li><a href="../index.html#contact">تواصل معنا</a></li>
|
||||||
|
</ul>
|
||||||
|
<div class="mobile-menu">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="product-category-hero">
|
||||||
|
<div class="container">
|
||||||
|
<h1>الزيوت الأساسية</h1>
|
||||||
|
<p>مجموعة متميزة من الزيوت الأساسية النقية 100% مستخلصة بعناية من أجود النباتات العطرية</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="products-grid-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="products-filter">
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" placeholder="ابحث عن منتج...">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</div>
|
||||||
|
<div class="sort-options">
|
||||||
|
<select>
|
||||||
|
<option value="newest">الأحدث</option>
|
||||||
|
<option value="price-low">السعر: من الأقل للأعلى</option>
|
||||||
|
<option value="price-high">السعر: من الأعلى للأقل</option>
|
||||||
|
<option value="name">الاسم: أ-ي</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="products-grid" id="products-container">
|
||||||
|
<!-- سيتم تحميل المنتجات هنا عبر JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>تواصل معنا</h3>
|
||||||
|
<p><i class="fas fa-phone"></i> +20 123 456 789</p>
|
||||||
|
<p><i class="fas fa-envelope"></i> info@shubraveil.com</p>
|
||||||
|
<div class="social-links">
|
||||||
|
<a href="#"><i class="fab fa-facebook"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||||
|
<a href="#"><i class="fab fa-whatsapp"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<h3>روابط سريعة</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../index.html#about">من نحن</a></li>
|
||||||
|
<li><a href="../index.html#products">منتجاتنا</a></li>
|
||||||
|
<li><a href="../index.html#benefits">فوائد الزيوت</a></li>
|
||||||
|
<li><a href="../index.html#contact">تواصل معنا</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2024 ShubraVeil. جميع الحقوق محفوظة</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// تحميل المنتجات من قاعدة البيانات
|
||||||
|
fetch('../api/products.php?type=essential_oils')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(products => {
|
||||||
|
const container = document.getElementById('products-container');
|
||||||
|
products.forEach(product => {
|
||||||
|
container.innerHTML += `
|
||||||
|
<div class="product-card">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="../${product.image}" alt="${product.name}">
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3>${product.name}</h3>
|
||||||
|
<p>${product.description}</p>
|
||||||
|
<div class="product-price">${product.price} جنيه</div>
|
||||||
|
<button class="btn primary">اطلب الآن</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// البحث في المنتجات
|
||||||
|
document.querySelector('.search-box input').addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const products = document.querySelectorAll('.product-card');
|
||||||
|
products.forEach(product => {
|
||||||
|
const name = product.querySelector('h3').textContent.toLowerCase();
|
||||||
|
const description = product.querySelector('p').textContent.toLowerCase();
|
||||||
|
if (name.includes(searchTerm) || description.includes(searchTerm)) {
|
||||||
|
product.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
product.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ترتيب المنتجات
|
||||||
|
document.querySelector('.sort-options select').addEventListener('change', function(e) {
|
||||||
|
const products = Array.from(document.querySelectorAll('.product-card'));
|
||||||
|
const container = document.getElementById('products-container');
|
||||||
|
|
||||||
|
products.sort((a, b) => {
|
||||||
|
const valueA = a.querySelector('h3').textContent;
|
||||||
|
const valueB = b.querySelector('h3').textContent;
|
||||||
|
const priceA = parseFloat(a.querySelector('.product-price').textContent);
|
||||||
|
const priceB = parseFloat(b.querySelector('.product-price').textContent);
|
||||||
|
|
||||||
|
switch(e.target.value) {
|
||||||
|
case 'price-low':
|
||||||
|
return priceA - priceB;
|
||||||
|
case 'price-high':
|
||||||
|
return priceB - priceA;
|
||||||
|
case 'name':
|
||||||
|
return valueA.localeCompare(valueB);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = '';
|
||||||
|
products.forEach(product => container.appendChild(product));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
products/fixed-oils.html
Normal file
37
products/fixed-oils.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<!-- نفس محتوى صفحة essential-oils.html مع تغيير العنوان والوصف -->
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>الزيوت الثابتة - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="../css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- نفس الهيدر مع تغيير الرابط النشط -->
|
||||||
|
<header>
|
||||||
|
<!-- ... (نفس محتوى الهيدر) ... -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="product-category-hero">
|
||||||
|
<div class="container">
|
||||||
|
<h1>الزيوت الثابتة</h1>
|
||||||
|
<p>مجموعة مختارة من أجود الزيوت الثابتة الطبيعية المستخلصة من البذور والمكسرات</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- نفس باقي المحتوى مع تغيير نوع المنتج في API call -->
|
||||||
|
<script>
|
||||||
|
fetch('../api/products.php?type=fixed_oils')
|
||||||
|
// ... باقي الكود
|
||||||
|
</script>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<!-- ... (نفس محتوى الفوتر) ... -->
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
198
register.php
Normal file
198
register.php
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
require_once 'includes/Security.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// إذا كان المستخدم مسجل دخوله بالفعل، قم بتحويله إلى الصفحة الرئيسية
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
$username = Security::sanitizeInput($_POST['username']);
|
||||||
|
$email = Security::sanitizeInput($_POST['email']);
|
||||||
|
$password = $_POST['password'];
|
||||||
|
$confirm_password = $_POST['confirm_password'];
|
||||||
|
|
||||||
|
// التحقق من البيانات
|
||||||
|
if (empty($username) || empty($email) || empty($password) || empty($confirm_password)) {
|
||||||
|
throw new Exception('جميع الحقول مطلوبة');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception('البريد الإلكتروني غير صالح');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
throw new Exception('يجب أن تكون كلمة المرور 8 أحرف على الأقل');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== $confirm_password) {
|
||||||
|
throw new Exception('كلمتا المرور غير متطابقتين');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception('فشل الاتصال بقاعدة البيانات');
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من عدم وجود البريد الإلكتروني أو اسم المستخدم مسبقاً
|
||||||
|
$stmt = $conn->prepare("SELECT id FROM users WHERE email = ? OR username = ?");
|
||||||
|
$stmt->bind_param("ss", $email, $username);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
throw new Exception('البريد الإلكتروني أو اسم المستخدم مستخدم بالفعل');
|
||||||
|
}
|
||||||
|
|
||||||
|
// تشفير كلمة المرور
|
||||||
|
$hashed_password = Security::hashPassword($password);
|
||||||
|
|
||||||
|
// إنشاء المستخدم
|
||||||
|
$stmt = $conn->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("sss", $username, $email, $hashed_password);
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('حدث خطأ أثناء إنشاء الحساب');
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = 'تم إنشاء الحساب بنجاح! يمكنك الآن تسجيل الدخول.';
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>إنشاء حساب جديد - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4">إنشاء حساب جديد</h2>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo $error; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<?php echo $success; ?>
|
||||||
|
<br>
|
||||||
|
<a href="login.php" class="alert-link">اضغط هنا لتسجيل الدخول</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">اسم المستخدم</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username" required
|
||||||
|
pattern="[A-Za-z0-9_]{3,20}" title="يجب أن يحتوي على أحرف وأرقام وشرطة سفلية فقط (3-20 حرف)">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
يرجى اختيار اسم مستخدم صحيح
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="email" class="form-label">البريد الإلكتروني</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
يرجى إدخال بريد إلكتروني صحيح
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">كلمة المرور</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required
|
||||||
|
minlength="8" title="يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">تأكيد كلمة المرور</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
كلمتا المرور غير متطابقتين
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="terms" required>
|
||||||
|
<label class="form-check-label" for="terms">
|
||||||
|
أوافق على <a href="terms.php">الشروط والأحكام</a>
|
||||||
|
</label>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
يجب الموافقة على الشروط والأحكام
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">إنشاء حساب</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<p>لديك حساب بالفعل؟ <a href="login.php">تسجيل الدخول</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من تطابق كلمتي المرور
|
||||||
|
var password = document.getElementById('password')
|
||||||
|
var confirm_password = document.getElementById('confirm_password')
|
||||||
|
|
||||||
|
if (password.value !== confirm_password.value) {
|
||||||
|
confirm_password.setCustomValidity('كلمتا المرور غير متطابقتين')
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
confirm_password.setCustomValidity('')
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
166
reset-password.php
Normal file
166
reset-password.php
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
require_once 'includes/Security.php';
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$error = '';
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
// التحقق من وجود الرمز
|
||||||
|
if (!isset($_GET['token'])) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = Security::sanitizeInput($_GET['token']);
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
$password = $_POST['password'];
|
||||||
|
$confirm_password = $_POST['confirm_password'];
|
||||||
|
|
||||||
|
if (empty($password) || empty($confirm_password)) {
|
||||||
|
throw new Exception('جميع الحقول مطلوبة');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
throw new Exception('يجب أن تكون كلمة المرور 8 أحرف على الأقل');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($password !== $confirm_password) {
|
||||||
|
throw new Exception('كلمتا المرور غير متطابقتين');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception('فشل الاتصال بقاعدة البيانات');
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من صلاحية الرمز
|
||||||
|
$stmt = $conn->prepare("SELECT user_id FROM password_resets WHERE token = ? AND expiry > NOW() AND used = 0");
|
||||||
|
$stmt->bind_param("s", $token);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 0) {
|
||||||
|
throw new Exception('رابط إعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية');
|
||||||
|
}
|
||||||
|
|
||||||
|
$reset = $result->fetch_assoc();
|
||||||
|
|
||||||
|
// تحديث كلمة المرور
|
||||||
|
$hashed_password = Security::hashPassword($password);
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE users SET password = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("si", $hashed_password, $reset['user_id']);
|
||||||
|
|
||||||
|
if (!$stmt->execute()) {
|
||||||
|
throw new Exception('حدث خطأ أثناء تحديث كلمة المرور');
|
||||||
|
}
|
||||||
|
|
||||||
|
// تعليم الرمز كمستخدم
|
||||||
|
$stmt = $conn->prepare("UPDATE password_resets SET used = 1 WHERE token = ?");
|
||||||
|
$stmt->bind_param("s", $token);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$success = 'تم تحديث كلمة المرور بنجاح! يمكنك الآن تسجيل الدخول باستخدام كلمة المرور الجديدة.';
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$error = $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>إعادة تعيين كلمة المرور - ShubraVeil</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.rtl.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center mt-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="text-center mb-4">إعادة تعيين كلمة المرور</h2>
|
||||||
|
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-danger"><?php echo $error; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<?php echo $success; ?>
|
||||||
|
<br>
|
||||||
|
<a href="login.php" class="alert-link">اضغط هنا لتسجيل الدخول</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="POST" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'] . '?token=' . $token); ?>" class="needs-validation" novalidate>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">كلمة المرور الجديدة</label>
|
||||||
|
<input type="password" class="form-control" id="password" name="password" required
|
||||||
|
minlength="8" title="يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل">
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">تأكيد كلمة المرور</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
|
<div class="invalid-feedback">
|
||||||
|
كلمتا المرور غير متطابقتين
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">تحديث كلمة المرور</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من تطابق كلمتي المرور
|
||||||
|
var password = document.getElementById('password')
|
||||||
|
var confirm_password = document.getElementById('confirm_password')
|
||||||
|
|
||||||
|
if (password.value !== confirm_password.value) {
|
||||||
|
confirm_password.setCustomValidity('كلمتا المرور غير متطابقتين')
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
} else {
|
||||||
|
confirm_password.setCustomValidity('')
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add('was-validated')
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
robots.txt
Normal file
14
robots.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Allow: /products/
|
||||||
|
Allow: /blog.html
|
||||||
|
Allow: /faq.html
|
||||||
|
Allow: /search.html
|
||||||
|
|
||||||
|
Disallow: /admin/
|
||||||
|
Disallow: /includes/
|
||||||
|
Disallow: /api/
|
||||||
|
Disallow: /backups/
|
||||||
|
Disallow: /cache/
|
||||||
|
|
||||||
|
Sitemap: https://shubraveil.com/sitemap.xml
|
88
search.html
Normal file
88
search.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>بحث المنتجات - ShubraVeil</title>
|
||||||
|
<meta name="description" content="ابحث عن الزيوت العطرية والطبيعية في موقع ShubraVeil">
|
||||||
|
<meta name="keywords" content="زيوت عطرية, زيوت طبيعية, بحث, شبرا بلولة">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<!-- نفس الهيدر السابق -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="search-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="section-title">بحث المنتجات</h1>
|
||||||
|
|
||||||
|
<div class="search-container">
|
||||||
|
<form id="advanced-search" class="search-form">
|
||||||
|
<div class="search-row">
|
||||||
|
<div class="search-input">
|
||||||
|
<input type="text" id="search-query" placeholder="ابحث عن المنتجات...">
|
||||||
|
<button type="submit" class="search-btn">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>نوع المنتج</label>
|
||||||
|
<select id="product-type">
|
||||||
|
<option value="">الكل</option>
|
||||||
|
<option value="essential">زيوت عطرية</option>
|
||||||
|
<option value="fixed">زيوت ثابتة</option>
|
||||||
|
<option value="hydrosol">مياه عطرية</option>
|
||||||
|
<option value="natural">مستحضرات طبيعية</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>السعر</label>
|
||||||
|
<div class="price-range">
|
||||||
|
<input type="number" id="min-price" placeholder="من">
|
||||||
|
<span>-</span>
|
||||||
|
<input type="number" id="max-price" placeholder="إلى">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>الترتيب حسب</label>
|
||||||
|
<select id="sort-by">
|
||||||
|
<option value="relevance">الأكثر صلة</option>
|
||||||
|
<option value="price-asc">السعر: من الأقل للأعلى</option>
|
||||||
|
<option value="price-desc">السعر: من الأعلى للأقل</option>
|
||||||
|
<option value="name-asc">الاسم: أ-ي</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="search-results" id="search-results">
|
||||||
|
<!-- سيتم ملء النتائج ديناميكياً -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comparison-tool">
|
||||||
|
<h3>مقارنة المنتجات</h3>
|
||||||
|
<div id="comparison-list" class="comparison-list">
|
||||||
|
<!-- سيتم إضافة المنتجات للمقارنة هنا -->
|
||||||
|
</div>
|
||||||
|
<button id="compare-btn" class="btn" disabled>قارن المنتجات</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<!-- نفس الفوتر السابق -->
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="js/search.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
33
sitemap.xml
Normal file
33
sitemap.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://shubraveil.com/</loc>
|
||||||
|
<lastmod>2024-12-09</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://shubraveil.com/products</loc>
|
||||||
|
<lastmod>2024-12-09</lastmod>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://shubraveil.com/blog.html</loc>
|
||||||
|
<lastmod>2024-12-09</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://shubraveil.com/faq.html</loc>
|
||||||
|
<lastmod>2024-12-09</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.7</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://shubraveil.com/search.html</loc>
|
||||||
|
<lastmod>2024-12-09</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
40
templates/emails/order-confirmation.html
Normal file
40
templates/emails/order-confirmation.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>تأكيد الطلب - ShubraVeil</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Tajawal', Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { text-align: center; padding: 20px 0; }
|
||||||
|
.content { background: #f9f9f9; padding: 20px; border-radius: 5px; }
|
||||||
|
.footer { text-align: center; padding: 20px 0; color: #666; }
|
||||||
|
.button { display: inline-block; padding: 10px 20px; background: #0c814a; color: white; text-decoration: none; border-radius: 5px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>شكراً لطلبك من ShubraVeil</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<h2>مرحباً {USER_NAME}،</h2>
|
||||||
|
<p>نشكرك على طلبك. تم استلام طلبك بنجاح وجاري تجهيزه.</p>
|
||||||
|
|
||||||
|
<h3>تفاصيل الطلب:</h3>
|
||||||
|
<p>رقم الطلب: {ORDER_ID}</p>
|
||||||
|
<p>إجمالي المبلغ: {ORDER_TOTAL} جنيه</p>
|
||||||
|
|
||||||
|
<p>يمكنك متابعة حالة طلبك من خلال حسابك على موقعنا.</p>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<a href="{SITE_URL}/orders" class="button">متابعة طلبك</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>إذا كان لديك أي استفسار، يرجى التواصل معنا على البريد الإلكتروني: support@shubraveil.com</p>
|
||||||
|
<p>مع تحيات فريق ShubraVeil</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
74
test_backup.php
Normal file
74
test_backup.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام النسخ الاحتياطي ===\n\n";
|
||||||
|
|
||||||
|
class DatabaseBackup {
|
||||||
|
private $db_host;
|
||||||
|
private $db_user;
|
||||||
|
private $db_pass;
|
||||||
|
private $db_name;
|
||||||
|
private $backup_dir;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db_host = DB_SERVER;
|
||||||
|
$this->db_user = DB_USERNAME;
|
||||||
|
$this->db_pass = DB_PASSWORD;
|
||||||
|
$this->db_name = DB_NAME;
|
||||||
|
$this->backup_dir = __DIR__ . '/backups';
|
||||||
|
|
||||||
|
if (!is_dir($this->backup_dir)) {
|
||||||
|
mkdir($this->backup_dir, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createBackup() {
|
||||||
|
$date = date('Y-m-d_H-i-s');
|
||||||
|
$backup_file = $this->backup_dir . "/backup_" . $date . ".sql";
|
||||||
|
|
||||||
|
$command = sprintf(
|
||||||
|
'mysqldump --host=%s --user=%s --password=%s %s > %s',
|
||||||
|
escapeshellarg($this->db_host),
|
||||||
|
escapeshellarg($this->db_user),
|
||||||
|
escapeshellarg($this->db_pass),
|
||||||
|
escapeshellarg($this->db_name),
|
||||||
|
escapeshellarg($backup_file)
|
||||||
|
);
|
||||||
|
|
||||||
|
system($command, $return_var);
|
||||||
|
|
||||||
|
if ($return_var === 0) {
|
||||||
|
echo "تم إنشاء النسخة الاحتياطية بنجاح: " . basename($backup_file) . "\n";
|
||||||
|
return $backup_file;
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل إنشاء النسخة الاحتياطية");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listBackups() {
|
||||||
|
$backups = glob($this->backup_dir . "/*.sql");
|
||||||
|
echo "\nالنسخ الاحتياطية المتوفرة:\n";
|
||||||
|
foreach ($backups as $backup) {
|
||||||
|
echo "- " . basename($backup) . " (" . $this->formatSize(filesize($backup)) . ")\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatSize($size) {
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$power = $size > 0 ? floor(log($size, 1024)) : 0;
|
||||||
|
return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $units[$power];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$backup = new DatabaseBackup();
|
||||||
|
|
||||||
|
// إنشاء نسخة احتياطية
|
||||||
|
$backup_file = $backup->createBackup();
|
||||||
|
|
||||||
|
// عرض قائمة النسخ الاحتياطية
|
||||||
|
$backup->listBackups();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage() . "\n";
|
||||||
|
}
|
37
test_connection.php
Normal file
37
test_connection.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("فشل الاتصال: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "تم الاتصال بقاعدة البيانات بنجاح!\n\n";
|
||||||
|
|
||||||
|
// اختبار استعلام لعرض الجداول
|
||||||
|
$result = $conn->query("SHOW TABLES");
|
||||||
|
if ($result) {
|
||||||
|
echo "الجداول الموجودة في قاعدة البيانات:\n";
|
||||||
|
while ($row = $result->fetch_array()) {
|
||||||
|
echo "- " . $row[0] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// اختبار إنشاء مستخدم تجريبي
|
||||||
|
$username = "test_user";
|
||||||
|
$email = "test@example.com";
|
||||||
|
$password = password_hash("test123", PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT IGNORE INTO users (username, email, password) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("sss", $username, $email, $password);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "\nتم إنشاء مستخدم تجريبي بنجاح أو كان موجوداً مسبقاً\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage();
|
||||||
|
}
|
25
test_db.php
Normal file
25
test_db.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("Connection failed: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Database connection successful!\n";
|
||||||
|
|
||||||
|
// Test query
|
||||||
|
$result = $conn->query("SHOW TABLES");
|
||||||
|
if ($result) {
|
||||||
|
echo "\nTables in database:\n";
|
||||||
|
while ($row = $result->fetch_array()) {
|
||||||
|
echo "- " . $row[0] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage();
|
||||||
|
}
|
104
test_images.php
Normal file
104
test_images.php
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام معالجة الصور ===\n\n";
|
||||||
|
|
||||||
|
class ImageProcessor {
|
||||||
|
private $upload_dir;
|
||||||
|
private $cache_dir;
|
||||||
|
private $allowed_types;
|
||||||
|
private $max_size;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->upload_dir = __DIR__ . '/uploads/images';
|
||||||
|
$this->cache_dir = __DIR__ . '/cache/images';
|
||||||
|
$this->allowed_types = ['image/jpeg', 'image/png', 'image/webp'];
|
||||||
|
$this->max_size = 5 * 1024 * 1024; // 5MB
|
||||||
|
|
||||||
|
// إنشاء المجلدات إذا لم تكن موجودة
|
||||||
|
foreach ([$this->upload_dir, $this->cache_dir] as $dir) {
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processTestImage() {
|
||||||
|
// إنشاء صورة اختبار
|
||||||
|
$width = 800;
|
||||||
|
$height = 600;
|
||||||
|
$image = imagecreatetruecolor($width, $height);
|
||||||
|
|
||||||
|
// تلوين الصورة
|
||||||
|
$bg_color = imagecolorallocate($image, 240, 240, 240);
|
||||||
|
$text_color = imagecolorallocate($image, 50, 50, 50);
|
||||||
|
|
||||||
|
imagefill($image, 0, 0, $bg_color);
|
||||||
|
|
||||||
|
// إضافة نص
|
||||||
|
$text = "ShubraVeil Test Image";
|
||||||
|
$font_size = 5;
|
||||||
|
|
||||||
|
// حساب موقع النص
|
||||||
|
$text_width = imagefontwidth($font_size) * strlen($text);
|
||||||
|
$text_height = imagefontheight($font_size);
|
||||||
|
|
||||||
|
$x = ($width - $text_width) / 2;
|
||||||
|
$y = ($height - $text_height) / 2;
|
||||||
|
|
||||||
|
imagestring($image, $font_size, $x, $y, $text, $text_color);
|
||||||
|
|
||||||
|
// حفظ الصورة الأصلية
|
||||||
|
$original_file = $this->upload_dir . '/test_image.png';
|
||||||
|
imagepng($image, $original_file);
|
||||||
|
|
||||||
|
echo "تم إنشاء صورة الاختبار: " . basename($original_file) . "\n";
|
||||||
|
echo "الحجم: " . $this->formatSize(filesize($original_file)) . "\n\n";
|
||||||
|
|
||||||
|
// إنشاء نسخة مصغرة
|
||||||
|
$thumb_width = 150;
|
||||||
|
$thumb_height = 150;
|
||||||
|
$thumbnail = imagecreatetruecolor($thumb_width, $thumb_height);
|
||||||
|
|
||||||
|
imagecopyresampled(
|
||||||
|
$thumbnail, $image,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
$thumb_width, $thumb_height,
|
||||||
|
$width, $height
|
||||||
|
);
|
||||||
|
|
||||||
|
$thumb_file = $this->cache_dir . '/thumb_test_image.png';
|
||||||
|
imagepng($thumbnail, $thumb_file);
|
||||||
|
|
||||||
|
echo "تم إنشاء النسخة المصغرة: " . basename($thumb_file) . "\n";
|
||||||
|
echo "الحجم: " . $this->formatSize(filesize($thumb_file)) . "\n";
|
||||||
|
|
||||||
|
// تنظيف الذاكرة
|
||||||
|
imagedestroy($image);
|
||||||
|
imagedestroy($thumbnail);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'original' => $original_file,
|
||||||
|
'thumbnail' => $thumb_file
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatSize($size) {
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
$power = $size > 0 ? floor(log($size, 1024)) : 0;
|
||||||
|
return number_format($size / pow(1024, $power), 2, '.', ',') . ' ' . $units[$power];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$processor = new ImageProcessor();
|
||||||
|
$files = $processor->processTestImage();
|
||||||
|
|
||||||
|
echo "\nاختبار اكتمل بنجاح!\n";
|
||||||
|
echo "الملفات التي تم إنشاؤها:\n";
|
||||||
|
echo "- الصورة الأصلية: " . $files['original'] . "\n";
|
||||||
|
echo "- النسخة المصغرة: " . $files['thumbnail'] . "\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage() . "\n";
|
||||||
|
}
|
105
test_orders.php
Normal file
105
test_orders.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
require_once __DIR__ . '/includes/Security.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام إدارة الطلبات ===\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("فشل الاتصال: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// إنشاء مستخدم للاختبار
|
||||||
|
$stmt = $conn->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
|
||||||
|
$username = "test_user_" . time(); // اسم مستخدم فريد
|
||||||
|
$email = "test_" . time() . "@example.com";
|
||||||
|
$password = Security::hashPassword("test123");
|
||||||
|
|
||||||
|
$stmt->bind_param("sss", $username, $email, $password);
|
||||||
|
$stmt->execute();
|
||||||
|
$user_id = $conn->insert_id;
|
||||||
|
|
||||||
|
echo "تم إنشاء مستخدم للاختبار (ID: $user_id)\n";
|
||||||
|
|
||||||
|
// إنشاء منتج للاختبار
|
||||||
|
$stmt = $conn->prepare("INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)");
|
||||||
|
$name = "زيت الورد الطبيعي";
|
||||||
|
$description = "زيت ورد طبيعي 100% للعناية بالبشرة";
|
||||||
|
$price = 199.99;
|
||||||
|
$stock = 30;
|
||||||
|
|
||||||
|
$stmt->bind_param("ssdi", $name, $description, $price, $stock);
|
||||||
|
$stmt->execute();
|
||||||
|
$product_id = $conn->insert_id;
|
||||||
|
|
||||||
|
echo "تم إنشاء منتج للاختبار (ID: $product_id)\n";
|
||||||
|
|
||||||
|
// إنشاء طلب جديد
|
||||||
|
$stmt = $conn->prepare("INSERT INTO orders (user_id, total_amount, status) VALUES (?, ?, ?)");
|
||||||
|
$total = 199.99;
|
||||||
|
$status = 'pending';
|
||||||
|
|
||||||
|
$stmt->bind_param("ids", $user_id, $total, $status);
|
||||||
|
$stmt->execute();
|
||||||
|
$order_id = $conn->insert_id;
|
||||||
|
|
||||||
|
echo "تم إنشاء طلب جديد (ID: $order_id)\n";
|
||||||
|
|
||||||
|
// إضافة عناصر الطلب
|
||||||
|
$stmt = $conn->prepare("INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)");
|
||||||
|
$quantity = 1;
|
||||||
|
|
||||||
|
$stmt->bind_param("iiid", $order_id, $product_id, $quantity, $price);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo "تم إضافة عناصر الطلب\n";
|
||||||
|
|
||||||
|
// استرجاع معلومات الطلب
|
||||||
|
$query = "
|
||||||
|
SELECT o.*, oi.quantity, oi.price as item_price, p.name as product_name, u.username
|
||||||
|
FROM orders o
|
||||||
|
JOIN order_items oi ON o.id = oi.order_id
|
||||||
|
JOIN products p ON oi.product_id = p.id
|
||||||
|
JOIN users u ON o.user_id = u.id
|
||||||
|
WHERE o.id = ?
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $conn->prepare($query);
|
||||||
|
$stmt->bind_param("i", $order_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$order = $result->fetch_assoc();
|
||||||
|
|
||||||
|
echo "\nتفاصيل الطلب:\n";
|
||||||
|
echo "رقم الطلب: " . $order['id'] . "\n";
|
||||||
|
echo "المستخدم: " . $order['username'] . "\n";
|
||||||
|
echo "المنتج: " . $order['product_name'] . "\n";
|
||||||
|
echo "الكمية: " . $order['quantity'] . "\n";
|
||||||
|
echo "السعر: " . $order['item_price'] . " جنيه\n";
|
||||||
|
echo "الإجمالي: " . $order['total_amount'] . " جنيه\n";
|
||||||
|
echo "الحالة: " . $order['status'] . "\n";
|
||||||
|
|
||||||
|
// تحديث حالة الطلب
|
||||||
|
$stmt = $conn->prepare("UPDATE orders SET status = ? WHERE id = ?");
|
||||||
|
$new_status = 'delivered';
|
||||||
|
|
||||||
|
$stmt->bind_param("si", $new_status, $order_id);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo "\nتم تحديث حالة الطلب إلى: delivered\n";
|
||||||
|
|
||||||
|
// تنظيف البيانات
|
||||||
|
$conn->query("DELETE FROM order_items WHERE order_id = $order_id");
|
||||||
|
$conn->query("DELETE FROM orders WHERE id = $order_id");
|
||||||
|
$conn->query("DELETE FROM products WHERE id = $product_id");
|
||||||
|
$conn->query("DELETE FROM users WHERE id = $user_id");
|
||||||
|
|
||||||
|
echo "\nتم تنظيف بيانات الاختبار بنجاح\n";
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage() . "\n";
|
||||||
|
}
|
84
test_products.php
Normal file
84
test_products.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
require_once __DIR__ . '/includes/Security.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام إدارة المنتجات ===\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("فشل الاتصال: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// إضافة منتج جديد
|
||||||
|
$stmt = $conn->prepare("INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$name = "زيت اللافندر الطبيعي";
|
||||||
|
$description = "زيت لافندر طبيعي 100% للعناية بالبشرة والشعر";
|
||||||
|
$price = 149.99;
|
||||||
|
$stock = 50;
|
||||||
|
$category = "essential_oils";
|
||||||
|
|
||||||
|
$stmt->bind_param("ssdis", $name, $description, $price, $stock, $category);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$product_id = $conn->insert_id;
|
||||||
|
echo "تم إضافة المنتج بنجاح (ID: $product_id)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// استرجاع المنتج
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM products WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $product_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$product = $result->fetch_assoc();
|
||||||
|
|
||||||
|
echo "\nبيانات المنتج:\n";
|
||||||
|
echo "الاسم: " . $product['name'] . "\n";
|
||||||
|
echo "السعر: " . $product['price'] . " جنيه\n";
|
||||||
|
echo "المخزون: " . $product['stock'] . " قطعة\n";
|
||||||
|
|
||||||
|
// تحديث المنتج
|
||||||
|
$new_price = 139.99;
|
||||||
|
$new_stock = 45;
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("UPDATE products SET price = ?, stock = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("dii", $new_price, $new_stock, $product_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "\nتم تحديث المنتج بنجاح\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من التحديث
|
||||||
|
$stmt = $conn->prepare("SELECT price, stock FROM products WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $product_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$updated_product = $result->fetch_assoc();
|
||||||
|
|
||||||
|
echo "\nبيانات المنتج بعد التحديث:\n";
|
||||||
|
echo "السعر الجديد: " . $updated_product['price'] . " جنيه\n";
|
||||||
|
echo "المخزون الجديد: " . $updated_product['stock'] . " قطعة\n";
|
||||||
|
|
||||||
|
// حذف المنتج
|
||||||
|
$stmt = $conn->prepare("DELETE FROM products WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $product_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "\nتم حذف المنتج بنجاح\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من الحذف
|
||||||
|
$stmt = $conn->prepare("SELECT COUNT(*) as count FROM products WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $product_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$count = $result->fetch_assoc()['count'];
|
||||||
|
|
||||||
|
echo "التحقق من الحذف: " . ($count == 0 ? "ناجح ✓" : "فشل ✗") . "\n";
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage() . "\n";
|
||||||
|
}
|
33
test_security.php
Normal file
33
test_security.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
require_once __DIR__ . '/includes/Security.php';
|
||||||
|
|
||||||
|
echo "=== اختبار وظائف الأمان ===\n\n";
|
||||||
|
|
||||||
|
// اختبار تشفير وفك تشفير البيانات
|
||||||
|
$testData = "بيانات حساسة للاختبار";
|
||||||
|
echo "البيانات الأصلية: " . $testData . "\n";
|
||||||
|
|
||||||
|
$encrypted = Security::encrypt($testData);
|
||||||
|
echo "البيانات المشفرة: " . $encrypted . "\n";
|
||||||
|
|
||||||
|
$decrypted = Security::decrypt($encrypted);
|
||||||
|
echo "البيانات بعد فك التشفير: " . $decrypted . "\n";
|
||||||
|
echo "نتيجة المقارنة: " . ($testData === $decrypted ? "ناجح ✓" : "فشل ✗") . "\n\n";
|
||||||
|
|
||||||
|
// اختبار تنظيف المدخلات
|
||||||
|
$dirtyInput = "<script>alert('XSS');</script>Hello' OR '1'='1";
|
||||||
|
echo "المدخلات غير النظيفة: " . $dirtyInput . "\n";
|
||||||
|
echo "المدخلات بعد التنظيف: " . Security::sanitizeInput($dirtyInput) . "\n\n";
|
||||||
|
|
||||||
|
// اختبار توليد والتحقق من رمز CSRF
|
||||||
|
$token = Security::generateCSRFToken();
|
||||||
|
echo "تم توليد رمز CSRF: " . $token . "\n";
|
||||||
|
echo "التحقق من الرمز: " . (Security::verifyCSRFToken($token) ? "ناجح ✓" : "فشل ✗") . "\n\n";
|
||||||
|
|
||||||
|
// اختبار تشفير كلمة المرور
|
||||||
|
$password = "TestPassword123";
|
||||||
|
$hashedPassword = Security::hashPassword($password);
|
||||||
|
echo "كلمة المرور الأصلية: " . $password . "\n";
|
||||||
|
echo "كلمة المرور المشفرة: " . $hashedPassword . "\n";
|
||||||
|
echo "التحقق من كلمة المرور: " . (Security::verifyPassword($password, $hashedPassword) ? "ناجح ✓" : "فشل ✗") . "\n";
|
102
test_user_system.php
Normal file
102
test_user_system.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'includes/config.php';
|
||||||
|
require_once 'includes/Security.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام المستخدمين ===\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// اختبار الاتصال بقاعدة البيانات
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("فشل الاتصال بقاعدة البيانات: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
echo "✓ تم الاتصال بقاعدة البيانات بنجاح\n";
|
||||||
|
|
||||||
|
// التحقق من وجود جدول المستخدمين
|
||||||
|
$result = $conn->query("SHOW TABLES LIKE 'users'");
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
echo "✓ جدول المستخدمين موجود\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("جدول المستخدمين غير موجود");
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من وجود جدول إعادة تعيين كلمات المرور
|
||||||
|
$result = $conn->query("SHOW TABLES LIKE 'password_resets'");
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
echo "✓ جدول إعادة تعيين كلمات المرور موجود\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("جدول إعادة تعيين كلمات المرور غير موجود");
|
||||||
|
}
|
||||||
|
|
||||||
|
// اختبار إنشاء مستخدم جديد
|
||||||
|
$test_username = "test_user_" . time();
|
||||||
|
$test_email = "test_" . time() . "@example.com";
|
||||||
|
$test_password = "Test@123456";
|
||||||
|
|
||||||
|
$hashed_password = Security::hashPassword($test_password);
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("sss", $test_username, $test_email, $hashed_password);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "✓ تم إنشاء مستخدم اختباري بنجاح\n";
|
||||||
|
$test_user_id = $stmt->insert_id;
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل في إنشاء مستخدم اختباري");
|
||||||
|
}
|
||||||
|
|
||||||
|
// اختبار تسجيل الدخول
|
||||||
|
$stmt = $conn->prepare("SELECT id, password FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $test_email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($row = $result->fetch_assoc()) {
|
||||||
|
if (Security::verifyPassword($test_password, $row['password'])) {
|
||||||
|
echo "✓ تم التحقق من كلمة المرور بنجاح\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل في التحقق من كلمة المرور");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل في العثور على المستخدم");
|
||||||
|
}
|
||||||
|
|
||||||
|
// اختبار إنشاء رمز إعادة تعيين كلمة المرور
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$expiry = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO password_resets (user_id, token, expiry) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("iss", $test_user_id, $token, $expiry);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "✓ تم إنشاء رمز إعادة تعيين كلمة المرور بنجاح\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل في إنشاء رمز إعادة تعيين كلمة المرور");
|
||||||
|
}
|
||||||
|
|
||||||
|
// اختبار التحقق من رمز إعادة تعيين كلمة المرور
|
||||||
|
$stmt = $conn->prepare("SELECT user_id FROM password_resets WHERE token = ? AND expiry > NOW() AND used = 0");
|
||||||
|
$stmt->bind_param("s", $token);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
echo "✓ تم التحقق من رمز إعادة تعيين كلمة المرور بنجاح\n";
|
||||||
|
} else {
|
||||||
|
throw new Exception("فشل في التحقق من رمز إعادة تعيين كلمة المرور");
|
||||||
|
}
|
||||||
|
|
||||||
|
// تنظيف البيانات الاختبارية
|
||||||
|
$conn->query("DELETE FROM password_resets WHERE user_id = " . $test_user_id);
|
||||||
|
$conn->query("DELETE FROM users WHERE id = " . $test_user_id);
|
||||||
|
echo "✓ تم تنظيف البيانات الاختبارية بنجاح\n";
|
||||||
|
|
||||||
|
echo "\n=== تم اكتمال الاختبار بنجاح ===\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "\n❌ خطأ: " . $e->getMessage() . "\n";
|
||||||
|
} finally {
|
||||||
|
if (isset($conn)) {
|
||||||
|
$conn->close();
|
||||||
|
}
|
||||||
|
}
|
74
test_users.php
Normal file
74
test_users.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/config.php';
|
||||||
|
require_once __DIR__ . '/includes/Security.php';
|
||||||
|
|
||||||
|
echo "=== اختبار نظام إدارة المستخدمين ===\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
if ($conn->connect_error) {
|
||||||
|
throw new Exception("فشل الاتصال: " . $conn->connect_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// تسجيل مستخدم جديد
|
||||||
|
$username = "test_user_" . time();
|
||||||
|
$email = "test_" . time() . "@example.com";
|
||||||
|
$password = "TestPass123!";
|
||||||
|
$hashed_password = Security::hashPassword($password);
|
||||||
|
|
||||||
|
$stmt = $conn->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
|
||||||
|
$stmt->bind_param("sss", $username, $email, $hashed_password);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$user_id = $conn->insert_id;
|
||||||
|
echo "تم تسجيل المستخدم بنجاح (ID: $user_id)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// محاولة تسجيل الدخول
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
|
||||||
|
if (Security::verifyPassword($password, $user['password'])) {
|
||||||
|
echo "تم تسجيل الدخول بنجاح\n";
|
||||||
|
} else {
|
||||||
|
echo "فشل تسجيل الدخول\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// تحديث معلومات المستخدم
|
||||||
|
$new_email = "updated_" . time() . "@example.com";
|
||||||
|
$stmt = $conn->prepare("UPDATE users SET email = ? WHERE id = ?");
|
||||||
|
$stmt->bind_param("si", $new_email, $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "تم تحديث معلومات المستخدم بنجاح\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// التحقق من التحديث
|
||||||
|
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$updated_user = $result->fetch_assoc();
|
||||||
|
|
||||||
|
echo "\nمعلومات المستخدم المحدثة:\n";
|
||||||
|
echo "اسم المستخدم: " . $updated_user['username'] . "\n";
|
||||||
|
echo "البريد الإلكتروني: " . $updated_user['email'] . "\n";
|
||||||
|
echo "نوع المستخدم: " . $updated_user['role'] . "\n";
|
||||||
|
|
||||||
|
// تنظيف البيانات
|
||||||
|
$stmt = $conn->prepare("DELETE FROM users WHERE id = ?");
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "\nتم حذف بيانات الاختبار بنجاح\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$conn->close();
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "خطأ: " . $e->getMessage() . "\n";
|
||||||
|
}
|
46
tests/SecurityTest.php
Normal file
46
tests/SecurityTest.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class SecurityTest extends TestCase {
|
||||||
|
private $security;
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
$this->security = Security::getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSanitizeInput() {
|
||||||
|
$input = "<script>alert('XSS')</script>";
|
||||||
|
$expected = "<script>alert('XSS')</script>";
|
||||||
|
$this->assertEquals($expected, $this->security->sanitizeInput($input));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCSRFToken() {
|
||||||
|
$token = $this->security->generateCSRFToken();
|
||||||
|
$this->assertTrue($this->security->validateCSRFToken($token));
|
||||||
|
$this->assertFalse($this->security->validateCSRFToken('invalid_token'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testJWT() {
|
||||||
|
$payload = ['user_id' => 1, 'role' => 'admin'];
|
||||||
|
$token = $this->security->generateJWT($payload);
|
||||||
|
$decoded = $this->security->validateJWT($token);
|
||||||
|
|
||||||
|
$this->assertIsArray($decoded);
|
||||||
|
$this->assertEquals(1, $decoded['user_id']);
|
||||||
|
$this->assertEquals('admin', $decoded['role']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRateLimit() {
|
||||||
|
$ip = '127.0.0.1';
|
||||||
|
$endpoint = 'test_endpoint';
|
||||||
|
|
||||||
|
// Should allow first request
|
||||||
|
$this->assertTrue($this->security->checkRateLimit($ip, $endpoint, 2, 3600));
|
||||||
|
|
||||||
|
// Should allow second request
|
||||||
|
$this->assertTrue($this->security->checkRateLimit($ip, $endpoint, 2, 3600));
|
||||||
|
|
||||||
|
// Should block third request
|
||||||
|
$this->assertFalse($this->security->checkRateLimit($ip, $endpoint, 2, 3600));
|
||||||
|
}
|
||||||
|
}
|
1
uploads/.gitkeep
Normal file
1
uploads/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
Loading…
Reference in New Issue
Block a user