commit f1f424e6ff99a6acc251cc554b3af4664b309bf6 Author: unknown Date: Wed Dec 25 13:05:50 2024 +0200 Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a553c41 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..7d257b5 --- /dev/null +++ b/.github/workflows/php.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cf12d1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..8b13352 --- /dev/null +++ b/.htaccess @@ -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 + + Order allow,deny + Deny from all + + +# Protect directories + + Order allow,deny + Deny from all + + +# 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 + + Header set Cache-Control "max-age=31536000, public" + + +# 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] diff --git a/404.html b/404.html new file mode 100644 index 0000000..3086758 --- /dev/null +++ b/404.html @@ -0,0 +1,17 @@ + + + + + + 404 - الصفحة غير موجودة | ShubraVeil + + + +
+

404

+

عذراً، الصفحة غير موجودة

+

الصفحة التي تبحث عنها غير موجودة أو تم نقلها.

+ العودة للصفحة الرئيسية +
+ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..67bcffb --- /dev/null +++ b/README.md @@ -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. diff --git a/admin/backup.php b/admin/backup.php new file mode 100644 index 0000000..0ef594a --- /dev/null +++ b/admin/backup.php @@ -0,0 +1,140 @@ +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; + } +} diff --git a/admin/config/database.php b/admin/config/database.php new file mode 100644 index 0000000..6c8370d --- /dev/null +++ b/admin/config/database.php @@ -0,0 +1,61 @@ + diff --git a/admin/config/setup.sql b/admin/config/setup.sql new file mode 100644 index 0000000..b18e6c1 --- /dev/null +++ b/admin/config/setup.sql @@ -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) +); diff --git a/admin/includes/auth.php b/admin/includes/auth.php new file mode 100644 index 0000000..6bf0935 --- /dev/null +++ b/admin/includes/auth.php @@ -0,0 +1,32 @@ + diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..8292259 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,179 @@ + + + + + + + لوحة التحكم - ShubraVeil + + + + + + +
+
+ + + + +
+
+

لوحة التحكم

+
+ +
+
+
+
+ +
+

المنتجات

+

+
+
+
+
+
+ +
+

إجمالي الطلبات

+

+
+
+
+
+
+ +
+

الطلبات المعلقة

+

+
+
+
+ + +
+

آخر الطلبات

+
+ + + + + + + + + + + "; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + ?> + +
#اسم العميلالتاريخالحالة
" . $row['id'] . "" . $row['customer_name'] . "" . $row['created_at'] . "" . $row['status'] . "
+
+
+
+
+
+ + + + + + diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..ed04e8a --- /dev/null +++ b/admin/login.php @@ -0,0 +1,179 @@ + + + + + + + + تسجيل الدخول - لوحة التحكم + + + + + +
+ +

تسجيل الدخول

+ + ' . $login_err . '
'; + } + ?> + +
" method="post"> +
+ + + +
+
+ + + +
+
+ +
+
+ +
+ العودة إلى الموقع الرئيسي +
+ + + + + + + diff --git a/admin/logout.php b/admin/logout.php new file mode 100644 index 0000000..a6f0bcf --- /dev/null +++ b/admin/logout.php @@ -0,0 +1,7 @@ + diff --git a/admin/products.php b/admin/products.php new file mode 100644 index 0000000..cce904e --- /dev/null +++ b/admin/products.php @@ -0,0 +1,237 @@ + + + + + + + إدارة المنتجات - ShubraVeil + + + + + + +
+
+ + + + +
+
+

إدارة المنتجات

+ +
+ + +
+ + + + + + + + + + + + + + "; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + ?> + +
#الصورةاسم المنتجالسعرالفئةنوع المنتجالإجراءات
" . $row['id'] . "" . $row[" . $row['name'] . "" . $row['price'] . "" . $row['category'] . "" . $row['product_type'] . " + + + + +
+
+ + + +
+
+
+ + + + + + diff --git a/apache.conf b/apache.conf new file mode 100644 index 0000000..bf21e3b --- /dev/null +++ b/apache.conf @@ -0,0 +1,13 @@ + + ServerName shubraveil.local + DocumentRoot /home/momaher/Public/Websites/shubraveil + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/shubraveil_error.log + CustomLog ${APACHE_LOG_DIR}/shubraveil_access.log combined + diff --git a/api/products.php b/api/products.php new file mode 100644 index 0000000..11c72c5 --- /dev/null +++ b/api/products.php @@ -0,0 +1,30 @@ + $row['id'], + 'name' => $row['name'], + 'description' => $row['description'], + 'price' => $row['price'], + 'image' => $row['image'], + 'category' => $row['category'] + ]; +} + +echo json_encode($products); +?> diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backups/.gitkeep @@ -0,0 +1 @@ + diff --git a/blog.html b/blog.html new file mode 100644 index 0000000..d84abf0 --- /dev/null +++ b/blog.html @@ -0,0 +1,103 @@ + + + + + + المدونة - ShubraVeil + + + + + + +
+ +
+ +
+
+
+

المدونة

+ +
+ + +
+ + + + +
+
+ +
+ +
+
+ فوائد اللافندر + فوائد صحية +
+
+

10 فوائد مذهلة لزيت اللافندر

+

اكتشف الفوائد المتعددة لزيت اللافندر العطري وكيفية استخدامه للحصول على أفضل النتائج...

+
+ 8 ديسمبر 2024 + 5 دقائق قراءة +
+ اقرأ المزيد +
+
+ + +
+
+ دليل الزيوت العطرية + نصائح واستخدامات +
+
+

دليلك الشامل لاستخدام الزيوت العطرية

+

تعرف على الطرق الصحيحة لاستخدام الزيوت العطرية وكيفية مزجها للحصول على أفضل النتائج...

