second commit
This commit is contained in:
parent
f1f424e6ff
commit
f89d1364fd
30
.htaccess
30
.htaccess
@ -7,36 +7,42 @@ RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Protect sensitive files
|
||||
<FilesMatch "^\.env">
|
||||
<FilesMatch "^\.env|composer\.json|composer\.lock|package\.json|package-lock\.json|README\.md|\.gitignore">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Protect directories
|
||||
<DirectoryMatch "^/.*/(?:logs|backups|cache)/">
|
||||
<DirectoryMatch "^/.*/(?:logs|backups|cache)/|^/.git/|/.github/|/vendor/|/node_modules/">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</DirectoryMatch>
|
||||
|
||||
# Handle PHP errors
|
||||
php_flag display_errors off
|
||||
php_value error_reporting E_ALL
|
||||
php_value error_log logs/php_errors.log
|
||||
|
||||
# Security Headers
|
||||
# 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=()"
|
||||
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'"
|
||||
|
||||
# Cache Control
|
||||
<FilesMatch "\.(jpg|jpeg|png|gif|ico|css|js)$">
|
||||
# Cache control
|
||||
<FilesMatch "\.(css|js|jpg|jpeg|png|gif|ico|webp)$">
|
||||
Header set Cache-Control "max-age=31536000, public"
|
||||
</FilesMatch>
|
||||
|
||||
# Prevent directory listing
|
||||
# PHP settings
|
||||
php_flag display_errors off
|
||||
php_value upload_max_filesize 5M
|
||||
php_value post_max_size 6M
|
||||
php_value max_execution_time 30
|
||||
php_value max_input_time 60
|
||||
php_value memory_limit 128M
|
||||
php_value error_reporting E_ALL
|
||||
php_value error_log logs/php_errors.log
|
||||
|
||||
# Directory protection
|
||||
Options -Indexes
|
||||
ServerSignature Off
|
||||
|
||||
# Custom error pages
|
||||
ErrorDocument 404 /404.html
|
||||
|
120
README.md
120
README.md
@ -2,62 +2,32 @@
|
||||
|
||||
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 التالية:
|
||||
- امتدادات PHP المطلوبة:
|
||||
- GD
|
||||
- MySQLi
|
||||
- ZIP
|
||||
- JSON
|
||||
- OpenSSL
|
||||
|
||||
### التثبيت
|
||||
## التثبيت
|
||||
|
||||
1. استنساخ المستودع:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/shubraveil.git
|
||||
@ -69,7 +39,7 @@ cd shubraveil
|
||||
composer install
|
||||
```
|
||||
|
||||
3. إنشاء ملف .env:
|
||||
3. إنشاء وتكوين ملف .env:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
@ -79,27 +49,61 @@ cp .env.example .env
|
||||
```bash
|
||||
mysql -u root -p
|
||||
CREATE DATABASE shubraveil_db;
|
||||
mysql -u root -p shubraveil_db < database/schema.sql
|
||||
```
|
||||
|
||||
5. تهيئة المجلدات:
|
||||
5. تهيئة المجلدات وضبط الصلاحيات:
|
||||
```bash
|
||||
mkdir -p uploads/products
|
||||
mkdir -p cache
|
||||
mkdir -p backups
|
||||
mkdir -p uploads/products cache backups
|
||||
chmod -R 755 uploads cache backups
|
||||
```
|
||||
|
||||
### التكوين
|
||||
#### الإعدادات الأساسية
|
||||
1. تكوين قاعدة البيانات في ملف .env
|
||||
2. تكوين SMTP لإرسال البريد الإلكتروني
|
||||
3. تكوين مفاتيح reCAPTCHA
|
||||
## الأمان
|
||||
|
||||
#### الأمان
|
||||
- تأكد من تعيين كلمات مرور قوية
|
||||
- قم بتحديث مفتاح JWT_SECRET
|
||||
- قم بتكوين HTTPS
|
||||
- تم تفعيل HTTPS إجبارياً
|
||||
- حماية الملفات والمجلدات الحساسة
|
||||
- استخدام CSRF tokens لحماية النماذج
|
||||
- تشفير كلمات المرور باستخدام password_hash
|
||||
- استخدام Prepared Statements لمنع SQL Injection
|
||||
- تصفية وتنظيف جميع المدخلات
|
||||
- رسائل خطأ آمنة لا تكشف معلومات حساسة
|
||||
|
||||
## 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.
|
||||
```
|
||||
shubraveil/
|
||||
├── admin/ # لوحة التحكم
|
||||
├── api/ # واجهة برمجة التطبيقات
|
||||
├── backups/ # النسخ الاحتياطية
|
||||
├── cache/ # التخزين المؤقت
|
||||
├── css/ # ملفات CSS
|
||||
├── database/ # ملفات قاعدة البيانات
|
||||
├── images/ # الصور الثابتة
|
||||
├── includes/ # ملفات PHP المشتركة
|
||||
├── js/ # ملفات JavaScript
|
||||
├── products/ # صفحات المنتجات
|
||||
├── templates/ # قوالب الصفحات
|
||||
├── uploads/ # الملفات المرفوعة
|
||||
└── tests/ # اختبارات الوحدة
|
||||
```
|
||||
|
||||
## الاختبارات
|
||||
|
||||
يمكن تشغيل الاختبارات باستخدام:
|
||||
```bash
|
||||
composer test
|
||||
```
|
||||
|
||||
## النسخ الاحتياطي
|
||||
|
||||
يتم إنشاء نسخة احتياطية تلقائياً كل يوم في الساعة 12 صباحاً في مجلد `backups/`.
|
||||
|
||||
## المساهمة
|
||||
|
||||
1. Fork المستودع
|
||||
2. إنشاء فرع للميزة الجديدة
|
||||
3. إرسال pull request
|
||||
|
||||
## الترخيص
|
||||
|
||||
جميع الحقوق محفوظة © 2024 ShubraVeil
|
||||
|
@ -1,24 +1,32 @@
|
||||
{
|
||||
"name": "shubraveil/website",
|
||||
"description": "ShubraVeil - نظام إدارة متجر الحجاب",
|
||||
"name": "shubraveil/essential-oils",
|
||||
"description": "ShubraVeil Essential Oils E-commerce Platform",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"php": "^7.4|^8.0",
|
||||
"stripe/stripe-php": "^10.0",
|
||||
"paypal/rest-api-sdk-php": "^1.14",
|
||||
"kreait/firebase-php": "^5.0",
|
||||
"phpmailer/phpmailer": "^6.8",
|
||||
"firebase/php-jwt": "^6.4",
|
||||
"vlucas/phpdotenv": "^5.5",
|
||||
"monolog/monolog": "^2.9",
|
||||
"intervention/image": "^2.7",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"monolog/monolog": "^2.0"
|
||||
"guzzlehttp/guzzle": "^7.7",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-mysqli": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-intl": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"squizlabs/php_codesniffer": "^3.7"
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"fakerphp/faker": "^1.23"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ShubraVeil\\": "includes/"
|
||||
"ShubraVeil\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
@ -28,11 +36,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"analyse": "phpstan analyse",
|
||||
"phpstan": "phpstan analyse",
|
||||
"cs": "phpcs",
|
||||
"post-install-cmd": [
|
||||
"php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
]
|
||||
"cs-fix": "phpcbf"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
112
includes/Language.php
Normal file
112
includes/Language.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
class Language {
|
||||
private static $translations = [];
|
||||
private static $currentLocale = 'ar';
|
||||
private static $fallbackLocale = 'en';
|
||||
private static $supportedLocales = ['ar', 'en'];
|
||||
|
||||
public static function init($locale = null) {
|
||||
if ($locale && in_array($locale, self::$supportedLocales)) {
|
||||
self::$currentLocale = $locale;
|
||||
} elseif (isset($_SESSION['locale'])) {
|
||||
self::$currentLocale = $_SESSION['locale'];
|
||||
} else {
|
||||
// Try to detect from browser
|
||||
$browserLang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
|
||||
self::$currentLocale = in_array($browserLang, self::$supportedLocales) ? $browserLang : self::$fallbackLocale;
|
||||
}
|
||||
|
||||
$_SESSION['locale'] = self::$currentLocale;
|
||||
self::loadTranslations();
|
||||
}
|
||||
|
||||
public static function setLocale($locale) {
|
||||
if (in_array($locale, self::$supportedLocales)) {
|
||||
self::$currentLocale = $locale;
|
||||
$_SESSION['locale'] = $locale;
|
||||
self::loadTranslations();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getCurrentLocale() {
|
||||
return self::$currentLocale;
|
||||
}
|
||||
|
||||
public static function isRTL() {
|
||||
return in_array(self::$currentLocale, ['ar']);
|
||||
}
|
||||
|
||||
public static function getDirection() {
|
||||
return self::isRTL() ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
public static function translate($key, $params = []) {
|
||||
$translation = self::$translations[self::$currentLocale][$key] ??
|
||||
self::$translations[self::$fallbackLocale][$key] ??
|
||||
$key;
|
||||
|
||||
if (!empty($params)) {
|
||||
foreach ($params as $param => $value) {
|
||||
$translation = str_replace(':' . $param, $value, $translation);
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
private static function loadTranslations() {
|
||||
// Load current locale
|
||||
$localePath = __DIR__ . '/../lang/' . self::$currentLocale . '.php';
|
||||
if (file_exists($localePath)) {
|
||||
self::$translations[self::$currentLocale] = require $localePath;
|
||||
}
|
||||
|
||||
// Load fallback locale if different
|
||||
if (self::$currentLocale !== self::$fallbackLocale) {
|
||||
$fallbackPath = __DIR__ . '/../lang/' . self::$fallbackLocale . '.php';
|
||||
if (file_exists($fallbackPath)) {
|
||||
self::$translations[self::$fallbackLocale] = require $fallbackPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAllTranslations() {
|
||||
return self::$translations;
|
||||
}
|
||||
|
||||
public static function getSupportedLocales() {
|
||||
return self::$supportedLocales;
|
||||
}
|
||||
|
||||
// Helper function for date formatting
|
||||
public static function formatDate($timestamp, $format = 'full') {
|
||||
$locale = self::$currentLocale . '_' . strtoupper(self::$currentLocale);
|
||||
setlocale(LC_TIME, $locale . '.UTF-8');
|
||||
|
||||
$formats = [
|
||||
'full' => '%A %d %B %Y',
|
||||
'long' => '%d %B %Y',
|
||||
'medium' => '%d %b %Y',
|
||||
'short' => '%d/%m/%Y'
|
||||
];
|
||||
|
||||
return strftime($formats[$format] ?? $formats['full'], $timestamp);
|
||||
}
|
||||
|
||||
// Helper function for number formatting
|
||||
public static function formatNumber($number, $decimals = 0) {
|
||||
$locale = self::$currentLocale . '_' . strtoupper(self::$currentLocale);
|
||||
$formatter = new NumberFormatter($locale, NumberFormatter::DECIMAL);
|
||||
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $decimals);
|
||||
return $formatter->format($number);
|
||||
}
|
||||
|
||||
// Helper function for currency formatting
|
||||
public static function formatCurrency($amount, $currency = 'EGP') {
|
||||
$locale = self::$currentLocale . '_' . strtoupper(self::$currentLocale);
|
||||
$formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
|
||||
return $formatter->formatCurrency($amount, $currency);
|
||||
}
|
||||
}
|
105
includes/Notification.php
Normal file
105
includes/Notification.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
class Notification {
|
||||
private $db;
|
||||
private $mailer;
|
||||
|
||||
public function __construct($db, $mailer) {
|
||||
$this->db = $db;
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
public function createNotification($user_id, $type, $message, $data = null) {
|
||||
$stmt = $this->db->prepare("INSERT INTO notifications (user_id, type, message, data, created_at) VALUES (?, ?, ?, ?, NOW())");
|
||||
$data_json = $data ? json_encode($data) : null;
|
||||
$stmt->bind_param('isss', $user_id, $type, $message, $data_json);
|
||||
$stmt->execute();
|
||||
|
||||
// Send email notification if enabled
|
||||
$this->sendEmailNotification($user_id, $type, $message);
|
||||
|
||||
// Send push notification if enabled
|
||||
$this->sendPushNotification($user_id, $type, $message);
|
||||
|
||||
return $stmt->insert_id;
|
||||
}
|
||||
|
||||
public function getUserNotifications($user_id, $limit = 10, $offset = 0) {
|
||||
$stmt = $this->db->prepare("SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?");
|
||||
$stmt->bind_param('iii', $user_id, $limit, $offset);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
public function markAsRead($notification_id, $user_id) {
|
||||
$stmt = $this->db->prepare("UPDATE notifications SET read_at = NOW() WHERE id = ? AND user_id = ?");
|
||||
$stmt->bind_param('ii', $notification_id, $user_id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
public function markAllAsRead($user_id) {
|
||||
$stmt = $this->db->prepare("UPDATE notifications SET read_at = NOW() WHERE user_id = ? AND read_at IS NULL");
|
||||
$stmt->bind_param('i', $user_id);
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
public function getUnreadCount($user_id) {
|
||||
$stmt = $this->db->prepare("SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND read_at IS NULL");
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
return $result['count'];
|
||||
}
|
||||
|
||||
private function sendEmailNotification($user_id, $type, $message) {
|
||||
// Get user email
|
||||
$stmt = $this->db->prepare("SELECT email, notification_preferences FROM users WHERE id = ?");
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$user = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($user && $this->shouldSendEmail($user['notification_preferences'], $type)) {
|
||||
$this->mailer->sendEmail(
|
||||
$user['email'],
|
||||
"New Notification from ShubraVeil",
|
||||
$message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendPushNotification($user_id, $type, $message) {
|
||||
// Get user's push notification token
|
||||
$stmt = $this->db->prepare("SELECT push_token, notification_preferences FROM users WHERE id = ?");
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$user = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($user && $this->shouldSendPush($user['notification_preferences'], $type) && $user['push_token']) {
|
||||
// Initialize Firebase
|
||||
$firebase = new \Kreait\Firebase\Factory();
|
||||
$messaging = $firebase->createMessaging();
|
||||
|
||||
$message = \Kreait\Firebase\Messaging\CloudMessage::withTarget('token', $user['push_token'])
|
||||
->withNotification([
|
||||
'title' => 'ShubraVeil',
|
||||
'body' => $message
|
||||
]);
|
||||
|
||||
try {
|
||||
$messaging->send($message);
|
||||
} catch (\Exception $e) {
|
||||
// Log error but don't throw
|
||||
error_log("Push notification failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldSendEmail($preferences, $type) {
|
||||
$prefs = json_decode($preferences, true);
|
||||
return isset($prefs['email'][$type]) ? $prefs['email'][$type] : true;
|
||||
}
|
||||
|
||||
private function shouldSendPush($preferences, $type) {
|
||||
$prefs = json_decode($preferences, true);
|
||||
return isset($prefs['push'][$type]) ? $prefs['push'][$type] : true;
|
||||
}
|
||||
}
|
109
includes/Payment.php
Normal file
109
includes/Payment.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
class Payment {
|
||||
private $db;
|
||||
private $stripe;
|
||||
private $paypal;
|
||||
|
||||
public function __construct($db) {
|
||||
$this->db = $db;
|
||||
|
||||
// Initialize Stripe
|
||||
if (getenv('STRIPE_SECRET_KEY')) {
|
||||
\Stripe\Stripe::setApiKey(getenv('STRIPE_SECRET_KEY'));
|
||||
$this->stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));
|
||||
}
|
||||
|
||||
// Initialize PayPal
|
||||
if (getenv('PAYPAL_CLIENT_ID') && getenv('PAYPAL_CLIENT_SECRET')) {
|
||||
$this->paypal = new \PayPal\Rest\ApiContext(
|
||||
new \PayPal\Auth\OAuthTokenCredential(
|
||||
getenv('PAYPAL_CLIENT_ID'),
|
||||
getenv('PAYPAL_CLIENT_SECRET')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function processStripePayment($amount, $currency, $token, $description) {
|
||||
try {
|
||||
$charge = \Stripe\Charge::create([
|
||||
'amount' => $amount * 100, // Convert to cents
|
||||
'currency' => $currency,
|
||||
'source' => $token,
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
$this->saveTransaction($charge->id, 'stripe', $amount, $currency, $charge->status);
|
||||
return ['success' => true, 'transaction_id' => $charge->id];
|
||||
|
||||
} catch (\Stripe\Exception\CardException $e) {
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function processPayPalPayment($amount, $currency, $returnUrl, $cancelUrl) {
|
||||
$payer = new \PayPal\Api\Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amount = new \PayPal\Api\Amount();
|
||||
$amount->setTotal($amount);
|
||||
$amount->setCurrency($currency);
|
||||
|
||||
$transaction = new \PayPal\Api\Transaction();
|
||||
$transaction->setAmount($amount);
|
||||
|
||||
$redirectUrls = new \PayPal\Api\RedirectUrls();
|
||||
$redirectUrls->setReturnUrl($returnUrl)
|
||||
->setCancelUrl($cancelUrl);
|
||||
|
||||
$payment = new \PayPal\Api\Payment();
|
||||
$payment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions(array($transaction))
|
||||
->setRedirectUrls($redirectUrls);
|
||||
|
||||
try {
|
||||
$payment->create($this->paypal);
|
||||
return ['success' => true, 'approval_url' => $payment->getApprovalLink()];
|
||||
|
||||
} catch (\PayPal\Exception\PayPalConnectionException $e) {
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function executePayPalPayment($paymentId, $payerId) {
|
||||
$payment = \PayPal\Api\Payment::get($paymentId, $this->paypal);
|
||||
|
||||
$execution = new \PayPal\Api\PaymentExecution();
|
||||
$execution->setPayerId($payerId);
|
||||
|
||||
try {
|
||||
$result = $payment->execute($execution, $this->paypal);
|
||||
$this->saveTransaction($result->getId(), 'paypal', $result->getTransactions()[0]->getAmount()->getTotal(), $result->getTransactions()[0]->getAmount()->getCurrency(), $result->getState());
|
||||
return ['success' => true, 'transaction_id' => $result->getId()];
|
||||
|
||||
} catch (\PayPal\Exception\PayPalConnectionException $e) {
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function saveTransaction($transaction_id, $provider, $amount, $currency, $status) {
|
||||
$stmt = $this->db->prepare("INSERT INTO transactions (transaction_id, provider, amount, currency, status, created_at) VALUES (?, ?, ?, ?, ?, NOW())");
|
||||
$stmt->bind_param('ssdss', $transaction_id, $provider, $amount, $currency, $status);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function getTransaction($transaction_id) {
|
||||
$stmt = $this->db->prepare("SELECT * FROM transactions WHERE transaction_id = ?");
|
||||
$stmt->bind_param('s', $transaction_id);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_assoc();
|
||||
}
|
||||
|
||||
public function getUserTransactions($user_id) {
|
||||
$stmt = $this->db->prepare("SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC");
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
}
|
193
includes/Review.php
Normal file
193
includes/Review.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
class Review {
|
||||
private $db;
|
||||
private $notification;
|
||||
|
||||
public function __construct($db, $notification) {
|
||||
$this->db = $db;
|
||||
$this->notification = $notification;
|
||||
}
|
||||
|
||||
public function addReview($user_id, $product_id, $rating, $comment, $images = []) {
|
||||
// Start transaction
|
||||
$this->db->begin_transaction();
|
||||
|
||||
try {
|
||||
// Add review
|
||||
$stmt = $this->db->prepare("INSERT INTO reviews (user_id, product_id, rating, comment, created_at) VALUES (?, ?, ?, ?, NOW())");
|
||||
$stmt->bind_param('iiis', $user_id, $product_id, $rating, $comment);
|
||||
$stmt->execute();
|
||||
$review_id = $stmt->insert_id;
|
||||
|
||||
// Add review images if any
|
||||
if (!empty($images)) {
|
||||
$stmt = $this->db->prepare("INSERT INTO review_images (review_id, image_path) VALUES (?, ?)");
|
||||
foreach ($images as $image) {
|
||||
$stmt->bind_param('is', $review_id, $image);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Update product rating
|
||||
$this->updateProductRating($product_id);
|
||||
|
||||
// Notify product owner
|
||||
$this->notifyProductOwner($product_id, $user_id, $rating);
|
||||
|
||||
$this->db->commit();
|
||||
return $review_id;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function getProductReviews($product_id, $limit = 10, $offset = 0) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT r.*, u.username, u.avatar,
|
||||
GROUP_CONCAT(ri.image_path) as images
|
||||
FROM reviews r
|
||||
LEFT JOIN users u ON r.user_id = u.id
|
||||
LEFT JOIN review_images ri ON r.id = ri.review_id
|
||||
WHERE r.product_id = ?
|
||||
GROUP BY r.id
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->bind_param('iii', $product_id, $limit, $offset);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
public function getUserReviews($user_id, $limit = 10, $offset = 0) {
|
||||
$stmt = $this->db->prepare("
|
||||
SELECT r.*, p.name as product_name, p.image as product_image,
|
||||
GROUP_CONCAT(ri.image_path) as images
|
||||
FROM reviews r
|
||||
LEFT JOIN products p ON r.product_id = p.id
|
||||
LEFT JOIN review_images ri ON r.id = ri.review_id
|
||||
WHERE r.user_id = ?
|
||||
GROUP BY r.id
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
");
|
||||
$stmt->bind_param('iii', $user_id, $limit, $offset);
|
||||
$stmt->execute();
|
||||
return $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
}
|
||||
|
||||
public function updateReview($review_id, $user_id, $rating, $comment, $images = []) {
|
||||
$this->db->begin_transaction();
|
||||
|
||||
try {
|
||||
// Update review
|
||||
$stmt = $this->db->prepare("UPDATE reviews SET rating = ?, comment = ?, updated_at = NOW() WHERE id = ? AND user_id = ?");
|
||||
$stmt->bind_param('isii', $rating, $comment, $review_id, $user_id);
|
||||
$stmt->execute();
|
||||
|
||||
// Update images
|
||||
if (!empty($images)) {
|
||||
// Delete old images
|
||||
$stmt = $this->db->prepare("DELETE FROM review_images WHERE review_id = ?");
|
||||
$stmt->bind_param('i', $review_id);
|
||||
$stmt->execute();
|
||||
|
||||
// Add new images
|
||||
$stmt = $this->db->prepare("INSERT INTO review_images (review_id, image_path) VALUES (?, ?)");
|
||||
foreach ($images as $image) {
|
||||
$stmt->bind_param('is', $review_id, $image);
|
||||
$stmt->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Update product rating
|
||||
$stmt = $this->db->prepare("SELECT product_id FROM reviews WHERE id = ?");
|
||||
$stmt->bind_param('i', $review_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
$this->updateProductRating($result['product_id']);
|
||||
|
||||
$this->db->commit();
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteReview($review_id, $user_id) {
|
||||
$this->db->begin_transaction();
|
||||
|
||||
try {
|
||||
// Get product_id before deletion
|
||||
$stmt = $this->db->prepare("SELECT product_id FROM reviews WHERE id = ? AND user_id = ?");
|
||||
$stmt->bind_param('ii', $review_id, $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception('Review not found or unauthorized');
|
||||
}
|
||||
|
||||
// Delete review images
|
||||
$stmt = $this->db->prepare("DELETE FROM review_images WHERE review_id = ?");
|
||||
$stmt->bind_param('i', $review_id);
|
||||
$stmt->execute();
|
||||
|
||||
// Delete review
|
||||
$stmt = $this->db->prepare("DELETE FROM reviews WHERE id = ? AND user_id = ?");
|
||||
$stmt->bind_param('ii', $review_id, $user_id);
|
||||
$stmt->execute();
|
||||
|
||||
// Update product rating
|
||||
$this->updateProductRating($result['product_id']);
|
||||
|
||||
$this->db->commit();
|
||||
return true;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->db->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProductRating($product_id) {
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE products p
|
||||
SET rating = (
|
||||
SELECT AVG(rating)
|
||||
FROM reviews
|
||||
WHERE product_id = ?
|
||||
)
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->bind_param('ii', $product_id, $product_id);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
private function notifyProductOwner($product_id, $reviewer_id, $rating) {
|
||||
$stmt = $this->db->prepare("SELECT user_id FROM products WHERE id = ?");
|
||||
$stmt->bind_param('i', $product_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($result) {
|
||||
$message = $rating >= 4
|
||||
? "Someone left a positive review on your product!"
|
||||
: "You received a new review on your product.";
|
||||
|
||||
$this->notification->createNotification(
|
||||
$result['user_id'],
|
||||
'product_review',
|
||||
$message,
|
||||
[
|
||||
'product_id' => $product_id,
|
||||
'reviewer_id' => $reviewer_id,
|
||||
'rating' => $rating
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -113,4 +113,122 @@ class Security {
|
||||
public static function verifyPassword($password, $hash) {
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
public static function sanitize_input($data) {
|
||||
$data = trim($data);
|
||||
$data = stripslashes($data);
|
||||
$data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function generate_csrf_token() {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
public static function verify_csrf_token($token) {
|
||||
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
|
||||
public static function check_rate_limit($key, $limit = 5, $period = 300) {
|
||||
$cache_key = "rate_limit:{$key}";
|
||||
$current = isset($_SESSION[$cache_key]) ? $_SESSION[$cache_key] : ['count' => 0, 'timestamp' => time()];
|
||||
|
||||
if (time() - $current['timestamp'] > $period) {
|
||||
$current = ['count' => 1, 'timestamp' => time()];
|
||||
} else {
|
||||
$current['count']++;
|
||||
}
|
||||
|
||||
$_SESSION[$cache_key] = $current;
|
||||
return $current['count'] <= $limit;
|
||||
}
|
||||
|
||||
public static function is_password_strong($password) {
|
||||
// Minimum 8 characters
|
||||
if (strlen($password) < 8) return false;
|
||||
|
||||
// Must contain at least one uppercase letter
|
||||
if (!preg_match('/[A-Z]/', $password)) return false;
|
||||
|
||||
// Must contain at least one lowercase letter
|
||||
if (!preg_match('/[a-z]/', $password)) return false;
|
||||
|
||||
// Must contain at least one number
|
||||
if (!preg_match('/[0-9]/', $password)) return false;
|
||||
|
||||
// Must contain at least one special character
|
||||
if (!preg_match('/[!@#$%^&*()\-_=+{};:,<.>]/', $password)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function is_file_upload_safe($file) {
|
||||
$allowed_types = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
$max_size = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
// Check file type
|
||||
if (!in_array($file['type'], $allowed_types)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if ($file['size'] > $max_size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is actually an image
|
||||
if (!getimagesize($file['tmp_name'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function set_secure_headers() {
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'");
|
||||
}
|
||||
|
||||
public static function secure_session_start() {
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.cookie_secure', 1);
|
||||
ini_set('session.use_only_cookies', 1);
|
||||
ini_set('session.cookie_samesite', 'Strict');
|
||||
session_start();
|
||||
}
|
||||
|
||||
public static function log_security_event($event_type, $details) {
|
||||
$log_file = __DIR__ . '/../logs/security.log';
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
$user = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'anonymous';
|
||||
|
||||
$log_entry = sprintf(
|
||||
"[%s] %s - IP: %s, User: %s, Details: %s\n",
|
||||
$timestamp,
|
||||
$event_type,
|
||||
$ip,
|
||||
$user,
|
||||
json_encode($details)
|
||||
);
|
||||
|
||||
error_log($log_entry, 3, $log_file);
|
||||
}
|
||||
|
||||
public static function init_security() {
|
||||
self::secure_session_start();
|
||||
self::set_secure_headers();
|
||||
|
||||
// Force HTTPS
|
||||
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
|
||||
header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ if (getenv('DEBUG_MODE') === 'true') {
|
||||
$env = parse_ini_file(__DIR__ . '/../.env');
|
||||
|
||||
// Database configuration
|
||||
define('DB_SERVER', 'localhost');
|
||||
define('DB_USERNAME', 'momaher');
|
||||
define('DB_PASSWORD', 'Mohamed@9498#');
|
||||
define('DB_NAME', 'shubraveil_db');
|
||||
define('DB_SERVER', getenv('DB_SERVER') ?: 'localhost');
|
||||
define('DB_USERNAME', getenv('DB_USERNAME') ?: 'root');
|
||||
define('DB_PASSWORD', getenv('DB_PASSWORD') ?: '');
|
||||
define('DB_NAME', getenv('DB_NAME') ?: 'shubraveil_db');
|
||||
|
||||
// Site configuration
|
||||
define('SITE_NAME', 'ShubraVeil');
|
||||
define('SITE_URL', $env['SITE_URL']);
|
||||
define('SITE_URL', getenv('SITE_URL') ?: 'https://localhost/shubraveil');
|
||||
define('UPLOAD_PATH', __DIR__ . '/../uploads');
|
||||
define('ALLOWED_IMAGE_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
|
||||
define('MAX_IMAGE_SIZE', 5 * 1024 * 1024); // 5MB
|
||||
|
107
lang/ar.php
Normal file
107
lang/ar.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
return [
|
||||
// العامة
|
||||
'site_name' => 'شبرا فيل',
|
||||
'welcome' => 'مرحباً بك في شبرا فيل',
|
||||
'home' => 'الرئيسية',
|
||||
'products' => 'المنتجات',
|
||||
'about' => 'عن الشركة',
|
||||
'contact' => 'اتصل بنا',
|
||||
'search' => 'بحث',
|
||||
'cart' => 'سلة التسوق',
|
||||
'account' => 'حسابي',
|
||||
|
||||
// المصادقة
|
||||
'login' => 'تسجيل الدخول',
|
||||
'register' => 'تسجيل جديد',
|
||||
'logout' => 'تسجيل الخروج',
|
||||
'email' => 'البريد الإلكتروني',
|
||||
'password' => 'كلمة المرور',
|
||||
'confirm_password' => 'تأكيد كلمة المرور',
|
||||
'forgot_password' => 'نسيت كلمة المرور؟',
|
||||
'remember_me' => 'تذكرني',
|
||||
|
||||
// المنتجات
|
||||
'product_name' => 'اسم المنتج',
|
||||
'price' => 'السعر',
|
||||
'quantity' => 'الكمية',
|
||||
'description' => 'الوصف',
|
||||
'add_to_cart' => 'أضف إلى السلة',
|
||||
'out_of_stock' => 'نفذت الكمية',
|
||||
'reviews' => 'التقييمات',
|
||||
'write_review' => 'اكتب تقييماً',
|
||||
|
||||
// سلة التسوق
|
||||
'shopping_cart' => 'سلة التسوق',
|
||||
'total' => 'المجموع',
|
||||
'checkout' => 'إتمام الشراء',
|
||||
'continue_shopping' => 'مواصلة التسوق',
|
||||
'empty_cart' => 'السلة فارغة',
|
||||
|
||||
// الدفع
|
||||
'payment' => 'الدفع',
|
||||
'payment_method' => 'طريقة الدفع',
|
||||
'card_number' => 'رقم البطاقة',
|
||||
'expiry_date' => 'تاريخ الانتهاء',
|
||||
'cvv' => 'رمز التحقق',
|
||||
'pay_now' => 'ادفع الآن',
|
||||
|
||||
// الطلبات
|
||||
'orders' => 'الطلبات',
|
||||
'order_number' => 'رقم الطلب',
|
||||
'order_date' => 'تاريخ الطلب',
|
||||
'order_status' => 'حالة الطلب',
|
||||
'order_total' => 'إجمالي الطلب',
|
||||
|
||||
// الإشعارات
|
||||
'notifications' => 'الإشعارات',
|
||||
'no_notifications' => 'لا توجد إشعارات',
|
||||
'mark_as_read' => 'تعليم كمقروء',
|
||||
'mark_all_as_read' => 'تعليم الكل كمقروء',
|
||||
|
||||
// الملف الشخصي
|
||||
'profile' => 'الملف الشخصي',
|
||||
'edit_profile' => 'تعديل الملف الشخصي',
|
||||
'name' => 'الاسم',
|
||||
'phone' => 'رقم الهاتف',
|
||||
'address' => 'العنوان',
|
||||
'save_changes' => 'حفظ التغييرات',
|
||||
|
||||
// رسائل
|
||||
'success' => 'تم بنجاح',
|
||||
'error' => 'حدث خطأ',
|
||||
'confirm' => 'تأكيد',
|
||||
'cancel' => 'إلغاء',
|
||||
'loading' => 'جاري التحميل...',
|
||||
|
||||
// الأخطاء
|
||||
'required_field' => 'هذا الحقل مطلوب',
|
||||
'invalid_email' => 'البريد الإلكتروني غير صحيح',
|
||||
'password_mismatch' => 'كلمات المرور غير متطابقة',
|
||||
'invalid_credentials' => 'بيانات الدخول غير صحيحة',
|
||||
|
||||
// التقييمات
|
||||
'rating' => 'التقييم',
|
||||
'comment' => 'التعليق',
|
||||
'submit_review' => 'إرسال التقييم',
|
||||
'edit_review' => 'تعديل التقييم',
|
||||
'delete_review' => 'حذف التقييم',
|
||||
|
||||
// الشحن
|
||||
'shipping' => 'الشحن',
|
||||
'shipping_address' => 'عنوان الشحن',
|
||||
'shipping_method' => 'طريقة الشحن',
|
||||
'shipping_cost' => 'تكلفة الشحن',
|
||||
|
||||
// الفواتير
|
||||
'invoice' => 'الفاتورة',
|
||||
'invoice_number' => 'رقم الفاتورة',
|
||||
'invoice_date' => 'تاريخ الفاتورة',
|
||||
'download_invoice' => 'تحميل الفاتورة',
|
||||
|
||||
// الدعم
|
||||
'support' => 'الدعم الفني',
|
||||
'faq' => 'الأسئلة الشائعة',
|
||||
'contact_support' => 'اتصل بالدعم',
|
||||
'submit_ticket' => 'إرسال تذكرة',
|
||||
];
|
107
lang/en.php
Normal file
107
lang/en.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
return [
|
||||
// General
|
||||
'site_name' => 'ShubraVeil',
|
||||
'welcome' => 'Welcome to ShubraVeil',
|
||||
'home' => 'Home',
|
||||
'products' => 'Products',
|
||||
'about' => 'About',
|
||||
'contact' => 'Contact',
|
||||
'search' => 'Search',
|
||||
'cart' => 'Cart',
|
||||
'account' => 'My Account',
|
||||
|
||||
// Authentication
|
||||
'login' => 'Login',
|
||||
'register' => 'Register',
|
||||
'logout' => 'Logout',
|
||||
'email' => 'Email',
|
||||
'password' => 'Password',
|
||||
'confirm_password' => 'Confirm Password',
|
||||
'forgot_password' => 'Forgot Password?',
|
||||
'remember_me' => 'Remember Me',
|
||||
|
||||
// Products
|
||||
'product_name' => 'Product Name',
|
||||
'price' => 'Price',
|
||||
'quantity' => 'Quantity',
|
||||
'description' => 'Description',
|
||||
'add_to_cart' => 'Add to Cart',
|
||||
'out_of_stock' => 'Out of Stock',
|
||||
'reviews' => 'Reviews',
|
||||
'write_review' => 'Write a Review',
|
||||
|
||||
// Shopping Cart
|
||||
'shopping_cart' => 'Shopping Cart',
|
||||
'total' => 'Total',
|
||||
'checkout' => 'Checkout',
|
||||
'continue_shopping' => 'Continue Shopping',
|
||||
'empty_cart' => 'Your cart is empty',
|
||||
|
||||
// Payment
|
||||
'payment' => 'Payment',
|
||||
'payment_method' => 'Payment Method',
|
||||
'card_number' => 'Card Number',
|
||||
'expiry_date' => 'Expiry Date',
|
||||
'cvv' => 'CVV',
|
||||
'pay_now' => 'Pay Now',
|
||||
|
||||
// Orders
|
||||
'orders' => 'Orders',
|
||||
'order_number' => 'Order Number',
|
||||
'order_date' => 'Order Date',
|
||||
'order_status' => 'Order Status',
|
||||
'order_total' => 'Order Total',
|
||||
|
||||
// Notifications
|
||||
'notifications' => 'Notifications',
|
||||
'no_notifications' => 'No notifications',
|
||||
'mark_as_read' => 'Mark as Read',
|
||||
'mark_all_as_read' => 'Mark All as Read',
|
||||
|
||||
// Profile
|
||||
'profile' => 'Profile',
|
||||
'edit_profile' => 'Edit Profile',
|
||||
'name' => 'Name',
|
||||
'phone' => 'Phone',
|
||||
'address' => 'Address',
|
||||
'save_changes' => 'Save Changes',
|
||||
|
||||
// Messages
|
||||
'success' => 'Success',
|
||||
'error' => 'Error',
|
||||
'confirm' => 'Confirm',
|
||||
'cancel' => 'Cancel',
|
||||
'loading' => 'Loading...',
|
||||
|
||||
// Errors
|
||||
'required_field' => 'This field is required',
|
||||
'invalid_email' => 'Invalid email address',
|
||||
'password_mismatch' => 'Passwords do not match',
|
||||
'invalid_credentials' => 'Invalid credentials',
|
||||
|
||||
// Reviews
|
||||
'rating' => 'Rating',
|
||||
'comment' => 'Comment',
|
||||
'submit_review' => 'Submit Review',
|
||||
'edit_review' => 'Edit Review',
|
||||
'delete_review' => 'Delete Review',
|
||||
|
||||
// Shipping
|
||||
'shipping' => 'Shipping',
|
||||
'shipping_address' => 'Shipping Address',
|
||||
'shipping_method' => 'Shipping Method',
|
||||
'shipping_cost' => 'Shipping Cost',
|
||||
|
||||
// Invoices
|
||||
'invoice' => 'Invoice',
|
||||
'invoice_number' => 'Invoice Number',
|
||||
'invoice_date' => 'Invoice Date',
|
||||
'download_invoice' => 'Download Invoice',
|
||||
|
||||
// Support
|
||||
'support' => 'Support',
|
||||
'faq' => 'FAQ',
|
||||
'contact_support' => 'Contact Support',
|
||||
'submit_ticket' => 'Submit Ticket',
|
||||
];
|
Loading…
Reference in New Issue
Block a user