+
+ 7 ديسمبر 2024 + 8 دقائق قراءة +
+ اقرأ المزيد +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + + diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cache/.gitkeep @@ -0,0 +1 @@ + diff --git a/composer-setup.php b/composer-setup.php new file mode 100644 index 0000000..a5efbed --- /dev/null +++ b/composer-setup.php @@ -0,0 +1,1748 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +setupEnvironment(); +process(is_array($argv) ? $argv : array()); + +/** + * Initializes various values + * + * @throws RuntimeException If uopz extension prevents exit calls + */ +function setupEnvironment() +{ + ini_set('display_errors', 1); + + if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) { + // uopz works at opcode level and disables exit calls + if (function_exists('uopz_allow_exit')) { + @uopz_allow_exit(true); + } else { + throw new RuntimeException('The uopz extension ignores exit calls and breaks this installer.'); + } + } + + $installer = 'ComposerInstaller'; + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + if ($version = getenv('COMPOSERSETUP')) { + $installer = sprintf('Composer-Setup.exe/%s', $version); + } + } + + define('COMPOSER_INSTALLER', $installer); +} + +/** + * Processes the installer + */ +function process($argv) +{ + // Determine ANSI output from --ansi and --no-ansi flags + setUseAnsi($argv); + + $help = in_array('--help', $argv) || in_array('-h', $argv); + if ($help) { + displayHelp(); + exit(0); + } + + $check = in_array('--check', $argv); + $force = in_array('--force', $argv); + $quiet = in_array('--quiet', $argv); + $channel = 'stable'; + if (in_array('--snapshot', $argv)) { + $channel = 'snapshot'; + } elseif (in_array('--preview', $argv)) { + $channel = 'preview'; + } elseif (in_array('--1', $argv)) { + $channel = '1'; + } elseif (in_array('--2', $argv)) { + $channel = '2'; + } elseif (in_array('--2.2', $argv)) { + $channel = '2.2'; + } + $disableTls = in_array('--disable-tls', $argv); + $installDir = getOptValue('--install-dir', $argv, false); + $version = getOptValue('--version', $argv, false); + $filename = getOptValue('--filename', $argv, 'composer.phar'); + $cafile = getOptValue('--cafile', $argv, false); + + if (!checkParams($installDir, $version, $cafile)) { + exit(1); + } + + $ok = checkPlatform($warnings, $quiet, $disableTls, true); + + if ($check) { + // Only show warnings if we haven't output any errors + if ($ok) { + showWarnings($warnings); + showSecurityWarning($disableTls); + } + exit($ok ? 0 : 1); + } + + if ($ok || $force) { + if ($channel === '1' && !$quiet) { + out('Warning: You forced the install of Composer 1.x via --1, but Composer 2.x is the latest stable version. Updating to it via composer self-update --stable is recommended.', 'error'); + } + + $installer = new Installer($quiet, $disableTls, $cafile); + if ($installer->run($version, $installDir, $filename, $channel)) { + showWarnings($warnings); + showSecurityWarning($disableTls); + exit(0); + } + } + + exit(1); +} + +/** + * Displays the help + */ +function displayHelp() +{ + echo << $value) { + $next = $key + 1; + if (0 === strpos($value, $opt)) { + if ($optLength === strlen($value) && isset($argv[$next])) { + return trim($argv[$next]); + } else { + return trim(substr($value, $optLength + 1)); + } + } + } + + return $default; +} + +/** + * Checks that user-supplied params are valid + * + * @param mixed $installDir The required istallation directory + * @param mixed $version The required composer version to install + * @param mixed $cafile Certificate Authority file + * + * @return bool True if the supplied params are okay + */ +function checkParams($installDir, $version, $cafile) +{ + $result = true; + + if (false !== $installDir && !is_dir($installDir)) { + out("The defined install dir ({$installDir}) does not exist.", 'info'); + $result = false; + } + + if (false !== $version && 1 !== preg_match('/^\d+\.\d+\.\d+(\-(alpha|beta|RC)\d*)*$/', $version)) { + out("The defined install version ({$version}) does not match release pattern.", 'info'); + $result = false; + } + + if (false !== $cafile && (!file_exists($cafile) || !is_readable($cafile))) { + out("The defined Certificate Authority (CA) cert file ({$cafile}) does not exist or is not readable.", 'info'); + $result = false; + } + return $result; +} + +/** + * Checks the platform for possible issues running Composer + * + * Errors are written to the output, warnings are saved for later display. + * + * @param array $warnings Populated by method, to be shown later + * @param bool $quiet Quiet mode + * @param bool $disableTls Bypass tls + * @param bool $install If we are installing, rather than diagnosing + * + * @return bool True if there are no errors + */ +function checkPlatform(&$warnings, $quiet, $disableTls, $install) +{ + getPlatformIssues($errors, $warnings, $install); + + // Make openssl warning an error if tls has not been specifically disabled + if (isset($warnings['openssl']) && !$disableTls) { + $errors['openssl'] = $warnings['openssl']; + unset($warnings['openssl']); + } + + if (!empty($errors)) { + // Composer-Setup.exe uses "Some settings" to flag platform errors + out('Some settings on your machine make Composer unable to work properly.', 'error'); + out('Make sure that you fix the issues listed below and run this script again:', 'error'); + outputIssues($errors); + return false; + } + + if (empty($warnings) && !$quiet) { + out('All settings correct for using Composer', 'success'); + } + return true; +} + +/** + * Checks platform configuration for common incompatibility issues + * + * @param array $errors Populated by method + * @param array $warnings Populated by method + * @param bool $install If we are installing, rather than diagnosing + * + * @return bool If any errors or warnings have been found + */ +function getPlatformIssues(&$errors, &$warnings, $install) +{ + $errors = array(); + $warnings = array(); + + if ($iniPath = php_ini_loaded_file()) { + $iniMessage = PHP_EOL.'The php.ini used by your command-line PHP is: ' . $iniPath; + } else { + $iniMessage = PHP_EOL.'A php.ini file does not exist. You will have to create one.'; + } + $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.'; + + if (ini_get('detect_unicode')) { + $errors['unicode'] = array( + 'The detect_unicode setting must be disabled.', + 'Add the following to the end of your `php.ini`:', + ' detect_unicode = Off', + $iniMessage + ); + } + + if (extension_loaded('suhosin')) { + $suhosin = ini_get('suhosin.executor.include.whitelist'); + $suhosinBlacklist = ini_get('suhosin.executor.include.blacklist'); + if (false === stripos($suhosin, 'phar') && (!$suhosinBlacklist || false !== stripos($suhosinBlacklist, 'phar'))) { + $errors['suhosin'] = array( + 'The suhosin.executor.include.whitelist setting is incorrect.', + 'Add the following to the end of your `php.ini` or suhosin.ini (Example path [for Debian]: /etc/php5/cli/conf.d/suhosin.ini):', + ' suhosin.executor.include.whitelist = phar '.$suhosin, + $iniMessage + ); + } + } + + if (!function_exists('json_decode')) { + $errors['json'] = array( + 'The json extension is missing.', + 'Install it or recompile php without --disable-json' + ); + } + + if (!extension_loaded('Phar')) { + $errors['phar'] = array( + 'The phar extension is missing.', + 'Install it or recompile php without --disable-phar' + ); + } + + if (!extension_loaded('filter')) { + $errors['filter'] = array( + 'The filter extension is missing.', + 'Install it or recompile php without --disable-filter' + ); + } + + if (!extension_loaded('hash')) { + $errors['hash'] = array( + 'The hash extension is missing.', + 'Install it or recompile php without --disable-hash' + ); + } + + if (!extension_loaded('iconv') && !extension_loaded('mbstring')) { + $errors['iconv_mbstring'] = array( + 'The iconv OR mbstring extension is required and both are missing.', + 'Install either of them or recompile php without --disable-iconv' + ); + } + + if (!ini_get('allow_url_fopen')) { + $errors['allow_url_fopen'] = array( + 'The allow_url_fopen setting is incorrect.', + 'Add the following to the end of your `php.ini`:', + ' allow_url_fopen = On', + $iniMessage + ); + } + + if (extension_loaded('ionCube Loader') && ioncube_loader_iversion() < 40009) { + $ioncube = ioncube_loader_version(); + $errors['ioncube'] = array( + 'Your ionCube Loader extension ('.$ioncube.') is incompatible with Phar files.', + 'Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:', + ' zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so', + $iniMessage + ); + } + + if (version_compare(PHP_VERSION, '5.3.2', '<')) { + $errors['php'] = array( + 'Your PHP ('.PHP_VERSION.') is too old, you must upgrade to PHP 5.3.2 or higher.' + ); + } + + if (version_compare(PHP_VERSION, '5.3.4', '<')) { + $warnings['php'] = array( + 'Your PHP ('.PHP_VERSION.') is quite old, upgrading to PHP 5.3.4 or higher is recommended.', + 'Composer works with 5.3.2+ for most people, but there might be edge case issues.' + ); + } + + if (!extension_loaded('openssl')) { + $warnings['openssl'] = array( + 'The openssl extension is missing, which means that secure HTTPS transfers are impossible.', + 'If possible you should enable it or recompile php with --with-openssl' + ); + } + + if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) { + // Attempt to parse version number out, fallback to whole string value. + $opensslVersion = trim(strstr(OPENSSL_VERSION_TEXT, ' ')); + $opensslVersion = substr($opensslVersion, 0, strpos($opensslVersion, ' ')); + $opensslVersion = $opensslVersion ? $opensslVersion : OPENSSL_VERSION_TEXT; + + $warnings['openssl_version'] = array( + 'The OpenSSL library ('.$opensslVersion.') used by PHP does not support TLSv1.2 or TLSv1.1.', + 'If possible you should upgrade OpenSSL to version 1.0.1 or above.' + ); + } + + if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && ini_get('apc.enable_cli')) { + $warnings['apc_cli'] = array( + 'The apc.enable_cli setting is incorrect.', + 'Add the following to the end of your `php.ini`:', + ' apc.enable_cli = Off', + $iniMessage + ); + } + + if (!$install && extension_loaded('xdebug')) { + $warnings['xdebug_loaded'] = array( + 'The xdebug extension is loaded, this can slow down Composer a little.', + 'Disabling it when using Composer is recommended.' + ); + + if (ini_get('xdebug.profiler_enabled')) { + $warnings['xdebug_profile'] = array( + 'The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.', + 'Add the following to the end of your `php.ini` to disable it:', + ' xdebug.profiler_enabled = 0', + $iniMessage + ); + } + } + + if (!extension_loaded('zlib')) { + $warnings['zlib'] = array( + 'The zlib extension is not loaded, this can slow down Composer a lot.', + 'If possible, install it or recompile php with --with-zlib', + $iniMessage + ); + } + + if (defined('PHP_WINDOWS_VERSION_BUILD') + && (version_compare(PHP_VERSION, '7.2.23', '<') + || (version_compare(PHP_VERSION, '7.3.0', '>=') + && version_compare(PHP_VERSION, '7.3.10', '<')))) { + $warnings['onedrive'] = array( + 'The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.', + 'Upgrade your PHP ('.PHP_VERSION.') to use this location with Composer.' + ); + } + + if (extension_loaded('uopz') && !(ini_get('uopz.disable') || ini_get('uopz.exit'))) { + $warnings['uopz'] = array( + 'The uopz extension ignores exit calls and may not work with all Composer commands.', + 'Disabling it when using Composer is recommended.' + ); + } + + ob_start(); + phpinfo(INFO_GENERAL); + $phpinfo = ob_get_clean(); + if (preg_match('{Configure Command(?: *| *=> *)(.*?)(?:|$)}m', $phpinfo, $match)) { + $configure = $match[1]; + + if (false !== strpos($configure, '--enable-sigchild')) { + $warnings['sigchild'] = array( + 'PHP was compiled with --enable-sigchild which can cause issues on some platforms.', + 'Recompile it without this flag if possible, see also:', + ' https://bugs.php.net/bug.php?id=22999' + ); + } + + if (false !== strpos($configure, '--with-curlwrappers')) { + $warnings['curlwrappers'] = array( + 'PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.', + 'Recompile it without this flag if possible' + ); + } + } + + // Stringify the message arrays + foreach ($errors as $key => $value) { + $errors[$key] = PHP_EOL.implode(PHP_EOL, $value); + } + + foreach ($warnings as $key => $value) { + $warnings[$key] = PHP_EOL.implode(PHP_EOL, $value); + } + + return !empty($errors) || !empty($warnings); +} + + +/** + * Outputs an array of issues + * + * @param array $issues + */ +function outputIssues($issues) +{ + foreach ($issues as $issue) { + out($issue, 'info'); + } + out(''); +} + +/** + * Outputs any warnings found + * + * @param array $warnings + */ +function showWarnings($warnings) +{ + if (!empty($warnings)) { + out('Some settings on your machine may cause stability issues with Composer.', 'error'); + out('If you encounter issues, try to change the following:', 'error'); + outputIssues($warnings); + } +} + +/** + * Outputs an end of process warning if tls has been bypassed + * + * @param bool $disableTls Bypass tls + */ +function showSecurityWarning($disableTls) +{ + if ($disableTls) { + out('You have instructed the Installer not to enforce SSL/TLS security on remote HTTPS requests.', 'info'); + out('This will leave all downloads during installation vulnerable to Man-In-The-Middle (MITM) attacks', 'info'); + } +} + +/** + * colorize output + */ +function out($text, $color = null, $newLine = true) +{ + $styles = array( + 'success' => "\033[0;32m%s\033[0m", + 'error' => "\033[31;31m%s\033[0m", + 'info' => "\033[33;33m%s\033[0m" + ); + + $format = '%s'; + + if (isset($styles[$color]) && USE_ANSI) { + $format = $styles[$color]; + } + + if ($newLine) { + $format .= PHP_EOL; + } + + printf($format, $text); +} + +/** + * Returns the system-dependent Composer home location, which may not exist + * + * @return string + */ +function getHomeDir() +{ + $home = getenv('COMPOSER_HOME'); + if ($home) { + return $home; + } + + $userDir = getUserDir(); + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + return $userDir.'/Composer'; + } + + $dirs = array(); + + if (useXdg()) { + // XDG Base Directory Specifications + $xdgConfig = getenv('XDG_CONFIG_HOME'); + if (!$xdgConfig) { + $xdgConfig = $userDir . '/.config'; + } + + $dirs[] = $xdgConfig . '/composer'; + } + + $dirs[] = $userDir . '/.composer'; + + // select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer + foreach ($dirs as $dir) { + if (is_dir($dir)) { + return $dir; + } + } + + // if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise) + return $dirs[0]; +} + +/** + * Returns the location of the user directory from the environment + * @throws RuntimeException If the environment value does not exists + * + * @return string + */ +function getUserDir() +{ + $userEnv = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'APPDATA' : 'HOME'; + $userDir = getenv($userEnv); + + if (!$userDir) { + throw new RuntimeException('The '.$userEnv.' or COMPOSER_HOME environment variable must be set for composer to run correctly'); + } + + return rtrim(strtr($userDir, '\\', '/'), '/'); +} + +/** + * @return bool + */ +function useXdg() +{ + foreach (array_keys($_SERVER) as $key) { + if (strpos($key, 'XDG_') === 0) { + return true; + } + } + + if (is_dir('/etc/xdg')) { + return true; + } + + return false; +} + +function validateCaFile($contents) +{ + // assume the CA is valid if php is vulnerable to + // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html + if ( + PHP_VERSION_ID <= 50327 + || (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422) + || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506) + ) { + return !empty($contents); + } + + return (bool) openssl_x509_parse($contents); +} + +class Installer +{ + private $quiet; + private $disableTls; + private $cafile; + private $displayPath; + private $target; + private $tmpFile; + private $tmpCafile; + private $baseUrl; + private $algo; + private $errHandler; + private $httpClient; + private $pubKeys = array(); + private $installs = array(); + + /** + * Constructor - must not do anything that throws an exception + * + * @param bool $quiet Quiet mode + * @param bool $disableTls Bypass tls + * @param mixed $cafile Path to CA bundle, or false + */ + public function __construct($quiet, $disableTls, $caFile) + { + if (($this->quiet = $quiet)) { + ob_start(); + } + $this->disableTls = $disableTls; + $this->cafile = $caFile; + $this->errHandler = new ErrorHandler(); + } + + /** + * Runs the installer + * + * @param mixed $version Specific version to install, or false + * @param mixed $installDir Specific installation directory, or false + * @param string $filename Specific filename to save to, or composer.phar + * @param string $channel Specific version channel to use + * @throws Exception If anything other than a RuntimeException is caught + * + * @return bool If the installation succeeded + */ + public function run($version, $installDir, $filename, $channel) + { + try { + $this->initTargets($installDir, $filename); + $this->initTls(); + $this->httpClient = new HttpClient($this->disableTls, $this->cafile); + $result = $this->install($version, $channel); + + // in case --1 or --2 is passed, we leave the default channel for next self-update to stable + if (1 === preg_match('{^\d+$}D', $channel)) { + $channel = 'stable'; + } + + if ($result && $channel !== 'stable' && !$version && defined('PHP_BINARY')) { + $null = (defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null'); + @exec(escapeshellarg(PHP_BINARY) .' '.escapeshellarg($this->target).' self-update --'.$channel.' --set-channel-only -q > '.$null.' 2> '.$null, $output); + } + } catch (Exception $e) { + $result = false; + } + + // Always clean up + $this->cleanUp($result); + + if (isset($e)) { + // Rethrow anything that is not a RuntimeException + if (!$e instanceof RuntimeException) { + throw $e; + } + out($e->getMessage(), 'error'); + } + return $result; + } + + /** + * Initialization methods to set the required filenames and composer url + * + * @param mixed $installDir Specific installation directory, or false + * @param string $filename Specific filename to save to, or composer.phar + * @throws RuntimeException If the installation directory is not writable + */ + protected function initTargets($installDir, $filename) + { + $this->displayPath = ($installDir ? rtrim($installDir, '/').'/' : '').$filename; + $installDir = $installDir ? realpath($installDir) : getcwd(); + + if (!is_writeable($installDir)) { + throw new RuntimeException('The installation directory "'.$installDir.'" is not writable'); + } + + $this->target = $installDir.DIRECTORY_SEPARATOR.$filename; + $this->tmpFile = $installDir.DIRECTORY_SEPARATOR.basename($this->target, '.phar').'-temp.phar'; + + $uriScheme = $this->disableTls ? 'http' : 'https'; + $this->baseUrl = $uriScheme.'://getcomposer.org'; + } + + /** + * A wrapper around methods to check tls and write public keys + * @throws RuntimeException If SHA384 is not supported + */ + protected function initTls() + { + if ($this->disableTls) { + return; + } + + if (!in_array('sha384', array_map('strtolower', openssl_get_md_methods()))) { + throw new RuntimeException('SHA384 is not supported by your openssl extension'); + } + + $this->algo = defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'SHA384'; + $home = $this->getComposerHome(); + + $this->pubKeys = array( + 'dev' => $this->installKey(self::getPKDev(), $home, 'keys.dev.pub'), + 'tags' => $this->installKey(self::getPKTags(), $home, 'keys.tags.pub') + ); + + if (empty($this->cafile) && !HttpClient::getSystemCaRootBundlePath()) { + $this->cafile = $this->tmpCafile = $this->installKey(HttpClient::getPackagedCaFile(), $home, 'cacert-temp.pem'); + } + } + + /** + * Returns the Composer home directory, creating it if required + * @throws RuntimeException If the directory cannot be created + * + * @return string + */ + protected function getComposerHome() + { + $home = getHomeDir(); + + if (!is_dir($home)) { + $this->errHandler->start(); + + if (!mkdir($home, 0777, true)) { + throw new RuntimeException(sprintf( + 'Unable to create Composer home directory "%s": %s', + $home, + $this->errHandler->message + )); + } + $this->installs[] = $home; + $this->errHandler->stop(); + } + return $home; + } + + /** + * Writes public key data to disc + * + * @param string $data The public key(s) in pem format + * @param string $path The directory to write to + * @param string $filename The name of the file + * @throws RuntimeException If the file cannot be written + * + * @return string The path to the saved data + */ + protected function installKey($data, $path, $filename) + { + $this->errHandler->start(); + + $target = $path.DIRECTORY_SEPARATOR.$filename; + $installed = file_exists($target); + $write = file_put_contents($target, $data, LOCK_EX); + @chmod($target, 0644); + + $this->errHandler->stop(); + + if (!$write) { + throw new RuntimeException(sprintf('Unable to write %s to: %s', $filename, $path)); + } + + if (!$installed) { + $this->installs[] = $target; + } + + return $target; + } + + /** + * The main install function + * + * @param mixed $version Specific version to install, or false + * @param string $channel Version channel to use + * + * @return bool If the installation succeeded + */ + protected function install($version, $channel) + { + $retries = 3; + $result = false; + $infoMsg = 'Downloading...'; + $infoType = 'info'; + + while ($retries--) { + if (!$this->quiet) { + out($infoMsg, $infoType); + $infoMsg = 'Retrying...'; + $infoType = 'error'; + } + + if (!$this->getVersion($channel, $version, $url, $error)) { + out($error, 'error'); + continue; + } + + if (!$this->downloadToTmp($url, $signature, $error)) { + out($error, 'error'); + continue; + } + + if (!$this->verifyAndSave($version, $signature, $error)) { + out($error, 'error'); + continue; + } + + $result = true; + break; + } + + if (!$this->quiet) { + if ($result) { + out(PHP_EOL."Composer (version {$version}) successfully installed to: {$this->target}", 'success'); + out("Use it: php {$this->displayPath}", 'info'); + out(''); + } else { + out('The download failed repeatedly, aborting.', 'error'); + } + } + return $result; + } + + /** + * Sets the version url, downloading version data if required + * + * @param string $channel Version channel to use + * @param false|string $version Version to install, or set by method + * @param null|string $url The versioned url, set by method + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function getVersion($channel, &$version, &$url, &$error) + { + $error = ''; + + if ($version) { + if (empty($url)) { + $url = $this->baseUrl."/download/{$version}/composer.phar"; + } + return true; + } + + $this->errHandler->start(); + + if ($this->downloadVersionData($data, $error)) { + $this->parseVersionData($data, $channel, $version, $url); + } + + $this->errHandler->stop(); + return empty($error); + } + + /** + * Downloads and json-decodes version data + * + * @param null|array $data Downloaded version data, set by method + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function downloadVersionData(&$data, &$error) + { + $url = $this->baseUrl.'/versions'; + $errFmt = 'The "%s" file could not be %s: %s'; + + if (!$json = $this->httpClient->get($url)) { + $error = sprintf($errFmt, $url, 'downloaded', $this->errHandler->message); + return false; + } + + if (!$data = json_decode($json, true)) { + $error = sprintf($errFmt, $url, 'json-decoded', $this->getJsonError()); + return false; + } + return true; + } + + /** + * A wrapper around the methods needed to download and save the phar + * + * @param string $url The versioned download url + * @param null|string $signature Set by method on successful download + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function downloadToTmp($url, &$signature, &$error) + { + $error = ''; + $errFmt = 'The "%s" file could not be downloaded: %s'; + $sigUrl = $url.'.sig'; + $this->errHandler->start(); + + if (!$fh = fopen($this->tmpFile, 'w')) { + $error = sprintf('Could not create file "%s": %s', $this->tmpFile, $this->errHandler->message); + + } elseif (!$this->getSignature($sigUrl, $signature)) { + $error = sprintf($errFmt, $sigUrl, $this->errHandler->message); + + } elseif (!fwrite($fh, $this->httpClient->get($url))) { + $error = sprintf($errFmt, $url, $this->errHandler->message); + } + + if (is_resource($fh)) { + fclose($fh); + } + $this->errHandler->stop(); + return empty($error); + } + + /** + * Verifies the downloaded file and saves it to the target location + * + * @param string $version The composer version downloaded + * @param string $signature The digital signature to check + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function verifyAndSave($version, $signature, &$error) + { + $error = ''; + + if (!$this->validatePhar($this->tmpFile, $pharError)) { + $error = 'The download is corrupt: '.$pharError; + + } elseif (!$this->verifySignature($version, $signature, $this->tmpFile)) { + $error = 'Signature mismatch, could not verify the phar file integrity'; + + } else { + $this->errHandler->start(); + + if (!rename($this->tmpFile, $this->target)) { + $error = sprintf('Could not write to file "%s": %s', $this->target, $this->errHandler->message); + } + chmod($this->target, 0755); + $this->errHandler->stop(); + } + + return empty($error); + } + + /** + * Parses an array of version data to match the required channel + * + * @param array $data Downloaded version data + * @param mixed $channel Version channel to use + * @param false|string $version Set by method + * @param mixed $url The versioned url, set by method + */ + protected function parseVersionData(array $data, $channel, &$version, &$url) + { + foreach ($data[$channel] as $candidate) { + if ($candidate['min-php'] <= PHP_VERSION_ID) { + $version = $candidate['version']; + $url = $this->baseUrl.$candidate['path']; + break; + } + } + + if (!$version) { + $error = sprintf( + 'None of the %d %s version(s) of Composer matches your PHP version (%s / ID: %d)', + count($data[$channel]), + $channel, + PHP_VERSION, + PHP_VERSION_ID + ); + throw new RuntimeException($error); + } + } + + /** + * Downloads the digital signature of required phar file + * + * @param string $url The signature url + * @param null|string $signature Set by method on success + * + * @return bool If the download succeeded + */ + protected function getSignature($url, &$signature) + { + if (!$result = $this->disableTls) { + $signature = $this->httpClient->get($url); + + if ($signature) { + $signature = json_decode($signature, true); + $signature = base64_decode($signature['sha384']); + $result = true; + } + } + + return $result; + } + + /** + * Verifies the signature of the downloaded phar + * + * @param string $version The composer versione + * @param string $signature The downloaded digital signature + * @param string $file The temp phar file + * + * @return bool If the operation succeeded + */ + protected function verifySignature($version, $signature, $file) + { + if (!$result = $this->disableTls) { + $path = preg_match('{^[0-9a-f]{40}$}', $version) ? $this->pubKeys['dev'] : $this->pubKeys['tags']; + $pubkeyid = openssl_pkey_get_public('file://'.$path); + + $result = 1 === openssl_verify( + file_get_contents($file), + $signature, + $pubkeyid, + $this->algo + ); + + // PHP 8 automatically frees the key instance and deprecates the function + if (PHP_VERSION_ID < 80000) { + openssl_free_key($pubkeyid); + } + } + + return $result; + } + + /** + * Validates the downloaded phar file + * + * @param string $pharFile The temp phar file + * @param null|string $error Set by method on failure + * + * @return bool If the operation succeeded + */ + protected function validatePhar($pharFile, &$error) + { + if (ini_get('phar.readonly')) { + return true; + } + + try { + // Test the phar validity + $phar = new Phar($pharFile); + // Free the variable to unlock the file + unset($phar); + $result = true; + + } catch (Exception $e) { + if (!$e instanceof UnexpectedValueException && !$e instanceof PharException) { + throw $e; + } + $error = $e->getMessage(); + $result = false; + } + return $result; + } + + /** + * Returns a string representation of the last json error + * + * @return string The error string or code + */ + protected function getJsonError() + { + if (function_exists('json_last_error_msg')) { + return json_last_error_msg(); + } else { + return 'json_last_error = '.json_last_error(); + } + } + + /** + * Cleans up resources at the end of the installation + * + * @param bool $result If the installation succeeded + */ + protected function cleanUp($result) + { + if (!$result) { + // Output buffered errors + if ($this->quiet) { + $this->outputErrors(); + } + // Clean up stuff we created + $this->uninstall(); + } elseif ($this->tmpCafile) { + @unlink($this->tmpCafile); + } + } + + /** + * Outputs unique errors when in quiet mode + * + */ + protected function outputErrors() + { + $errors = explode(PHP_EOL, ob_get_clean()); + $shown = array(); + + foreach ($errors as $error) { + if ($error && !in_array($error, $shown)) { + out($error, 'error'); + $shown[] = $error; + } + } + } + + /** + * Uninstalls newly-created files and directories on failure + * + */ + protected function uninstall() + { + foreach (array_reverse($this->installs) as $target) { + if (is_file($target)) { + @unlink($target); + } elseif (is_dir($target)) { + @rmdir($target); + } + } + + if ($this->tmpFile !== null && file_exists($this->tmpFile)) { + @unlink($this->tmpFile); + } + } + + public static function getPKDev() + { + return <<message) { + $this->message .= PHP_EOL; + } + $this->message .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg); + } + + /** + * Starts error-handling if not already active + * + * Any message is cleared + */ + public function start() + { + if (!$this->active) { + set_error_handler(array($this, 'handleError')); + $this->active = true; + } + $this->message = ''; + } + + /** + * Stops error-handling if active + * + * Any message is preserved until the next call to start() + */ + public function stop() + { + if ($this->active) { + restore_error_handler(); + $this->active = false; + } + } +} + +class NoProxyPattern +{ + private $composerInNoProxy = false; + private $rulePorts = array(); + + public function __construct($pattern) + { + $rules = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY); + + if ($matches = preg_grep('{getcomposer\.org(?::\d+)?}i', $rules)) { + $this->composerInNoProxy = true; + + foreach ($matches as $match) { + if (strpos($match, ':') !== false) { + list(, $port) = explode(':', $match); + $this->rulePorts[] = (int) $port; + } + } + } + } + + /** + * Returns true if NO_PROXY contains getcomposer.org + * + * @param string $url http(s)://getcomposer.org + * + * @return bool + */ + public function test($url) + { + if (!$this->composerInNoProxy) { + return false; + } + + if (empty($this->rulePorts)) { + return true; + } + + if (strpos($url, 'http://') === 0) { + $port = 80; + } else { + $port = 443; + } + + return in_array($port, $this->rulePorts); + } +} + +class HttpClient { + + /** @var null|string */ + private static $caPath; + + private $options = array('http' => array()); + private $disableTls = false; + + public function __construct($disableTls = false, $cafile = false) + { + $this->disableTls = $disableTls; + if ($this->disableTls === false) { + if (!empty($cafile) && !is_dir($cafile)) { + if (!is_readable($cafile) || !validateCaFile(file_get_contents($cafile))) { + throw new RuntimeException('The configured cafile (' .$cafile. ') was not valid or could not be read.'); + } + } + $options = $this->getTlsStreamContextDefaults($cafile); + $this->options = array_replace_recursive($this->options, $options); + } + } + + public function get($url) + { + $context = $this->getStreamContext($url); + $result = file_get_contents($url, false, $context); + + if ($result && extension_loaded('zlib')) { + $decode = false; + foreach ($http_response_header as $header) { + if (preg_match('{^content-encoding: *gzip *$}i', $header)) { + $decode = true; + continue; + } elseif (preg_match('{^HTTP/}i', $header)) { + $decode = false; + } + } + + if ($decode) { + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + $result = zlib_decode($result); + } else { + // work around issue with gzuncompress & co that do not work with all gzip checksums + $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); + } + + if (!$result) { + throw new RuntimeException('Failed to decode zlib stream'); + } + } + } + + return $result; + } + + protected function getStreamContext($url) + { + if ($this->disableTls === false) { + if (PHP_VERSION_ID < 50600) { + $this->options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST); + } + } + // Keeping the above mostly isolated from the code copied from Composer. + return $this->getMergedStreamContext($url); + } + + protected function getTlsStreamContextDefaults($cafile) + { + $ciphers = implode(':', array( + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'DHE-DSS-AES128-GCM-SHA256', + 'kEDH+AESGCM', + 'ECDHE-RSA-AES128-SHA256', + 'ECDHE-ECDSA-AES128-SHA256', + 'ECDHE-RSA-AES128-SHA', + 'ECDHE-ECDSA-AES128-SHA', + 'ECDHE-RSA-AES256-SHA384', + 'ECDHE-ECDSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA', + 'ECDHE-ECDSA-AES256-SHA', + 'DHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA', + 'DHE-DSS-AES128-SHA256', + 'DHE-RSA-AES256-SHA256', + 'DHE-DSS-AES256-SHA', + 'DHE-RSA-AES256-SHA', + 'AES128-GCM-SHA256', + 'AES256-GCM-SHA384', + 'AES128-SHA256', + 'AES256-SHA256', + 'AES128-SHA', + 'AES256-SHA', + 'AES', + 'CAMELLIA', + 'DES-CBC3-SHA', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!aECDH', + '!EDH-DSS-DES-CBC3-SHA', + '!EDH-RSA-DES-CBC3-SHA', + '!KRB5-DES-CBC3-SHA', + )); + + /** + * CN_match and SNI_server_name are only known once a URL is passed. + * They will be set in the getOptionsForUrl() method which receives a URL. + * + * cafile or capath can be overridden by passing in those options to constructor. + */ + $options = array( + 'ssl' => array( + 'ciphers' => $ciphers, + 'verify_peer' => true, + 'verify_depth' => 7, + 'SNI_enabled' => true, + ) + ); + + /** + * Attempt to find a local cafile or throw an exception. + * The user may go download one if this occurs. + */ + if (!$cafile) { + $cafile = self::getSystemCaRootBundlePath(); + } + if (is_dir($cafile)) { + $options['ssl']['capath'] = $cafile; + } elseif ($cafile) { + $options['ssl']['cafile'] = $cafile; + } else { + throw new RuntimeException('A valid cafile could not be located automatically.'); + } + + /** + * Disable TLS compression to prevent CRIME attacks where supported. + */ + if (version_compare(PHP_VERSION, '5.4.13') >= 0) { + $options['ssl']['disable_compression'] = true; + } + + return $options; + } + + /** + * function copied from Composer\Util\StreamContextFactory::initOptions + * + * Any changes should be applied there as well, or backported here. + * + * @param string $url URL the context is to be used for + * @return resource Default context + * @throws \RuntimeException if https proxy required and OpenSSL uninstalled + */ + protected function getMergedStreamContext($url) + { + $options = $this->options; + + // Handle HTTP_PROXY/http_proxy on CLI only for security reasons + if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { + $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); + } + + // Prefer CGI_HTTP_PROXY if available + if (!empty($_SERVER['CGI_HTTP_PROXY'])) { + $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']); + } + + // Override with HTTPS proxy if present and URL is https + if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { + $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']); + } + + // Remove proxy if URL matches no_proxy directive + if (!empty($_SERVER['NO_PROXY']) || !empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { + $pattern = new NoProxyPattern(!empty($_SERVER['no_proxy']) ? $_SERVER['no_proxy'] : $_SERVER['NO_PROXY']); + if ($pattern->test($url)) { + unset($proxy); + } + } + + if (!empty($proxy)) { + $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : ''; + $proxyURL .= isset($proxy['host']) ? $proxy['host'] : ''; + + if (isset($proxy['port'])) { + $proxyURL .= ":" . $proxy['port']; + } elseif (strpos($proxyURL, 'http://') === 0) { + $proxyURL .= ":80"; + } elseif (strpos($proxyURL, 'https://') === 0) { + $proxyURL .= ":443"; + } + + // check for a secure proxy + if (strpos($proxyURL, 'https://') === 0) { + if (!extension_loaded('openssl')) { + throw new RuntimeException('You must enable the openssl extension to use a secure proxy.'); + } + if (strpos($url, 'https://') === 0) { + throw new RuntimeException('PHP does not support https requests through a secure proxy.'); + } + } + + // http(s):// is not supported in proxy + $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL); + + $options['http'] = array( + 'proxy' => $proxyURL, + ); + + // add request_fulluri for http requests + if ('http' === parse_url($url, PHP_URL_SCHEME)) { + $options['http']['request_fulluri'] = true; + } + + // handle proxy auth if present + if (isset($proxy['user'])) { + $auth = rawurldecode($proxy['user']); + if (isset($proxy['pass'])) { + $auth .= ':' . rawurldecode($proxy['pass']); + } + $auth = base64_encode($auth); + + $options['http']['header'] = "Proxy-Authorization: Basic {$auth}\r\n"; + } + } + + if (isset($options['http']['header'])) { + $options['http']['header'] .= "Connection: close\r\n"; + } else { + $options['http']['header'] = "Connection: close\r\n"; + } + if (extension_loaded('zlib')) { + $options['http']['header'] .= "Accept-Encoding: gzip\r\n"; + } + $options['http']['header'] .= "User-Agent: ".COMPOSER_INSTALLER."\r\n"; + $options['http']['protocol_version'] = 1.1; + $options['http']['timeout'] = 600; + + return stream_context_create($options); + } + + /** + * This method was adapted from Sslurp. + * https://github.com/EvanDotPro/Sslurp + * + * (c) Evan Coury + * + * For the full copyright and license information, please see below: + * + * Copyright (c) 2013, Evan Coury + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + public static function getSystemCaRootBundlePath() + { + if (self::$caPath !== null) { + return self::$caPath; + } + + // If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that. + // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. + $envCertFile = getenv('SSL_CERT_FILE'); + if ($envCertFile && is_readable($envCertFile) && validateCaFile(file_get_contents($envCertFile))) { + return self::$caPath = $envCertFile; + } + + // If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that. + // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. + $envCertDir = getenv('SSL_CERT_DIR'); + if ($envCertDir && is_dir($envCertDir) && is_readable($envCertDir)) { + return self::$caPath = $envCertDir; + } + + $configured = ini_get('openssl.cafile'); + if ($configured && strlen($configured) > 0 && is_readable($configured) && validateCaFile(file_get_contents($configured))) { + return self::$caPath = $configured; + } + + $configured = ini_get('openssl.capath'); + if ($configured && is_dir($configured) && is_readable($configured)) { + return self::$caPath = $configured; + } + + $caBundlePaths = array( + '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) + '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) + '/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package) + '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package) + '/usr/ssl/certs/ca-bundle.crt', // Cygwin + '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package + '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option) + '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? + '/etc/ssl/cert.pem', // OpenBSD + '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x + '/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package + '/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package + '/opt/homebrew/etc/openssl@3/cert.pem', // macOS silicon homebrew, openssl@3 package + '/opt/homebrew/etc/openssl@1.1/cert.pem', // macOS silicon homebrew, openssl@1.1 package + ); + + foreach ($caBundlePaths as $caBundle) { + if (@is_readable($caBundle) && validateCaFile(file_get_contents($caBundle))) { + return self::$caPath = $caBundle; + } + } + + foreach ($caBundlePaths as $caBundle) { + $caBundle = dirname($caBundle); + if (is_dir($caBundle) && glob($caBundle.'/*')) { + return self::$caPath = $caBundle; + } + } + + return self::$caPath = false; + } + + public static function getPackagedCaFile() + { + return <<=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 +} diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..5890806 --- /dev/null +++ b/css/style.css @@ -0,0 +1,3412 @@ +:root { + /* Brand Colors */ + --salem: #0c814a; + --spring-bud: #cddf96; + --palm-leaf: #768b46; + --pine-tree: #252a16; + --raisin-black: #231f20; + --cultured: #f5f5f5; + + /* Typography */ + --font-primary: 'Tajawal', sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-primary); + line-height: 1.6; + color: var(--raisin-black); + direction: rtl; +} + +.container { + width: 90%; + max-width: 1200px; + margin: 0 auto; + padding: 0 15px; +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 12px 30px; + border-radius: 5px; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn.primary { + background-color: var(--salem); + color: white; +} + +.btn.primary:hover { + background-color: var(--palm-leaf); +} + +.btn.secondary { + background-color: transparent; + border: 2px solid var(--salem); + color: var(--salem); +} + +.btn.secondary:hover { + background-color: var(--salem); + color: white; +} + +/* Navigation */ +.navbar { + position: fixed; + width: 100%; + top: 0; + left: 0; + z-index: 1000; + padding: 15px 0; + transition: all 0.3s ease; +} + +.navbar.scrolled { + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); +} + +.navbar.scrolled .brand-first, +.navbar.scrolled .brand-second, +.navbar.scrolled .nav-link, +.navbar.scrolled .search-input, +.navbar.scrolled .search-btn { + color: var(--raisin-black); +} + +.navbar.scrolled .brand-first { + color: var(--salem); +} + +.navbar.scrolled .brand-second { + color: var(--palm-leaf); +} + +.navbar.scrolled .brand-name:hover .brand-first { + color: var(--palm-leaf); +} + +.navbar.scrolled .brand-name:hover .brand-second { + color: var(--salem); +} + +.navbar .container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; +} + +.logo { + flex: 0 0 auto; +} + +.logo img { + height: 50px; + width: auto; + transition: all 0.3s ease; +} + +.header-search { + flex: 0 0 auto; + max-width: 300px; + margin: 0 30px; + position: relative; +} + +.search-container { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 8px 35px; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.15); + font-family: var(--font-primary); + font-size: 14px; + color: var(--raisin-black); + transition: all 0.3s ease; + backdrop-filter: blur(5px); +} + +.search-input::placeholder { + color: rgba(35, 31, 32, 0.7); +} + +.search-input:focus { + outline: none; + background-color: rgba(255, 255, 255, 0.95); + border-color: var(--salem); + box-shadow: 0 0 0 2px rgba(12, 129, 74, 0.1); + color: var(--raisin-black); +} + +.search-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--salem); + font-size: 14px; + opacity: 0.8; +} + +.voice-search-btn { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--salem); + cursor: pointer; + padding: 0; + font-size: 14px; + transition: all 0.3s ease; + opacity: 0.8; +} + +.voice-search-btn:hover, +.search-input:focus + .search-icon { + opacity: 1; +} + +.search-results { + position: absolute; + top: calc(100% + 10px); + left: 0; + right: 0; + background: #fff; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 10px; + max-height: 400px; + overflow-y: auto; + z-index: 1000; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s; +} + +.search-results.show { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.result-item { + display: flex; + align-items: center; + padding: 12px; + border-radius: 8px; + text-decoration: none; + color: #333; + transition: all 0.2s; +} + +.result-item:hover { + background-color: #f8f9fa; + transform: translateX(-5px); +} + +.result-item img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 8px; + margin-left: 15px; +} + +.result-info { + flex: 1; +} + +.result-info h4 { + margin: 0 0 5px; + font-size: 15px; + color: #333; + font-weight: 500; +} + +.result-info p { + margin: 0; + font-size: 13px; + color: #666; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.result-info .price { + display: block; + margin-top: 5px; + color: #2ecc71; + font-weight: 600; + font-size: 14px; +} + +.search-filters { + position: absolute; + top: calc(100% + 5px); + left: 0; + right: 0; + background: #fff; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 15px; + z-index: 999; + display: none; +} + +.search-container:focus-within .search-filters { + display: block; +} + +.nav-link { + color: var(--raisin-black); + font-weight: 500; + transition: color 0.3s ease; +} + +.brand-first, +.brand-second { + transition: color 0.3s ease; +} + +.navbar:not(.scrolled) .brand-first { + color: var(--salem); +} + +.navbar:not(.scrolled) .brand-second { + color: var(--palm-leaf); +} + +.navbar.scrolled .brand-first { + color: var(--salem); +} + +.navbar.scrolled .brand-second { + color: var(--palm-leaf); +} + +.navbar.scrolled .nav-link { + color: var(--raisin-black); +} + +.brand-name:hover .brand-first { + color: var(--palm-leaf); +} + +.brand-name:hover .brand-second { + color: var(--salem); +} + +/* Navigation */ +.nav-links { + flex: 0 0 auto; + display: flex; + gap: 30px; + align-items: center; +} + +.nav-links a, +.nav-links .dropdown-toggle { + color: #fff; + text-decoration: none; + font-weight: 500; + font-size: 15px; + transition: all 0.3s ease; + position: relative; + padding: 5px 0; +} + +.navbar.scrolled .nav-links a, +.navbar.scrolled .nav-links .dropdown-toggle { + color: var(--raisin-black); +} + +.nav-links a:hover, +.nav-links a.active, +.nav-links .dropdown:hover .dropdown-toggle { + color: var(--spring-bud); +} + +.navbar.scrolled .nav-links a:hover, +.navbar.scrolled .nav-links a.active, +.navbar.scrolled .nav-links .dropdown:hover .dropdown-toggle { + color: var(--salem); +} + +.nav-links a::after, +.nav-links .dropdown-toggle::after { + content: ''; + position: absolute; + width: 0; + height: 2px; + bottom: 0; + right: 0; + background-color: var(--spring-bud); + transition: width 0.3s ease; +} + +.navbar.scrolled .nav-links a::after, +.navbar.scrolled .nav-links .dropdown-toggle::after { + background-color: var(--salem); +} + +.nav-links a:hover::after, +.nav-links a.active::after, +.nav-links .dropdown:hover .dropdown-toggle::after { + width: 100%; +} + +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-toggle { + position: relative; + padding-left: 15px !important; +} + +.dropdown-toggle::after { + content: '\f107'; + font-family: 'Font Awesome 5 Free'; + font-weight: 900; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + transition: transform 0.3s ease; +} + +.dropdown:hover .dropdown-toggle::after { + transform: translateY(-50%) rotate(180deg); +} + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + min-width: 220px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 8px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + padding: 8px 0; + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all 0.3s ease; + z-index: 100; + border: 1px solid rgba(12, 129, 74, 0.1); +} + +.navbar.scrolled .dropdown-menu { + background: #fff; +} + +.dropdown:hover .dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.dropdown-menu li { + list-style: none; +} + +.dropdown-menu li a { + display: block; + padding: 8px 20px; + color: var(--raisin-black); + font-size: 14px; + transition: all 0.2s ease; +} + +.dropdown-menu li a:hover { + background-color: rgba(12, 129, 74, 0.05); + color: var(--salem); + padding-right: 25px; +} + +@media (max-width: 768px) { + .dropdown-menu { + position: static; + background: transparent; + box-shadow: none; + border: none; + padding-right: 15px; + min-width: auto; + opacity: 1; + visibility: visible; + transform: none; + } + + .dropdown-menu li a { + padding: 5px 15px; + font-size: 13px; + } + + .nav-links { + gap: 20px; + } + + .nav-links a, + .nav-links .dropdown-toggle { + font-size: 14px; + } +} + +/* Advanced Search Styles */ +.search-results { + position: absolute; + top: calc(100% + 10px); + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + display: none; + z-index: 1000; + backdrop-filter: blur(10px); + border: 1px solid rgba(12, 129, 74, 0.1); + max-height: 500px; + overflow-y: auto; +} + +.search-results.active { + display: block; +} + +.search-filters { + padding: 15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} + +.filter-group { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.filter-group:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.filter-group h3 { + margin: 0 0 10px; + font-size: 14px; + color: #666; + font-weight: 500; +} + +.category-filters { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.category-filter { + display: flex; + align-items: center; +} + +.category-filter input[type="checkbox"] { + margin-left: 6px; +} + +.price-range { + display: flex; + gap: 10px; +} + +.price-range input { + width: 100px; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.sort-by { + width: 200px; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; +} + +.search-results-list { + padding: 10px 0; +} + +.result-item { + padding: 10px; + border-radius: 8px; + text-decoration: none; + color: #333; + transition: all 0.2s; +} + +.result-item:hover { + background-color: #f8f9fa; + transform: translateX(-5px); +} + +.result-item img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 8px; + margin-left: 15px; +} + +.result-info { + flex: 1; +} + +.result-info h4 { + margin: 0 0 5px; + font-size: 15px; + color: #333; + font-weight: 500; +} + +.result-info p { + margin: 0; + font-size: 13px; + color: #666; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.result-info .price { + display: block; + margin-top: 5px; + color: #2ecc71; + font-weight: 600; + font-size: 14px; +} + +.search-filters { + position: absolute; + top: calc(100% + 5px); + left: 0; + right: 0; + background: #fff; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 15px; + z-index: 999; + display: none; +} + +.search-container:focus-within .search-filters { + display: block; +} + +/* Hero Section */ +.hero { + position: relative; + height: 100vh; + background-image: linear-gradient(rgba(12, 129, 74, 0.3), rgba(37, 42, 22, 0.6)), url('../images/1.jpg'); + background-size: cover; + background-position: center; + display: flex; + align-items: center; + text-align: center; + color: white; + margin-top: 0; +} + +.hero-content { + position: relative; + z-index: 2; + max-width: 800px; + margin: 0 auto; + padding: 0 20px; +} + +.hero-content h1 { + font-size: 4rem; + font-weight: 700; + margin-bottom: 20px; + line-height: 1.2; +} + +.hero-content p { + font-size: 1.3rem; + margin-bottom: 40px; + line-height: 1.6; +} + +.hero-buttons { + display: flex; + gap: 20px; + justify-content: center; +} + +/* Features Section */ +.features { + padding: 80px 0; + background-color: var(--cultured); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 30px; + text-align: center; +} + +.feature-card { + padding: 30px; + background: white; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); + transition: transform 0.3s ease; +} + +.feature-card:hover { + transform: translateY(-5px); +} + +.feature-card i { + font-size: 40px; + color: var(--salem); + margin-bottom: 20px; +} + +/* Section Headers */ +.section-header { + text-align: center; + margin-bottom: 50px; +} + +.section-header h2 { + font-size: 2.5rem; + color: var(--salem); + margin-bottom: 10px; +} + +.section-header p { + color: var(--raisin-black); + font-size: 1.1rem; +} + +/* About Section */ +.about { + padding: 100px 0; + background-color: var(--cultured); + position: relative; + overflow: hidden; +} + +.about-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 50px; + align-items: center; +} + +.about-text { + font-size: 1.1rem; + line-height: 1.8; +} + +.about-features { + list-style: none; + margin-top: 30px; +} + +.about-features li { + margin-bottom: 15px; + padding-right: 30px; + position: relative; + font-weight: 500; +} + +.about-features li:before { + content: "✓"; + color: var(--salem); + position: absolute; + right: 0; + font-weight: bold; +} + +.about-image { + position: relative; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +} + +.about-image img { + width: 100%; + height: 400px; + object-fit: cover; + transition: transform 0.5s ease; +} + +.about-image:hover img { + transform: scale(1.05); +} + +/* Products Section */ +.products { + padding: 80px 0; + background-color: var(--cultured); +} + +.products-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 30px; +} + +.product-card { + background: white; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 5px 20px rgba(0,0,0,0.1); + transition: all 0.3s ease; + position: relative; +} + +.product-card:hover { + transform: translateY(-10px); + box-shadow: 0 10px 30px rgba(12, 129, 74, 0.2); +} + +.product-image { + position: relative; + overflow: hidden; + height: 300px; +} + +.product-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s ease; +} + +.product-card:hover .product-image img { + transform: scale(1.1); +} + +.product-info { + padding: 25px; + text-align: center; + background: white; + position: relative; +} + +.product-info h3 { + color: var(--salem); + font-size: 1.5rem; + margin-bottom: 15px; +} + +.product-features { + display: flex; + justify-content: center; + gap: 15px; + margin: 15px 0; +} + +.product-features span { + background-color: var(--spring-bud); + color: var(--salem); + padding: 5px 15px; + border-radius: 20px; + font-size: 0.9rem; +} + +/* Product Category Pages */ +.product-category-hero { + background-color: var(--salem); + color: white; + padding: 60px 0; + text-align: center; + margin-bottom: 40px; +} + +.product-category-hero h1 { + font-size: 2.5rem; + margin-bottom: 20px; +} + +.product-category-hero p { + font-size: 1.2rem; + max-width: 800px; + margin: 0 auto; +} + +.products-filter { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + flex-wrap: wrap; + gap: 20px; +} + +.search-box { + position: relative; + flex: 1; + max-width: 400px; +} + +.search-box input { + width: 100%; + padding: 12px 40px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.search-box i { + position: absolute; + left: 15px; + top: 50%; + transform: translateY(-50%); + color: #666; +} + +.sort-options select { + padding: 12px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; + min-width: 200px; +} + +.products-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 30px; + margin-bottom: 40px; +} + +.product-card { + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 15px rgba(0,0,0,0.1); + transition: transform 0.3s ease; +} + +.product-card:hover { + transform: translateY(-5px); +} + +.product-image { + height: 200px; + overflow: hidden; +} + +.product-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.product-info { + padding: 20px; +} + +.product-info h3 { + margin: 0 0 10px; + font-size: 1.2rem; +} + +.product-info p { + color: #666; + margin-bottom: 15px; + line-height: 1.5; +} + +.product-price { + font-size: 1.25rem; + font-weight: bold; + color: var(--salem); + margin-bottom: 15px; +} + +/* Benefits Section */ +.benefits { + padding: 80px 0; +} + +.benefits-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 30px; + text-align: center; +} + +.benefit-card { + padding: 30px; + border-radius: 10px; + background: white; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.benefit-card i { + font-size: 40px; + color: var(--salem); + margin-bottom: 20px; +} + +/* Contact Section */ +.contact { + padding: 80px 0; + background-color: var(--cultured); +} + +.contact-info { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 30px; + text-align: center; +} + +.info-item { + padding: 30px; + background: white; + border-radius: 10px; + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.info-item i { + font-size: 30px; + color: var(--salem); + margin-bottom: 15px; +} + +/* Dropdown Menu Styles */ +.dropdown { + position: relative; +} + +.dropdown-toggle { + display: flex; + align-items: center; + gap: 5px; +} + +.dropdown-toggle i { + font-size: 12px; + transition: transform 0.3s ease; +} + +.dropdown:hover .dropdown-toggle i { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + top: 100%; + right: 0; + background-color: #fff; + min-width: 200px; + border-radius: 8px; + box-shadow: 0 2px 15px rgba(0,0,0,0.1); + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all 0.3s ease; + z-index: 1000; + padding: 10px 0; +} + +.dropdown:hover .dropdown-menu { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.dropdown-menu li { + display: block; + margin: 0; + padding: 0; +} + +.dropdown-menu li a { + display: block; + padding: 10px 20px; + color: #333; + transition: all 0.3s ease; + font-size: 14px; +} + +.dropdown-menu li a:hover { + background-color: #f8f9fa; + color: #0c814a; + padding-right: 25px; +} + +/* Mobile Responsive Dropdown */ +@media (max-width: 768px) { + .dropdown-menu { + position: static; + background-color: #f8f9fa; + box-shadow: none; + opacity: 1; + visibility: visible; + transform: none; + display: none; + } + + .dropdown.active .dropdown-menu { + display: block; + } + + .dropdown-menu li a { + padding: 10px 30px; + } +} + +/* Footer */ +footer { + background-color: var(--pine-tree); + color: white; + padding: 60px 0 20px; +} + +.footer-content { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 50px; + margin-bottom: 40px; +} + +.footer-about img { + height: 40px; + margin-bottom: 20px; +} + +.footer-logo { + height: 50px; + width: auto; + filter: brightness(0) invert(1); + margin-bottom: 20px; +} + +.footer-links ul { + list-style: none; +} + +.footer-links li { + margin-bottom: 10px; +} + +.footer-links a { + color: white; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-links a:hover { + color: var(--spring-bud); +} + +.social-links { + display: flex; + gap: 15px; +} + +.social-links a { + color: white; + font-size: 20px; + transition: color 0.3s ease; +} + +.social-links a:hover { + color: var(--spring-bud); +} + +.footer-bottom { + text-align: center; + padding-top: 20px; + border-top: 1px solid rgba(255,255,255,0.1); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .nav-links { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: white; + padding: 20px 0; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + flex-direction: column; + gap: 15px; + text-align: center; + } + + .nav-links.active { + display: flex; + } + + .nav-links a { + color: var(--raisin-black); + } + + .mobile-menu { + display: block; + color: var(--raisin-black); + } + + .navbar { + background-color: rgba(255, 255, 255, 0.95); + padding: 15px 0; + } + + .hero-content h1 { + font-size: 2.5rem; + } + + .hero-content p { + font-size: 1.1rem; + } + + .about-content { + grid-template-columns: 1fr; + } + + .about-image { + order: -1; + } + + .product-image { + height: 250px; + } + + .about-image img { + height: 300px; + } + + .footer-content { + grid-template-columns: 1fr; + text-align: center; + } + + .social-links { + justify-content: center; + } +} + +/* قسم المنتجات المميزة */ +.featured-section { + padding: 60px 0; + background-color: #f9f9f9; +} + +.featured-products { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 30px; + margin-top: 40px; +} + +.product-card { + background: #fff; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: transform 0.3s ease; +} + +.product-card:hover { + transform: translateY(-5px); +} + +.product-image { + position: relative; + height: 200px; + overflow: hidden; +} + +.product-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.badge { + position: absolute; + top: 10px; + right: 10px; + background: #cddf96; + color: #333; + padding: 5px 10px; + border-radius: 4px; + font-size: 0.8em; +} + +.product-info { + padding: 20px; + text-align: center; +} + +.product-info h3 { + margin: 0 0 10px; + font-size: 1.2em; + color: #333; +} + +.product-info p { + color: #666; + margin-bottom: 15px; + line-height: 1.5; +} + +/* تحسينات الفوتر */ +.footer-logo { + width: 120px; + margin-bottom: 15px; +} + +.footer-about p { + line-height: 1.6; + margin-bottom: 20px; +} + +.footer-links h4, +.footer-social h4 { + color: #cddf96; + margin-bottom: 20px; +} + +.footer-links ul { + list-style: none; + padding: 0; +} + +.footer-links ul li { + margin-bottom: 10px; +} + +.footer-links ul li a { + color: #fff; + text-decoration: none; + transition: color 0.3s ease; +} + +.footer-links ul li a:hover { + color: #cddf96; +} + +.social-links { + display: flex; + gap: 15px; +} + +.social-links a { + color: #fff; + font-size: 20px; + transition: color 0.3s ease; +} + +.social-links a:hover { + color: #cddf96; +} + +/* تحسينات عامة */ +.btn { + background: #cddf96; + color: #333; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn:hover { + background: #bcd278; + transform: translateY(-2px); +} + +/* قسم الشهادات */ +.testimonials-section { + padding: 80px 0; + background-color: #fff; +} + +.testimonials-slider { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 30px; + margin-top: 40px; +} + +.testimonial-card { + background: #f9f9f9; + border-radius: 15px; + padding: 30px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease; +} + +.testimonial-card:hover { + transform: translateY(-5px); +} + +.rating { + color: #ffd700; + margin-bottom: 15px; +} + +.testimonial-content p { + font-size: 1.1em; + line-height: 1.6; + color: #555; + margin-bottom: 20px; +} + +.testimonial-author { + display: flex; + align-items: center; + gap: 15px; +} + +.testimonial-author img { + width: 60px; + height: 60px; + border-radius: 50%; + object-fit: cover; +} + +.author-info h4 { + margin: 0; + color: #333; + font-size: 1.1em; +} + +.author-info span { + color: #666; + font-size: 0.9em; +} + +/* قسم النشرة البريدية */ +.newsletter-section { + padding: 60px 0; + background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), url('../images/newsletter-bg.jpg'); + background-size: cover; + background-position: center; + color: #fff; + text-align: center; +} + +.newsletter-content { + max-width: 600px; + margin: 0 auto; +} + +.newsletter-content h2 { + margin-bottom: 20px; + font-size: 2em; +} + +.newsletter-content p { + margin-bottom: 30px; + opacity: 0.9; +} + +.newsletter-form .form-group { + display: flex; + gap: 10px; + max-width: 500px; + margin: 0 auto; +} + +.newsletter-form input[type="email"] { + flex: 1; + padding: 12px 20px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.newsletter-form .btn { + padding: 12px 30px; + background: #cddf96; + color: #333; + border: none; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; +} + +.newsletter-form .btn:hover { + background: #bcd278; + transform: translateY(-2px); +} + +@media (max-width: 768px) { + .newsletter-form .form-group { + flex-direction: column; + } + + .newsletter-form .btn { + width: 100%; + } +} + +/* صفحة الأسئلة الشائعة */ +.faq-section { + padding: 80px 0; + background-color: #f9f9f9; +} + +.faq-container { + max-width: 800px; + margin: 40px auto; +} + +.faq-item { + background: #fff; + border-radius: 8px; + margin-bottom: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.faq-question { + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.faq-question:hover { + background-color: #f5f5f5; +} + +.faq-question h3 { + margin: 0; + font-size: 1.1em; + color: #333; +} + +.faq-question i { + transition: transform 0.3s ease; +} + +.faq-item.active .faq-question i { + transform: rotate(180deg); +} + +.faq-answer { + padding: 0 20px; + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, padding 0.3s ease; +} + +.faq-item.active .faq-answer { + padding: 20px; + max-height: 1000px; +} + +.faq-answer p { + margin: 0; + line-height: 1.6; + color: #666; +} + +/* صفحة البحث */ +.search-section { + padding: 60px 0; + background-color: #f9f9f9; +} + +.search-container { + max-width: 1200px; + margin: 0 auto; +} + +.search-form { + background: #fff; + padding: 20px; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; +} + +.search-row { + margin-bottom: 20px; +} + +.search-input { + position: relative; +} + +.search-input input { + width: 100%; + padding: 15px 50px; + padding-left: 15px; + border: 2px solid #eee; + border-radius: 5px; + font-size: 16px; +} + +.search-btn { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: #666; + cursor: pointer; +} + +.filters { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.filter-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.filter-group label { + font-weight: bold; + color: #333; +} + +.filter-group select, +.filter-group input { + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.price-range { + display: flex; + align-items: center; + gap: 10px; +} + +.price-range input { + width: 100px; +} + +/* صفحة المدونة */ +.blog-section { + padding: 80px 0; + background-color: #fff; +} + +.blog-filters { + margin-bottom: 40px; + text-align: center; +} + +.categories { + margin-top: 20px; +} + +.category-btn { + padding: 8px 20px; + margin: 0 5px; + border: none; + border-radius: 20px; + background: #f0f0f0; + color: #666; + cursor: pointer; + transition: all 0.3s ease; +} + +.category-btn.active, +.category-btn:hover { + background: #cddf96; + color: #333; +} + +.blog-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 30px; + margin-bottom: 40px; +} + +.blog-card { + background: #fff; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 3px 15px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +.blog-card:hover { + transform: translateY(-5px); +} + +.blog-image { + position: relative; + height: 200px; +} + +.blog-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.blog-image .category { + position: absolute; + top: 15px; + right: 15px; + background: rgba(205, 223, 150, 0.9); + padding: 5px 15px; + border-radius: 20px; + color: #333; + font-size: 0.9em; +} + +.blog-content { + padding: 20px; +} + +.blog-content h2 { + margin: 0 0 15px; + font-size: 1.3em; + color: #333; +} + +.blog-content p { + color: #666; + line-height: 1.6; + margin-bottom: 20px; +} + +.blog-meta { + display: flex; + gap: 20px; + color: #888; + font-size: 0.9em; + margin-bottom: 15px; +} + +.blog-meta i { + margin-left: 5px; +} + +.read-more { + display: inline-block; + color: #333; + text-decoration: none; + font-weight: bold; + transition: color 0.3s ease; +} + +.read-more:hover { + color: #cddf96; +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; +} + +.pagination button { + background: none; + border: none; + cursor: pointer; + color: #666; +} + +.pagination button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.current-page { + font-weight: bold; + color: #333; +} + +/* نظام المقارنة */ +.comparison-tool { + margin-top: 30px; + padding: 20px; + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.comparison-list { + display: flex; + gap: 15px; + margin: 15px 0; + min-height: 100px; + padding: 10px; + background: #f9f9f9; + border-radius: 5px; +} + +.comparison-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + background: #fff; + border-radius: 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.comparison-item img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 5px; +} + +.comparison-item button { + background: none; + border: none; + color: #666; + cursor: pointer; +} + +.comparison-item button:hover { + color: #ff4444; +} + +/* تحسينات SEO */ +.meta-tags { + display: none; +} + +/* تحسينات عامة */ +@media (max-width: 768px) { + .filters { + grid-template-columns: 1fr; + } + + .blog-grid { + grid-template-columns: 1fr; + } + + .comparison-list { + flex-direction: column; + } +} + +/* نظام التعليقات */ +.comments-section { + margin-top: 40px; + padding: 20px; + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.comment-form { + margin-bottom: 30px; +} + +.comment-form textarea { + width: 100%; + padding: 15px; + border: 1px solid #ddd; + border-radius: 5px; + resize: vertical; + min-height: 100px; + margin-bottom: 15px; +} + +.comment { + padding: 20px; + border-bottom: 1px solid #eee; + margin-bottom: 20px; +} + +.comment:last-child { + border-bottom: none; +} + +.comment-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; +} + +.comment-info h4 { + margin: 0; + color: #333; +} + +.comment-date { + color: #888; + font-size: 0.9em; +} + +.comment-content { + line-height: 1.6; + color: #555; + margin-bottom: 15px; +} + +.comment-actions { + display: flex; + gap: 20px; +} + +.like-btn, +.reply-btn { + background: none; + border: none; + color: #666; + cursor: pointer; + display: flex; + align-items: center; + gap: 5px; +} + +.like-btn.liked { + color: #ff4444; +} + +.reply-form { + margin-top: 15px; + padding-right: 55px; + display: none; +} + +.reply-form.active { + display: block; +} + +/* سلة المشتريات */ +.cart-container { + position: relative; +} + +.cart-icon { + position: relative; + cursor: pointer; +} + +.cart-count { + position: absolute; + top: -8px; + right: -8px; + background: #ff4444; + color: white; + font-size: 0.8em; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.cart-dropdown { + position: absolute; + top: 100%; + left: 0; + width: 300px; + background: #fff; + border-radius: 10px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + padding: 15px; + display: none; + z-index: 1000; +} + +.cart-dropdown.active { + display: block; +} + +.cart-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 0; + border-bottom: 1px solid #eee; +} + +.cart-item:last-child { + border-bottom: none; +} + +.cart-item img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 5px; +} + +.item-details { + flex: 1; +} + +.item-details h4 { + margin: 0 0 5px; + font-size: 1em; + color: #333; +} + +.item-price { + color: #666; + font-size: 0.9em; +} + +.quantity-controls { + display: flex; + align-items: center; + gap: 10px; + margin-top: 5px; +} + +.quantity-controls button { + width: 24px; + height: 24px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; + cursor: pointer; +} + +.remove-item { + background: none; + border: none; + color: #666; + cursor: pointer; + padding: 5px; +} + +.remove-item:hover { + color: #ff4444; +} + +.cart-total { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + text-align: left; + font-weight: bold; +} + +.empty-cart { + text-align: center; + color: #666; + padding: 20px 0; +} + +.cart-notification { + position: fixed; + bottom: 20px; + left: 20px; + background: #cddf96; + color: #333; + padding: 10px 20px; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transform: translateY(100px); + opacity: 0; + transition: all 0.3s ease; +} + +.cart-notification.show { + transform: translateY(0); + opacity: 1; +} + +/* تحسينات عامة */ +@media (max-width: 768px) { + .cart-dropdown { + width: 100%; + position: fixed; + top: auto; + bottom: 0; + left: 0; + border-radius: 10px 10px 0 0; + max-height: 80vh; + overflow-y: auto; + } + + .reply-form { + padding-right: 20px; + } +} + +/* نظام البحث الصوتي */ +.voice-search-btn { + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: #666; + transition: color 0.3s; +} + +.voice-search-btn.listening { + color: #e74c3c; + animation: pulse 1.5s infinite; +} + +.voice-notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 24px; + background: #333; + color: white; + border-radius: 4px; + opacity: 0; + transform: translateY(20px); + transition: all 0.3s; + z-index: 1000; +} + +.voice-notification.show { + opacity: 1; + transform: translateY(0); +} + +.voice-notification.error { + background: #e74c3c; +} + +/* نظام المشاركة */ +.share-menu { + position: absolute; + background: white; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + border-radius: 8px; + padding: 8px; + z-index: 1000; +} + +.share-option { + display: flex; + align-items: center; + padding: 8px 16px; + border: none; + background: none; + width: 100%; + cursor: pointer; + transition: background-color 0.3s; +} + +.share-option:hover { + background-color: #f5f5f5; +} + +.share-option i { + margin-left: 12px; + font-size: 18px; +} + +.share-notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 24px; + border-radius: 4px; + color: white; + opacity: 0; + transform: translateY(20px); + transition: all 0.3s; + z-index: 1000; +} + +.share-notification.success { + background: #2ecc71; +} + +.share-notification.error { + background: #e74c3c; +} + +.share-notification.show { + opacity: 1; + transform: translateY(0); +} + +/* نظام المراجعات */ +.review-card { + background: white; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.review-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.reviewer-info { + display: flex; + align-items: center; +} + +.reviewer-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + margin-left: 12px; +} + +.review-rating { + color: #f1c40f; + margin: 4px 0; +} + +.review-date { + color: #666; + font-size: 0.9em; +} + +.verified-badge { + display: inline-flex; + align-items: center; + color: #27ae60; + font-size: 0.9em; +} + +.verified-badge i { + margin-left: 4px; +} + +.review-content { + margin: 12px 0; + line-height: 1.6; +} + +.review-images { + display: flex; + gap: 8px; + margin-top: 12px; + overflow-x: auto; + padding-bottom: 8px; +} + +.review-images img { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; + cursor: pointer; +} + +.review-actions { + display: flex; + gap: 12px; + margin-top: 12px; +} + +.helpful-btn, +.report-btn { + display: flex; + align-items: center; + padding: 6px 12px; + border: 1px solid #ddd; + border-radius: 4px; + background: none; + cursor: pointer; + transition: all 0.3s; +} + +.helpful-btn:hover { + background: #f5f5f5; +} + +.report-btn:hover { + color: #e74c3c; + border-color: #e74c3c; +} + +.helpful-count { + margin-right: 8px; +} + +.review-filters { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.review-filter { + padding: 6px 12px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; + color: #666; + cursor: pointer; + transition: all 0.3s; +} + +.review-filter.active { + background: #2ecc71; + color: white; + border-color: #2ecc71; +} + +.review-sort { + margin-bottom: 16px; +} + +.image-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + position: relative; + max-width: 90%; + max-height: 90%; +} + +.modal-content img { + max-width: 100%; + max-height: 90vh; + border-radius: 4px; +} + +.close-modal { + position: absolute; + top: -40px; + right: 0; + color: white; + background: none; + border: none; + font-size: 24px; + cursor: pointer; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} + +/* تحديث أنماط قسم المراجعات */ +.reviews-section { + padding: 40px; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin: 20px 0; + text-align: right; +} + +.reviews-section h2 { + color: #333; + margin-bottom: 30px; + font-size: 24px; +} + +.review-summary { + display: flex; + justify-content: space-between; + align-items: start; + margin-bottom: 30px; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; +} + +.average-rating { + text-align: center; +} + +.average-rating h3 { + margin-bottom: 15px; + color: #333; +} + +.rating-value { + font-size: 48px; + font-weight: bold; + color: #2ecc71; + display: block; + margin: 10px 0; +} + +.total-reviews { + color: #666; + font-size: 14px; +} + +.rating-breakdown { + flex: 1; + margin-right: 40px; +} + +.rating-bar { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.rating-bar span { + min-width: 60px; + color: #666; +} + +.rating-bar .progress { + flex: 1; + height: 8px; + background: #eee; + border-radius: 4px; + margin: 0 10px; + overflow: hidden; +} + +.rating-bar .progress-bar { + height: 100%; + background: #2ecc71; + border-radius: 4px; + transition: width 0.3s ease; +} + +.review-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 15px; +} + +.review-filters { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.review-filter { + padding: 8px 16px; + border: 1px solid #ddd; + border-radius: 20px; + background: #fff; + color: #666; + cursor: pointer; + transition: all 0.3s; + font-size: 14px; +} + +.review-filter:hover { + border-color: #2ecc71; + color: #2ecc71; +} + +.review-filter.active { + background: #2ecc71; + color: white; + border-color: #2ecc71; +} + +.review-sort { + padding: 8px 16px; + border: 1px solid #ddd; + border-radius: 20px; + background: #fff; + color: #666; + cursor: pointer; + outline: none; + font-size: 14px; +} + +.review-form { + margin-top: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 8px; +} + +.review-form h3 { + margin-bottom: 20px; + color: #333; +} + +.rating-input-container { + margin-bottom: 20px; +} + +.rating-input-container label { + display: block; + margin-bottom: 10px; + color: #666; +} + +.rating-stars { + display: flex; + gap: 5px; + direction: ltr; +} + +.rating-stars i { + font-size: 24px; + color: #ffd700; + cursor: pointer; + transition: all 0.2s; +} + +.rating-stars i:hover { + transform: scale(1.2); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 10px; + color: #666; +} + +.form-group textarea { + width: 100%; + min-height: 120px; + padding: 12px; + border: 1px solid #ddd; + border-radius: 8px; + resize: vertical; + font-family: inherit; +} + +.form-group input[type="file"] { + display: block; + width: 100%; + padding: 8px; + border: 1px dashed #ddd; + border-radius: 8px; + background: #fff; +} + +.submit-review { + background: #2ecc71; + color: white; + padding: 12px 24px; + border: none; + border-radius: 20px; + cursor: pointer; + transition: background-color 0.3s; + font-size: 16px; +} + +.submit-review:hover { + background: #27ae60; +} + +/* تحسينات للتجاوب مع الشاشات الصغيرة */ +@media (max-width: 768px) { + .review-summary { + flex-direction: column; + gap: 20px; + } + + .rating-breakdown { + margin-right: 0; + width: 100%; + } + + .review-controls { + flex-direction: column; + align-items: stretch; + } + + .review-filters { + justify-content: center; + } + + .review-sort { + width: 100%; + } +} + +/* نظام البحث */ +.search-section { + padding: 20px; + background: #fff; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 30px; +} + +.search-container { + position: relative; + max-width: 800px; + margin: 0 auto; +} + +.search-input { + width: 100%; + padding: 15px 20px; + padding-left: 50px; + border: 2px solid #eee; + border-radius: 30px; + font-size: 16px; +} + +.search-input:focus { + outline: none; + border-color: #2ecc71; + box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.1); + background-color: #fff; +} + +.search-icon { + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + color: #666; +} + +.search-results { + position: absolute; + top: calc(100% + 10px); + left: 0; + right: 0; + background: #fff; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + margin-top: 10px; + max-height: 400px; + overflow-y: auto; + z-index: 1000; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s; +} + +.search-results.show { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.results-list { + padding: 10px; +} + +.result-item { + display: flex; + padding: 10px; + border-bottom: 1px solid #eee; + text-decoration: none; + color: inherit; + transition: all 0.2s; +} + +.result-item:last-child { + border-bottom: none; +} + +.result-item:hover { + background-color: #f8f9fa; +} + +.result-item img { + width: 60px; + height: 60px; + object-fit: cover; + border-radius: 4px; + margin-left: 15px; +} + +.result-info { + flex: 1; +} + +.result-info h4 { + margin: 0 0 5px; + font-size: 16px; + color: #333; +} + +.result-info p { + margin: 0; + font-size: 14px; + color: #666; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.result-info .price { + display: block; + margin-top: 5px; + color: #2ecc71; + font-weight: bold; + font-size: 14px; +} + +.no-results, +.search-error { + padding: 30px; + text-align: center; + color: #666; +} + +.no-results i, +.search-error i { + font-size: 24px; + margin-bottom: 10px; + display: block; +} + +.search-error i { + color: #e74c3c; +} + +mark { + background: #fff3cd; + padding: 0 2px; + border-radius: 2px; +} + +/* فلاتر البحث */ +.search-filters { + margin-top: 20px; + padding: 20px; + background: #f8f9fa; + border-radius: 8px; +} + +.filter-group { + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; +} + +.filter-group:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.filter-group h3 { + margin: 0 0 10px; + font-size: 16px; + color: #666; + font-weight: 500; +} + +.category-filters { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.category-filter { + display: flex; + align-items: center; +} + +.category-filter input[type="checkbox"] { + margin-left: 8px; +} + +.price-range { + display: flex; + gap: 10px; +} + +.price-range input { + width: 100px; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.sort-by { + width: 200px; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; +} + +/* تحسينات للشاشات الصغيرة */ +@media (max-width: 768px) { + .search-section { + padding: 15px; + } + + .search-input { + font-size: 14px; + padding: 12px 15px; + padding-left: 40px; + } + + .result-item { + padding: 10px; + } + + .result-item img { + width: 50px; + height: 50px; + } + + .result-info h4 { + font-size: 14px; + } + + .result-info p { + font-size: 12px; + } + + .result-info .price { + font-size: 13px; + } + + .price-range { + flex-direction: column; + } + + .price-range input { + width: 100%; + } + + .sort-by { + width: 100%; + } +} + +/* تحديث تنسيق البحث في الهيدر */ +.header-search { + flex: 0 0 auto; + max-width: 300px; + margin: 0 30px; + position: relative; +} + +.search-container { + position: relative; + width: 100%; +} + +.search-input { + width: 100%; + padding: 8px 35px; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.15); + font-family: var(--font-primary); + font-size: 14px; + color: var(--raisin-black); + transition: all 0.3s ease; + backdrop-filter: blur(5px); +} + +.search-input::placeholder { + color: rgba(35, 31, 32, 0.7); +} + +.search-input:focus { + outline: none; + background-color: rgba(255, 255, 255, 0.95); + border-color: var(--salem); + box-shadow: 0 0 0 2px rgba(12, 129, 74, 0.1); + color: var(--raisin-black); +} + +.search-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--salem); + font-size: 14px; + opacity: 0.8; +} + +.voice-search-btn { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--salem); + cursor: pointer; + padding: 0; + font-size: 14px; + transition: all 0.3s ease; + opacity: 0.8; +} + +.voice-search-btn:hover, +.search-input:focus + .search-icon { + opacity: 1; +} + +.search-results { + position: absolute; + top: calc(100% + 10px); + left: 0; + right: 0; + background: #fff; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 10px; + max-height: 400px; + overflow-y: auto; + z-index: 1000; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all 0.3s; +} + +.search-results.show { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.result-item { + display: flex; + align-items: center; + padding: 12px; + border-radius: 8px; + text-decoration: none; + color: #333; + transition: all 0.2s; +} + +.result-item:hover { + background-color: #f8f9fa; + transform: translateX(-5px); +} + +.result-item img { + width: 50px; + height: 50px; + object-fit: cover; + border-radius: 8px; + margin-left: 15px; +} + +.result-info { + flex: 1; +} + +.result-info h4 { + margin: 0 0 5px; + font-size: 15px; + color: #333; + font-weight: 500; +} + +.result-info p { + margin: 0; + font-size: 13px; + color: #666; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.result-info .price { + display: block; + margin-top: 5px; + color: #2ecc71; + font-weight: 600; + font-size: 14px; +} + +.search-filters { + position: absolute; + top: calc(100% + 5px); + left: 0; + right: 0; + background: #fff; + border-radius: 15px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + padding: 15px; + z-index: 999; + display: none; +} + +.search-container:focus-within .search-filters { + display: block; +} + +.filter-group { + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.filter-group:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.filter-group h3 { + margin: 0 0 10px; + font-size: 14px; + color: #666; + font-weight: 500; +} + +.category-filters { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.category-filter { + display: flex; + align-items: center; + font-size: 13px; + color: #333; +} + +.category-filter input[type="checkbox"] { + margin-left: 6px; +} + +/* تحسينات للشاشات الصغيرة */ +@media (max-width: 768px) { + .header-search { + max-width: 100%; + margin: 10px 0; + } + + .search-input { + font-size: 14px; + padding: 10px 40px; + } + + .search-icon, + .voice-search-btn { + font-size: 16px; + } + + .result-item { + padding: 10px; + } + + .result-item img { + width: 40px; + height: 40px; + } + + .result-info h4 { + font-size: 14px; + } + + .result-info p { + font-size: 12px; + } + + .result-info .price { + font-size: 13px; + } +} + +/* تنسيق الشعار */ +.logo { + padding: 10px 0; +} + +.brand-name { + text-decoration: none; + font-size: 2rem; + font-weight: 700; + letter-spacing: 1px; + display: flex; + align-items: center; + gap: 5px; + transition: all 0.3s ease; +} + +.brand-first { + color: var(--salem); +} + +.brand-second { + color: var(--palm-leaf); +} + +.brand-name:hover .brand-first { + color: var(--palm-leaf); +} + +.brand-name:hover .brand-second { + color: var(--salem); +} + +/* تحسين حجم الشعار في الشاشات الصغيرة */ +@media (max-width: 768px) { + .brand-name { + font-size: 1.5rem; + } +} + +/* تعديل لون الشعار عند التمرير */ +.navbar.scrolled .brand-first, +.navbar.scrolled .brand-second { + color: var(--salem); +} + +.navbar.scrolled .brand-name:hover .brand-first { + color: var(--palm-leaf); +} + +.navbar.scrolled .brand-name:hover .brand-second { + color: var(--salem); +} + +/* تنسيق أزرار التنقل */ +.nav-actions { + display: flex; + align-items: center; + gap: 20px; + margin-right: 20px; +} + +.account-btn, +.cart-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 15px; + border-radius: 20px; + background: transparent; + color: var(--raisin-black); + text-decoration: none; + transition: all 0.3s ease; +} + +.account-btn:hover, +.cart-btn:hover { + background: rgba(12, 129, 74, 0.1); + color: var(--salem); +} + +.account-btn i, +.cart-btn i { + font-size: 1.2rem; +} + +.btn-text { + font-size: 0.9rem; + font-weight: 500; +} + +/* تنسيق سلة التسوق */ +.shopping-cart { + position: relative; +} + +.cart-count { + position: absolute; + top: -5px; + right: -5px; + background: var(--salem); + color: white; + font-size: 0.7rem; + font-weight: bold; + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +/* تنسيق قائمة الحساب المنسدلة */ +.user-account { + position: relative; +} + +.account-dropdown { + position: absolute; + top: 100%; + left: 0; + background: white; + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + min-width: 200px; + padding: 10px 0; + display: none; + z-index: 1000; +} + +.user-account:hover .account-dropdown { + display: block; +} + +.account-dropdown a { + display: block; + padding: 8px 20px; + color: var(--raisin-black); + text-decoration: none; + transition: all 0.3s ease; +} + +.account-dropdown a:hover { + background: rgba(12, 129, 74, 0.1); + color: var(--salem); +} + +/* تحسينات للتجاوب مع الشاشات الصغيرة */ +@media (max-width: 768px) { + .nav-actions { + margin: 10px 0; + justify-content: center; + } + + .btn-text { + display: none; + } + + .account-btn, + .cart-btn { + padding: 8px; + } + + .account-dropdown { + left: auto; + right: 0; + } +} + +/* قسم العروض الخاصة */ +.special-offers { + padding: 60px 0; + background: linear-gradient(to bottom, #fff, var(--cultured)); +} + +.offers-grid { + display: grid; + gap: 30px; + margin-top: 40px; +} + +/* العرض الرئيسي */ +.main-offer { + grid-column: 1 / -1; +} + +.offer-card { + background: white; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); + position: relative; + transition: all 0.3s ease; +} + +.offer-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.offer-card.featured { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; +} + +.offer-badge { + position: absolute; + top: 20px; + right: 20px; + background: var(--salem); + color: white; + padding: 8px 15px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 500; + z-index: 1; +} + +.offer-card img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.offer-content { + padding: 25px; +} + +.offer-card.featured .offer-content { + display: flex; + flex-direction: column; + justify-content: center; +} + +.offer-content h3 { + font-size: 1.5rem; + margin-bottom: 10px; + color: var(--raisin-black); +} + +.offer-content p { + color: #666; + margin-bottom: 20px; +} + +.price { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 20px; +} + +.old-price { + color: #999; + text-decoration: line-through; + font-size: 0.9rem; +} + +.new-price { + color: var(--salem); + font-size: 1.4rem; + font-weight: bold; +} + +/* العروض الفرعية */ +.offers-list { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.offers-list .offer-card { + display: flex; + flex-direction: column; +} + +.offers-list .offer-card img { + height: 200px; +} + +/* شريط العروض المتحرك */ +.offers-ticker { + grid-column: 1 / -1; + background: var(--salem); + border-radius: 10px; + padding: 15px; + overflow: hidden; +} + +.ticker-content { + display: flex; + gap: 50px; + animation: ticker 20s linear infinite; + white-space: nowrap; +} + +.ticker-content span { + color: white; + font-size: 1rem; + display: flex; + align-items: center; + gap: 10px; +} + +@keyframes ticker { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} + +/* تحسين التجاوب مع الشاشات الصغيرة */ +@media (max-width: 992px) { + .offer-card.featured { + grid-template-columns: 1fr; + } + + .offers-list { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .offers-list { + grid-template-columns: 1fr; + } + + .offer-content h3 { + font-size: 1.3rem; + } + + .new-price { + font-size: 1.2rem; + } +} diff --git a/database/backup.sql b/database/backup.sql new file mode 100644 index 0000000..2babad2 --- /dev/null +++ b/database/backup.sql @@ -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 diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..2dec0d6 --- /dev/null +++ b/database/schema.sql @@ -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'; diff --git a/database/shubraveil_db.sql b/database/shubraveil_db.sql new file mode 100644 index 0000000..d856d56 --- /dev/null +++ b/database/shubraveil_db.sql @@ -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; diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..a556f79 --- /dev/null +++ b/deploy.sh @@ -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" diff --git a/faq.html b/faq.html new file mode 100644 index 0000000..16062f5 --- /dev/null +++ b/faq.html @@ -0,0 +1,112 @@ + + + + + + الأسئلة الشائعة - ShubraVeil + + + + + +
+ +
+ +
+ +
+
+

الأسئلة الشائعة

+
+
+
+

ما هي الزيوت العطرية؟

+ +
+
+

الزيوت العطرية هي مركبات نباتية مركزة تستخرج من النباتات والأعشاب. تتميز برائحتها القوية وفوائدها العلاجية المتعددة.

+
+
+ +
+
+

كيف يمكنني استخدام الزيوت العطرية بأمان؟

+ +
+
+

يجب تخفيف معظم الزيوت العطرية قبل الاستخدام المباشر على الجلد. استخدم زيت حامل مثل زيت جوز الهند أو زيت اللوز. قم دائماً باختبار الحساسية قبل الاستخدام الكامل.

+
+
+ +
+
+

ما هي مدة صلاحية الزيوت العطرية؟

+ +
+
+

تختلف مدة الصلاحية حسب نوع الزيت، لكن معظم الزيوت العطرية تدوم من سنة إلى سنتين عند تخزينها بشكل صحيح في مكان بارد ومظلم.

+
+
+ +
+
+

هل منتجاتكم طبيعية 100%؟

+ +
+
+

نعم، جميع منتجاتنا طبيعية 100% ومستخلصة من مصادر عضوية موثوقة. نحن نضمن جودة منتجاتنا ونقدم شهادات الجودة عند الطلب.

+
+
+
+
+
+
+ + +
+
+ + +
+
+ + + + diff --git a/forgot-password.php b/forgot-password.php new file mode 100644 index 0000000..9f3a734 --- /dev/null +++ b/forgot-password.php @@ -0,0 +1,103 @@ +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(); + } +} +?> + + + + + + نسيت كلمة المرور - ShubraVeil + + + + +
+
+
+
+
+

استعادة كلمة المرور

+ + +
+ + + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+
+
+
+ + + + diff --git a/images/1.jpg b/images/1.jpg new file mode 100644 index 0000000..b359b0c Binary files /dev/null and b/images/1.jpg differ diff --git a/images/2.jpg b/images/2.jpg new file mode 100644 index 0000000..cbfd605 Binary files /dev/null and b/images/2.jpg differ diff --git a/images/hero-bg.jpg b/images/hero-bg.jpg new file mode 100644 index 0000000..d3e775d --- /dev/null +++ b/images/hero-bg.jpg @@ -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. diff --git a/images/logo.jpg b/images/logo.jpg new file mode 100644 index 0000000..edf0009 Binary files /dev/null and b/images/logo.jpg differ diff --git a/images/patrin .jpg b/images/patrin .jpg new file mode 100644 index 0000000..93435fc Binary files /dev/null and b/images/patrin .jpg differ diff --git a/images/products 2.jpg b/images/products 2.jpg new file mode 100644 index 0000000..69928db Binary files /dev/null and b/images/products 2.jpg differ diff --git a/images/proudctus.jpg b/images/proudctus.jpg new file mode 100644 index 0000000..b71abda Binary files /dev/null and b/images/proudctus.jpg differ diff --git a/includes/Cache.php b/includes/Cache.php new file mode 100644 index 0000000..5e6c31a --- /dev/null +++ b/includes/Cache.php @@ -0,0 +1,76 @@ +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'; + } +} diff --git a/includes/ImageHandler.php b/includes/ImageHandler.php new file mode 100644 index 0000000..8761cf4 --- /dev/null +++ b/includes/ImageHandler.php @@ -0,0 +1,115 @@ +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; + } +} diff --git a/includes/ImageProcessor.php b/includes/ImageProcessor.php new file mode 100644 index 0000000..68334f6 --- /dev/null +++ b/includes/ImageProcessor.php @@ -0,0 +1,84 @@ +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; + } +} diff --git a/includes/Logger.php b/includes/Logger.php new file mode 100644 index 0000000..02bd0ea --- /dev/null +++ b/includes/Logger.php @@ -0,0 +1,72 @@ +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); + } +} diff --git a/includes/Mailer.php b/includes/Mailer.php new file mode 100644 index 0000000..817c79b --- /dev/null +++ b/includes/Mailer.php @@ -0,0 +1,92 @@ +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; + } + } +} diff --git a/includes/Security.php b/includes/Security.php new file mode 100644 index 0000000..436741c --- /dev/null +++ b/includes/Security.php @@ -0,0 +1,116 @@ +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); + } +} diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..e2038d3 --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,120 @@ +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' => 'كلمة المرور الحالية غير صحيحة']; + } +} diff --git a/includes/cart.php b/includes/cart.php new file mode 100644 index 0000000..ca74e0d --- /dev/null +++ b/includes/cart.php @@ -0,0 +1,214 @@ +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 + ]; + } +} diff --git a/includes/config.php b/includes/config.php new file mode 100644 index 0000000..e5ba658 --- /dev/null +++ b/includes/config.php @@ -0,0 +1,103 @@ + $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(); +} diff --git a/includes/config.production.php b/includes/config.production.php new file mode 100644 index 0000000..7b5605b --- /dev/null +++ b/includes/config.production.php @@ -0,0 +1,34 @@ +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; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..71c52a2 --- /dev/null +++ b/index.html @@ -0,0 +1,570 @@ + + + + + + ShubraVeil - زيوت طبيعية عطرية + + + + + +
+ +
+ +
+
+
+
+

زيوت طبيعية نقية 100%

+

نقدم لكم أجود أنواع الزيوت العطرية المستخلصة من أراضي شبرا بلولة الخصبة

+ +
+
+
+ +
+
+
+
+ +

طبيعي 100%

+

زيوت مستخلصة بطريقة طبيعية

+
+
+ +

جودة عالية

+

معتمدة من أفضل المختبرات

+
+
+ +

شحن سريع

+

توصيل لجميع المناطق

+
+
+
+
+ + + +
+
+
+

عروض خاصة

+

اكتشف أفضل العروض والتخفيضات على منتجاتنا

+
+ +
+ +
+ +
+ + +
+
+
جديد
+ زيت إكليل الجبل +
+

زيت إكليل الجبل

+

منتج جديد - عرض خاص لفترة محدودة

+
+ 180 ج.م +
+ +
+
+ +
+
خصم 20%
+ زيت شجرة الشاي +
+

زيت شجرة الشاي

+

العرض ساري حتى نفاذ الكمية

+
+ 250 ج.م + 200 ج.م +
+ +
+
+ +
+
عرض خاص
+ زيت النعناع +
+

زيت النعناع الطبيعي

+

اشترِ قارورتين واحصل على الثالثة مجاناً

+
+ 150 ج.م +
+ +
+
+
+ + +
+
+ 🌟 خصم 15% على جميع المنتجات للأعضاء الجدد + 🚚 شحن مجاني للطلبات أكثر من 500 ج.م + 🎁 هدية مجانية مع كل طلب أكثر من 1000 ج.م +
+
+
+
+
+ +
+
+
+

منتجاتنا

+

مجموعة متنوعة من الزيوت العطرية الطبيعية

+
+
+
+
+ زيت الياسمين المطلق +
+
+

زيت الياسمين المطلق

+

زيت عطري طبيعي 100% من زهور الياسمين

+
+ طبيعي 100% + استخلاص مباشر +
+ اطلب الآن +
+
+
+
+ زيت إكليل الجبل +
+
+

زيت إكليل الجبل

+

زيت عطري نقي مستخلص من نبات إكليل الجبل

+
+ طبيعي 100% + استخلاص مباشر +
+ اطلب الآن +
+
+
+
+
+ +
+
+
+

قرية شبرا بلولة

+

مصدر الكنوز العطرية الطبيعية

+
+
+
+

تعد قرية شبرا بلولة مصدراً للكنوز العطرية التي تنمو بين أراضيها الخصبة. من هنا، تستخلص شركتنا زيوتها، معتمدة على هذا المورد الطبيعي الثمين لتقديم منتجات عالية الجودة، تتميز برائحة الياسمين الفريدة، التي تساهم في تعزيز الصحة والجمال.

+
    +
  • استخلاص طبيعي 100%
  • +
  • زراعة عضوية مستدامة
  • +
  • جودة عالمية
  • +
+
+
+ حقول شبرا بلولة +
+
+
+
+ +
+
+
+

فوائد الزيوت العطرية

+

اكتشف الفوائد الصحية والجمالية لزيوتنا الطبيعية

+
+
+
+ +

الصحة النفسية

+

تساعد على الاسترخاء وتحسين المزاج

+
+
+ +

العناية بالبشرة

+

تغذية وترطيب طبيعي للبشرة

+
+
+ +

تعطير المنزل

+

روائح طبيعية منعشة للمنزل

+
+
+
+
+ +
+
+

آراء عملائنا

+
+
+
+
+ + + + + +
+

"زيوت عطرية رائعة وطبيعية 100%. استخدمت زيت اللافندر للاسترخاء وكانت النتائج مذهلة!"

+
+ سارة أحمد +
+

سارة أحمد

+ عميلة منذ 2023 +
+
+
+
+
+
+
+ + + + + +
+

"جودة استثنائية وخدمة عملاء ممتازة. أنصح بشدة بزيت إكليل الجبل لتقوية الشعر."

+
+ محمد علي +
+

محمد علي

+ عميل منذ 2024 +
+
+
+
+
+
+
+ + + +
+
+
+

تواصل معنا

+

نحن هنا للإجابة على استفساراتكم

+
+
+
+
+ +

اتصل بنا

+

+20 123 456 789

+
+
+ +

راسلنا

+

info@shubraveil.com

+
+
+ +

زورنا

+

شبرا بلولة، مصر

+
+
+
+
+
+ +
+

مراجعات العملاء

+ +
+
+

التقييم العام

+
4.5
+
+ + + + + +
+ 125 مراجعة +
+ +
+
+ 5 نجوم +
+
+
+ 70% +
+
+ 4 نجوم +
+
+
+ 20% +
+
+ 3 نجوم +
+
+
+ 5% +
+
+ 2 نجوم +
+
+
+ 3% +
+
+ 1 نجمة +
+
+
+ 2% +
+
+
+ +
+
+ + + + + + +
+ + +
+ +
+ +
+ +
+

أضف مراجعتك

+ +
+ +
+ + + + + +
+ +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ +
+
+ + +
+
+ + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..8b2b1aa --- /dev/null +++ b/index.php @@ -0,0 +1,100 @@ + + + + + + + ShubraVeil - متجر الزيوت الطبيعية + + + + + + + + +
+
+
+

مرحباً بكم في ShubraVeil

+

اكتشف مجموعتنا المميزة من الزيوت الطبيعية عالية الجودة

+ + + +
+
+
+ + + + + + + diff --git a/js/cart.js b/js/cart.js new file mode 100644 index 0000000..18b9e10 --- /dev/null +++ b/js/cart.js @@ -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 = '

السلة فارغة

'; + return; + } + + this.cartItems.innerHTML = this.cart.map(item => ` +
+ ${item.name} +
+

${item.name}

+
${item.price} جنيه
+
+ + ${item.quantity} + +
+
+ +
+ `).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(); diff --git a/js/comments.js b/js/comments.js new file mode 100644 index 0000000..9eb6c9a --- /dev/null +++ b/js/comments.js @@ -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 = ` +
+ ${comment.userName} +
+

${comment.userName}

+ ${comment.date} +
+
+
+

${comment.content}

+
+
+ + +
+
+
+ + +
+
+ `; + + this.commentsContainer.prepend(commentElement); + } +} + +// تهيئة نظام التعليقات +document.addEventListener('DOMContentLoaded', () => { + new CommentSystem(); +}); diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..0e0d416 --- /dev/null +++ b/js/main.js @@ -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 => + `
${item}
` + ).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 => `${item}`) + .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 + } +}); diff --git a/js/reviews.js b/js/reviews.js new file mode 100644 index 0000000..2223a66 --- /dev/null +++ b/js/reviews.js @@ -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 = '

لا توجد مراجعات بعد

'; + return; + } + + this.reviewsContainer.innerHTML = reviews.map(review => ` +
+
+
+ ${review.userName} +
+

${review.userName}

+
+ ${this.generateStars(review.rating)} +
+ ${review.date} +
+
+
+ ${review.verified ? ' شراء موثق' : ''} +
+
+
+

${review.content}

+ ${review.images ? this.generateImageGallery(review.images) : ''} +
+
+ + +
+
+ `).join(''); + } + + generateStars(rating) { + return Array(5).fill().map((_, index) => ` + + `).join(''); + } + + generateImageGallery(images) { + return ` +
+ ${images.map(image => ` + صورة المراجعة + `).join('')} +
+ `; + } + + 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 = ` + + `; + + 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(); diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..980850f --- /dev/null +++ b/js/search.js @@ -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 = '
لم يتم العثور على نتائج
'; + return; + } + + resultsContainer.innerHTML = results.map(product => ` +
+
+ ${product.name} + ${product.badge ? `${product.badge}` : ''} +
+
+

${product.name}

+

${product.description}

+
${product.price} جنيه
+
+ + +
+
+
+ `).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 => ` +
+ ${product.name} + ${product.name} + +
+ `).join(''); + }); + } else { + comparisonList.innerHTML = '

اختر منتجات للمقارنة

'; + } +} + +// دالة مساعدة للحد من تكرار الطلبات +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 = `
${message}
`; +} + +// جلب تفاصيل المنتجات للمقارنة +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 = '
لا توجد نتائج
'; + return; + } + + results.forEach(result => { + const resultItem = document.createElement('div'); + resultItem.className = 'result-item'; + resultItem.innerHTML = ` + ${result.title} +
+
${result.title}
+
${result.category}
+
+
${result.price} ر.س
+ `; + 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 = ` +
+ +

لم يتم العثور على نتائج

+
+ `; + } else { + this.searchResults.innerHTML = ` + + `; + } + + 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, '$1'); + } + + showResults() { + this.searchResults?.classList.add('show'); + } + + hideResults() { + this.searchResults?.classList.remove('show'); + } + + showError(message) { + if (!this.searchResults) return; + + this.searchResults.innerHTML = ` +
+ +

${message}

+
+ `; + 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(); diff --git a/js/voice-search.js b/js/voice-search.js new file mode 100644 index 0000000..50b77dd --- /dev/null +++ b/js/voice-search.js @@ -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 => ` + + `).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(); diff --git a/js/wishlist.js b/js/wishlist.js new file mode 100644 index 0000000..41cb1be --- /dev/null +++ b/js/wishlist.js @@ -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 = '

لا توجد منتجات في المفضلة

'; + return; + } + + wishlistContainer.innerHTML = this.wishlist.map(item => ` +
+ ${item.name} +
+

${item.name}

+
${item.price} جنيه
+
+
+ + +
+
+ `).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(); diff --git a/login.php b/login.php new file mode 100644 index 0000000..b337034 --- /dev/null +++ b/login.php @@ -0,0 +1,124 @@ +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(); + } +} +?> + + + + + + تسجيل الدخول - ShubraVeil + + + + +
+
+
+
+
+

تسجيل الدخول

+ + +
+ + + +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+ +
+
+ + +
+
+
+
+
+ + + + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..2be362f --- /dev/null +++ b/logout.php @@ -0,0 +1,17 @@ + + + + + + الزيوت الأساسية - ShubraVeil + + + + + +
+ +
+ +
+
+
+

الزيوت الأساسية

+

مجموعة متميزة من الزيوت الأساسية النقية 100% مستخلصة بعناية من أجود النباتات العطرية

+
+
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+ + + + + + diff --git a/products/fixed-oils.html b/products/fixed-oils.html new file mode 100644 index 0000000..3c7c87a --- /dev/null +++ b/products/fixed-oils.html @@ -0,0 +1,37 @@ + + + + + + + الزيوت الثابتة - ShubraVeil + + + + + + +
+ +
+ +
+
+
+

الزيوت الثابتة

+

مجموعة مختارة من أجود الزيوت الثابتة الطبيعية المستخلصة من البذور والمكسرات

+
+
+ + + +
+ +
+ +
+ + diff --git a/register.php b/register.php new file mode 100644 index 0000000..ecf2a0b --- /dev/null +++ b/register.php @@ -0,0 +1,198 @@ +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(); + } +} +?> + + + + + + إنشاء حساب جديد - ShubraVeil + + + + +
+
+
+
+
+

إنشاء حساب جديد

+ + +
+ + + + + + +
+
+ + +
+ يرجى اختيار اسم مستخدم صحيح +
+
+ +
+ + +
+ يرجى إدخال بريد إلكتروني صحيح +
+
+ +
+ + +
+ يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل +
+
+ +
+ + +
+ كلمتا المرور غير متطابقتين +
+
+ +
+
+ + +
+ يجب الموافقة على الشروط والأحكام +
+
+
+ +
+ +
+
+ +
+

لديك حساب بالفعل؟ تسجيل الدخول

+
+
+
+
+
+
+ + + + + diff --git a/reset-password.php b/reset-password.php new file mode 100644 index 0000000..5d6bc58 --- /dev/null +++ b/reset-password.php @@ -0,0 +1,166 @@ +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(); + } +} +?> + + + + + + إعادة تعيين كلمة المرور - ShubraVeil + + + + +
+
+
+
+
+

إعادة تعيين كلمة المرور

+ + +
+ + + + + +
+
+ + +
+ يجب أن تحتوي كلمة المرور على 8 أحرف على الأقل +
+
+ +
+ + +
+ كلمتا المرور غير متطابقتين +
+
+ +
+ +
+
+ +
+
+
+
+
+ + + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..483ae82 --- /dev/null +++ b/robots.txt @@ -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 diff --git a/search.html b/search.html new file mode 100644 index 0000000..f5511f4 --- /dev/null +++ b/search.html @@ -0,0 +1,88 @@ + + + + + + بحث المنتجات - ShubraVeil + + + + + + +
+ +
+ +
+
+
+

بحث المنتجات

+ +
+ + +
+ +
+ +
+

مقارنة المنتجات

+
+ +
+ +
+
+
+
+
+ +
+ +
+ + + + diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..9202a88 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,33 @@ + + + + https://shubraveil.com/ + 2024-12-09 + daily + 1.0 + + + https://shubraveil.com/products + 2024-12-09 + daily + 0.9 + + + https://shubraveil.com/blog.html + 2024-12-09 + weekly + 0.8 + + + https://shubraveil.com/faq.html + 2024-12-09 + monthly + 0.7 + + + https://shubraveil.com/search.html + 2024-12-09 + monthly + 0.6 + + diff --git a/templates/emails/order-confirmation.html b/templates/emails/order-confirmation.html new file mode 100644 index 0000000..632f1d6 --- /dev/null +++ b/templates/emails/order-confirmation.html @@ -0,0 +1,40 @@ + + + + + تأكيد الطلب - ShubraVeil + + + +
+
+

شكراً لطلبك من ShubraVeil

+
+
+

مرحباً {USER_NAME}،

+

نشكرك على طلبك. تم استلام طلبك بنجاح وجاري تجهيزه.

+ +

تفاصيل الطلب:

+

رقم الطلب: {ORDER_ID}

+

إجمالي المبلغ: {ORDER_TOTAL} جنيه

+ +

يمكنك متابعة حالة طلبك من خلال حسابك على موقعنا.

+ + +
+ +
+ + diff --git a/test_backup.php b/test_backup.php new file mode 100644 index 0000000..1dca49c --- /dev/null +++ b/test_backup.php @@ -0,0 +1,74 @@ +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"; +} diff --git a/test_connection.php b/test_connection.php new file mode 100644 index 0000000..7d83ad7 --- /dev/null +++ b/test_connection.php @@ -0,0 +1,37 @@ +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(); +} diff --git a/test_db.php b/test_db.php new file mode 100644 index 0000000..0c10f10 --- /dev/null +++ b/test_db.php @@ -0,0 +1,25 @@ +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(); +} diff --git a/test_images.php b/test_images.php new file mode 100644 index 0000000..eca662b --- /dev/null +++ b/test_images.php @@ -0,0 +1,104 @@ +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"; +} diff --git a/test_orders.php b/test_orders.php new file mode 100644 index 0000000..a742bee --- /dev/null +++ b/test_orders.php @@ -0,0 +1,105 @@ +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"; +} diff --git a/test_products.php b/test_products.php new file mode 100644 index 0000000..e994c42 --- /dev/null +++ b/test_products.php @@ -0,0 +1,84 @@ +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"; +} diff --git a/test_security.php b/test_security.php new file mode 100644 index 0000000..086a04c --- /dev/null +++ b/test_security.php @@ -0,0 +1,33 @@ +alert('XSS');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"; diff --git a/test_user_system.php b/test_user_system.php new file mode 100644 index 0000000..155821c --- /dev/null +++ b/test_user_system.php @@ -0,0 +1,102 @@ +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(); + } +} diff --git a/test_users.php b/test_users.php new file mode 100644 index 0000000..b4b7638 --- /dev/null +++ b/test_users.php @@ -0,0 +1,74 @@ +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"; +} diff --git a/tests/SecurityTest.php b/tests/SecurityTest.php new file mode 100644 index 0000000..9735d21 --- /dev/null +++ b/tests/SecurityTest.php @@ -0,0 +1,46 @@ +security = Security::getInstance(); + } + + public function testSanitizeInput() { + $input = ""; + $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)); + } +} diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/uploads/.gitkeep @@ -0,0 +1 @@ +