diff --git a/WALLET_INTEGRATION.md b/WALLET_INTEGRATION.md
new file mode 100644
index 0000000..902aaa6
--- /dev/null
+++ b/WALLET_INTEGRATION.md
@@ -0,0 +1,160 @@
+# راهنمای اتصال کیف پول به شبکه BSC
+
+## ویژگیهای پیادهسازی شده
+
+### 1. **Entity و Database**
+- ✅ Entity `Wallet` با فیلدهای کامل
+- ✅ رابطه One-to-Many با User
+- ✅ Migration برای جدول `wallets`
+- ✅ Repository با متدهای مفید
+
+### 2. **Backend API**
+- ✅ `WalletController` با endpoints کامل
+- ✅ `WalletService` برای منطق کسبوکار
+- ✅ `WalletConnectFormType` برای فرمها
+- ✅ اعتبارسنجی و امنیت
+
+### 3. **Frontend**
+- ✅ JavaScript برای اتصال کیف پولها
+- ✅ پشتیبانی از MetaMask و Trust Wallet
+- ✅ رابط کاربری در پروفایل
+- ✅ مدیریت کیف پولها (حذف، فعال/غیرفعال، تنظیم اصلی)
+
+### 4. **امکانات**
+- ✅ اتصال حداکثر 5 کیف پول per user
+- ✅ تنظیم کیف پول اصلی
+- ✅ فعال/غیرفعال کردن کیف پولها
+- ✅ حذف کیف پولها
+- ✅ تأیید مالکیت با امضای دیجیتال
+- ✅ نمایش آدرس کوتاه شده
+
+## API Endpoints
+
+### اتصال کیف پول
+```
+POST /api/wallet/connect
+{
+ "walletAddress": "0x...",
+ "walletType": "MetaMask",
+ "signature": "..."
+}
+```
+
+### لیست کیف پولها
+```
+GET /api/wallet/list
+```
+
+### تنظیم کیف پول اصلی
+```
+PUT /api/wallet/{id}/set-primary
+```
+
+### تغییر وضعیت
+```
+PUT /api/wallet/{id}/toggle-status
+```
+
+### حذف کیف پول
+```
+DELETE /api/wallet/{id}
+```
+
+### دریافت پیام برای امضا
+```
+GET /api/wallet/sign-message
+```
+
+## نحوه استفاده
+
+### 1. **برای کاربران**
+1. وارد پروفایل خود شوید
+2. به بخش "مدیریت کیف پولها" بروید
+3. نوع کیف پول خود را انتخاب کنید
+4. روی "اتصال کیف پول" کلیک کنید
+5. در کیف پول خود تراکنش را تأیید کنید
+6. کیف پول شما متصل خواهد شد
+
+### 2. **برای توسعهدهندگان**
+
+#### استفاده از WalletService
+```php
+// اتصال کیف پول
+$result = $walletService->connectWallet($user, $address, $type, $signature);
+
+// دریافت کیف پولهای کاربر
+$wallets = $walletService->getUserWallets($user);
+
+// تنظیم کیف پول اصلی
+$walletService->setPrimaryWallet($user, $walletId);
+```
+
+#### استفاده از Repository
+```php
+// پیدا کردن کیف پول اصلی
+$primaryWallet = $walletRepository->findPrimaryWalletByUser($user);
+
+// بررسی وجود آدرس
+$exists = $walletRepository->isWalletAddressExists($address);
+```
+
+## امنیت
+
+### 1. **اعتبارسنجی**
+- بررسی فرمت آدرس کیف پول
+- تأیید امضای دیجیتال
+- محدودیت تعداد کیف پولها
+
+### 2. **مجوزها**
+- فقط کاربران وارد شده
+- دسترسی فقط به کیف پولهای خود
+- تأیید برای عملیات حساس
+
+## نکات مهم
+
+### 1. **Web3 Integration**
+- نیاز به MetaMask یا Trust Wallet
+- پشتیبانی از شبکه BSC
+- امضای پیام برای تأیید مالکیت
+
+### 2. **Database**
+- جدول `wallets` ایجاد شده
+- رابطه با جدول `user`
+- Indexes برای عملکرد بهتر
+
+### 3. **Frontend**
+- JavaScript در `/public/js/wallet-connect.js`
+- CSS در template پروفایل
+- آیکون کیف پول اضافه شده
+
+## مراحل بعدی (اختیاری)
+
+1. **بهبود امنیت**
+ - پیادهسازی تأیید امضای واقعی با Web3
+ - اضافه کردن rate limiting
+ - لاگگیری عملیات
+
+2. **ویژگیهای اضافی**
+ - نمایش موجودی کیف پول
+ - تاریخچه تراکنشها
+ - اتصال به سایر شبکهها
+
+3. **بهبود UI/UX**
+ - انیمیشنهای بهتر
+ - پیامهای خطای دقیقتر
+ - راهنمای استفاده
+
+## تست
+
+برای تست عملکرد:
+1. یک کاربر ایجاد کنید
+2. وارد پروفایل شوید
+3. کیف پول MetaMask را اتصال دهید
+4. عملیات مختلف را تست کنید
+
+## پشتیبانی
+
+در صورت بروز مشکل:
+1. Console مرورگر را بررسی کنید
+2. لاگهای Symfony را چک کنید
+3. اتصال Web3 را بررسی کنید
diff --git a/assets/app.js b/assets/app.js
index b81903c..c5b1619 100644
--- a/assets/app.js
+++ b/assets/app.js
@@ -10,5 +10,6 @@ import './bootstrap.js';
import './styles/global.scss';
import './styles/app.css';
-// the bootstrap module doesn't export/return anything
-require('bootstrap');
+// Import Bootstrap and make it globally available
+import * as bootstrap from 'bootstrap';
+window.bootstrap = bootstrap;
diff --git a/assets/controllers/installation_controller.js b/assets/controllers/installation_controller.js
index 16ede19..4e03b09 100644
--- a/assets/controllers/installation_controller.js
+++ b/assets/controllers/installation_controller.js
@@ -315,78 +315,160 @@ export default class extends Controller {
}
window.showApiInstall = function() {
- document.getElementById('apiInstallSection').style.display = '';
- document.getElementById('ftpOption').style.display = 'none';
+ const apiInstallSection = document.getElementById('apiInstallSection');
+ const ftpOption = document.getElementById('ftpOption');
+ const ftpStep1 = document.getElementById('ftpStep1');
+ const ftpStep2 = document.getElementById('ftpStep2');
+ const ftpStep3 = document.getElementById('ftpStep3');
+
+ if (apiInstallSection) {
+ apiInstallSection.style.display = '';
+ }
+ if (ftpOption) {
+ ftpOption.style.display = 'none';
+ }
// ریست مراحل FTP
- document.getElementById('ftpStep1').classList.remove('step-hidden');
- document.getElementById('ftpStep2').classList.add('step-hidden');
- document.getElementById('ftpStep3').classList.add('step-hidden');
+ if (ftpStep1) {
+ ftpStep1.classList.remove('step-hidden');
+ }
+ if (ftpStep2) {
+ ftpStep2.classList.add('step-hidden');
+ }
+ if (ftpStep3) {
+ ftpStep3.classList.add('step-hidden');
+ }
}
window.showFtpInstall = function() {
- document.getElementById('apiInstallSection').style.display = 'none';
+ const apiInstallSection = document.getElementById('apiInstallSection');
const ftpOption = document.getElementById('ftpOption');
- ftpOption.style.display = '';
- ftpOption.classList.remove('step-hidden');
+ const ftpStep1 = document.getElementById('ftpStep1');
+ const ftpStep2 = document.getElementById('ftpStep2');
+ const ftpStep3 = document.getElementById('ftpStep3');
+
+ if (apiInstallSection) {
+ apiInstallSection.style.display = 'none';
+ }
+ if (ftpOption) {
+ ftpOption.style.display = '';
+ ftpOption.classList.remove('step-hidden');
+ }
// ریست مراحل FTP
- document.getElementById('ftpStep1').classList.remove('step-hidden');
- document.getElementById('ftpStep2').classList.add('step-hidden');
- document.getElementById('ftpStep3').classList.add('step-hidden');
+ if (ftpStep1) {
+ ftpStep1.classList.remove('step-hidden');
+ }
+ if (ftpStep2) {
+ ftpStep2.classList.add('step-hidden');
+ }
+ if (ftpStep3) {
+ ftpStep3.classList.add('step-hidden');
+ }
}
function showBootstrapAlert(message, type = 'danger', parentId = 'ftpStep1') {
// type: 'danger', 'success', 'info', ...
let parent = document.getElementById(parentId);
+ if (!parent) return;
+
let oldAlert = parent.querySelector('.bootstrap-alert');
if (oldAlert) oldAlert.remove();
+
+ let stepContent = parent.querySelector('.step-content');
+ if (!stepContent) return;
+
let div = document.createElement('div');
div.className = `alert alert-${type} bootstrap-alert`;
div.style.marginTop = '10px';
div.innerHTML = message;
- parent.querySelector('.step-content').prepend(div);
+ stepContent.prepend(div);
}
window.showFtpDbStep = function() {
- const ftpHost = document.getElementById('ftpHost').value;
- const ftpUsername = document.getElementById('ftpUsername').value;
- const ftpPassword = document.getElementById('ftpPassword').value;
+ const ftpHost = document.getElementById('ftpHost');
+ const ftpUsername = document.getElementById('ftpUsername');
+ const ftpPassword = document.getElementById('ftpPassword');
+
if (!ftpHost || !ftpUsername || !ftpPassword) {
showBootstrapAlert('تمام فیلدهای FTP الزامی است', 'danger', 'ftpStep1');
return;
}
+
+ const hostValue = ftpHost.value;
+ const usernameValue = ftpUsername.value;
+ const passwordValue = ftpPassword.value;
+
+ if (!hostValue || !usernameValue || !passwordValue) {
+ showBootstrapAlert('تمام فیلدهای FTP الزامی است', 'danger', 'ftpStep1');
+ return;
+ }
+
// حذف هشدار قبلی
showBootstrapAlert('', 'danger', 'ftpStep1');
- document.getElementById('ftpStep1').classList.add('step-hidden');
- document.getElementById('ftpStep2').classList.remove('step-hidden');
+
+ const ftpStep1 = document.getElementById('ftpStep1');
+ const ftpStep2 = document.getElementById('ftpStep2');
+
+ if (ftpStep1) {
+ ftpStep1.classList.add('step-hidden');
+ }
+ if (ftpStep2) {
+ ftpStep2.classList.remove('step-hidden');
+ }
}
-document.addEventListener('DOMContentLoaded', function() {
- showApiInstall();
-});
+function initializeInstallationController() {
+ // فقط در صفحات نصب اجرا شود
+ if (document.getElementById('apiInstallSection')) {
+ showApiInstall();
+ }
+}
+
+// اجرا در هر دو حالت
+document.addEventListener('DOMContentLoaded', initializeInstallationController);
+document.addEventListener('turbo:load', initializeInstallationController);
window.startFtpInstall = async function() {
// مرحله دوم: اطلاعات دیتابیس
- const ftpHost = document.getElementById('ftpHost').value;
- const ftpUsername = document.getElementById('ftpUsername').value;
- const ftpPassword = document.getElementById('ftpPassword').value;
- const ftpPort = document.getElementById('ftpPort').value;
- const domain = document.querySelector('[data-installation-target="host"]').value;
- const databaseName = document.getElementById('ftpDatabaseName').value;
- const databaseUser = document.getElementById('ftpDatabaseUser').value;
- const databasePassword = document.getElementById('ftpDatabasePassword').value;
- if (!databaseName || !databaseUser || !databasePassword) {
+ const ftpHost = document.getElementById('ftpHost');
+ const ftpUsername = document.getElementById('ftpUsername');
+ const ftpPassword = document.getElementById('ftpPassword');
+ const ftpPort = document.getElementById('ftpPort');
+ const domainElement = document.querySelector('[data-installation-target="host"]');
+ const databaseName = document.getElementById('ftpDatabaseName');
+ const databaseUser = document.getElementById('ftpDatabaseUser');
+ const databasePassword = document.getElementById('ftpDatabasePassword');
+
+ if (!ftpHost || !ftpUsername || !ftpPassword || !ftpPort || !domainElement ||
+ !databaseName || !databaseUser || !databasePassword) {
+ showBootstrapAlert('تمام فیلدهای مورد نیاز یافت نشد', 'danger', 'ftpStep2');
+ return;
+ }
+
+ const hostValue = ftpHost.value;
+ const usernameValue = ftpUsername.value;
+ const passwordValue = ftpPassword.value;
+ const portValue = ftpPort.value;
+ const domainValue = domainElement.value;
+ const dbNameValue = databaseName.value;
+ const dbUserValue = databaseUser.value;
+ const dbPasswordValue = databasePassword.value;
+
+ if (!dbNameValue || !dbUserValue || !dbPasswordValue) {
showBootstrapAlert('تمام فیلدهای دیتابیس الزامی است', 'danger', 'ftpStep2');
return;
}
+
// حذف هشدار قبلی
showBootstrapAlert('', 'danger', 'ftpStep2');
+
const payload = {
- ftp_host: ftpHost,
- ftp_username: ftpUsername,
- ftp_password: ftpPassword,
- ftp_port: ftpPort,
- domain: domain,
- database_name: databaseName,
- database_user: databaseUser,
- database_password: databasePassword
+ ftp_host: hostValue,
+ ftp_username: usernameValue,
+ ftp_password: passwordValue,
+ ftp_port: portValue,
+ domain: domainValue,
+ database_name: dbNameValue,
+ database_user: dbUserValue,
+ database_password: dbPasswordValue
};
+
try {
const response = await fetch('/api/installation/ftp-install', {
method: 'POST',
@@ -397,21 +479,42 @@ window.startFtpInstall = async function() {
body: JSON.stringify(payload)
});
const result = await response.json();
- document.getElementById('ftpStep2').classList.add('step-hidden');
- document.getElementById('ftpStep3').classList.remove('step-hidden');
+
+ const ftpStep2 = document.getElementById('ftpStep2');
+ const ftpStep3 = document.getElementById('ftpStep3');
const msg = document.getElementById('ftpResultMsg');
- if (result.success) {
- msg.className = 'status-message status-success';
- msg.innerHTML = 'نصب با موفقیت از طریق FTP انجام شد!';
- } else {
- msg.className = 'status-message status-error';
- msg.innerHTML = 'خطا: ' + (result.error || 'خطای نامشخص');
+
+ if (ftpStep2) {
+ ftpStep2.classList.add('step-hidden');
+ }
+ if (ftpStep3) {
+ ftpStep3.classList.remove('step-hidden');
+ }
+
+ if (msg) {
+ if (result.success) {
+ msg.className = 'status-message status-success';
+ msg.innerHTML = 'نصب با موفقیت از طریق FTP انجام شد!';
+ } else {
+ msg.className = 'status-message status-error';
+ msg.innerHTML = 'خطا: ' + (result.error || 'خطای نامشخص');
+ }
}
} catch (error) {
- document.getElementById('ftpStep2').classList.add('step-hidden');
- document.getElementById('ftpStep3').classList.remove('step-hidden');
+ const ftpStep2 = document.getElementById('ftpStep2');
+ const ftpStep3 = document.getElementById('ftpStep3');
const msg = document.getElementById('ftpResultMsg');
- msg.className = 'status-message status-error';
- msg.innerHTML = 'خطا در ارتباط با سرور: ' + error.message;
+
+ if (ftpStep2) {
+ ftpStep2.classList.add('step-hidden');
+ }
+ if (ftpStep3) {
+ ftpStep3.classList.remove('step-hidden');
+ }
+
+ if (msg) {
+ msg.className = 'status-message status-error';
+ msg.innerHTML = 'خطا در ارتباط با سرور: ' + error.message;
+ }
}
}
\ No newline at end of file
diff --git a/assets/controllers/wallet_connect_controller.js b/assets/controllers/wallet_connect_controller.js
new file mode 100644
index 0000000..1161e1b
--- /dev/null
+++ b/assets/controllers/wallet_connect_controller.js
@@ -0,0 +1,321 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["walletType", "walletAddress", "connectBtn", "signature", "modal", "modalTitle", "modalBody"]
+ static values = {
+ signMessage: String,
+ csrfToken: String
+ }
+
+ connect() {
+ console.log('Wallet Connect Controller connected');
+ this.selectedWallet = null;
+ this.walletAddress = null;
+ this.signature = null;
+ }
+
+ async onWalletTypeChange(event) {
+ this.selectedWallet = event.target.value;
+ console.log('Selected wallet type:', this.selectedWallet);
+
+ if (this.selectedWallet) {
+ this.connectBtnTarget.disabled = false;
+ this.connectBtnTarget.textContent = 'اتصال کیف پول';
+ } else {
+ this.connectBtnTarget.disabled = true;
+ this.connectBtnTarget.textContent = 'نوع کیف پول را انتخاب کنید';
+ }
+ }
+
+ async connectWallet() {
+ if (!this.selectedWallet) {
+ this.showMessage('لطفاً نوع کیف پول را انتخاب کنید', 'error');
+ return;
+ }
+
+ try {
+ this.connectBtnTarget.disabled = true;
+ this.connectBtnTarget.textContent = 'در حال اتصال...';
+
+ // Check if Web3 is available
+ if (!window.ethereum) {
+ this.showMessage('کیف پول Web3 یافت نشد. لطفاً MetaMask یا Trust Wallet را نصب کنید.', 'error');
+ return;
+ }
+
+ // Connect to wallet
+ const accounts = await window.ethereum.request({
+ method: 'eth_requestAccounts'
+ });
+
+ if (accounts.length === 0) {
+ this.showMessage('هیچ حساب کیف پولی یافت نشد', 'error');
+ return;
+ }
+
+ this.walletAddress = accounts[0];
+ console.log('Connected wallet address:', this.walletAddress);
+
+ // Get sign message
+ const signMessage = this.signMessageValue || 'Please sign this message to connect your wallet';
+
+ // Sign message
+ this.signature = await this.signMessage(signMessage);
+ if (!this.signature) {
+ return;
+ }
+
+ // Submit to server
+ await this.submitWalletConnection();
+
+ } catch (error) {
+ console.error('Error connecting wallet:', error);
+ this.showMessage('خطا در اتصال کیف پول: ' + error.message, 'error');
+ } finally {
+ this.connectBtnTarget.disabled = false;
+ this.connectBtnTarget.textContent = 'اتصال کیف پول';
+ }
+ }
+
+ async signMessage(message) {
+ try {
+ const signature = await window.ethereum.request({
+ method: 'personal_sign',
+ params: [message, this.walletAddress]
+ });
+ return signature;
+ } catch (error) {
+ console.error('Error signing message:', error);
+ this.showMessage('خطا در امضای پیام: ' + error.message, 'error');
+ return null;
+ }
+ }
+
+ async submitWalletConnection() {
+ try {
+ const formData = {
+ walletAddress: this.walletAddress,
+ walletType: this.selectedWallet,
+ signature: this.signature
+ };
+
+ console.log('Submitting wallet connection:', formData);
+
+ const response = await fetch('/api/wallet/connect', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': this.csrfTokenValue || ''
+ },
+ body: JSON.stringify(formData)
+ });
+
+ console.log('Response status:', response.status);
+ const data = await response.json();
+ console.log('Response data:', data);
+
+ if (data.success) {
+ this.showMessage(data.message, 'success');
+ // Reload page to show updated wallet list
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ } else {
+ this.showMessage(data.message, 'error');
+ }
+
+ } catch (error) {
+ console.error('Error submitting wallet connection:', error);
+ this.showMessage('خطا در ارسال اطلاعات: ' + error.message, 'error');
+ }
+ }
+
+ async setPrimaryWallet(walletId) {
+ if (!confirm('آیا میخواهید این کیف پول را به عنوان اصلی تنظیم کنید؟')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/wallet/${walletId}/set-primary`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': this.csrfTokenValue || ''
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showMessage(data.message, 'success');
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ } else {
+ this.showMessage(data.message, 'error');
+ }
+
+ } catch (error) {
+ console.error('Error setting primary wallet:', error);
+ this.showMessage('خطا در تنظیم کیف پول اصلی: ' + error.message, 'error');
+ }
+ }
+
+ async toggleWalletStatus(walletId) {
+ try {
+ const response = await fetch(`/api/wallet/${walletId}/toggle-status`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': this.csrfTokenValue || ''
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showMessage(data.message, 'success');
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ } else {
+ this.showMessage(data.message, 'error');
+ }
+
+ } catch (error) {
+ console.error('Error toggling wallet status:', error);
+ this.showMessage('خطا در تغییر وضعیت کیف پول: ' + error.message, 'error');
+ }
+ }
+
+ async deleteWallet(walletId) {
+ if (!confirm('آیا مطمئن هستید که میخواهید این کیف پول را حذف کنید؟')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/api/wallet/${walletId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': this.csrfTokenValue || ''
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showMessage(data.message, 'success');
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ } else {
+ this.showMessage(data.message, 'error');
+ }
+
+ } catch (error) {
+ console.error('Error deleting wallet:', error);
+ this.showMessage('خطا در حذف کیف پول: ' + error.message, 'error');
+ }
+ }
+
+ showMessage(message, type = 'info') {
+ // Create modal if it doesn't exist
+ if (!this.hasModalTarget) {
+ this.createModal();
+ }
+
+ // Get modal elements directly from DOM
+ const modalTitle = document.querySelector('#walletModal .modal-title');
+ const modalBody = document.querySelector('#walletModal .modal-body');
+ const modalElement = document.querySelector('#walletModal');
+
+ if (!modalTitle || !modalBody || !modalElement) {
+ console.error('Modal elements not found');
+ return;
+ }
+
+ // Set modal content
+ modalTitle.textContent = type === 'success' ? 'موفقیت' :
+ type === 'error' ? 'خطا' : 'اطلاعات';
+
+ modalBody.innerHTML = `
+
+ ${message}
+
+ `;
+
+ // Show modal
+ if (window.bootstrap && window.bootstrap.Modal) {
+ const modal = new window.bootstrap.Modal(modalElement);
+ modal.show();
+ } else {
+ // Fallback: show modal using basic CSS
+ modalElement.style.display = 'block';
+ modalElement.classList.add('show');
+ document.body.classList.add('modal-open');
+
+ // Add backdrop
+ const backdrop = document.createElement('div');
+ backdrop.className = 'modal-backdrop fade show';
+ backdrop.id = 'walletModalBackdrop';
+ document.body.appendChild(backdrop);
+
+ // Close modal when clicking backdrop
+ backdrop.addEventListener('click', () => {
+ this.hideModal();
+ });
+
+ // Close modal when clicking close button
+ const closeBtn = modalElement.querySelector('[data-bs-dismiss="modal"]');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => {
+ this.hideModal();
+ });
+ }
+ }
+ }
+
+ createModal() {
+ // Check if modal already exists
+ if (document.querySelector('#walletModal')) {
+ return;
+ }
+
+ const modalHtml = `
+
+ `;
+
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ }
+
+ hideModal() {
+ const modalElement = document.querySelector('#walletModal');
+ const backdrop = document.querySelector('#walletModalBackdrop');
+
+ if (modalElement) {
+ modalElement.style.display = 'none';
+ modalElement.classList.remove('show');
+ }
+
+ if (backdrop) {
+ backdrop.remove();
+ }
+
+ document.body.classList.remove('modal-open');
+ }
+}
diff --git a/assets/styles/app.css b/assets/styles/app.css
index b903606..a9bb020 100644
--- a/assets/styles/app.css
+++ b/assets/styles/app.css
@@ -63,3 +63,62 @@ pre {
padding: 0.9rem 0.8rem;
color: blue;
}
+
+/* استایلهای RTL برای Modal */
+#walletModal {
+ direction: rtl;
+ text-align: right;
+}
+
+#walletModal .modal-content {
+ font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
+ direction: rtl;
+ text-align: right;
+}
+
+#walletModal .modal-header {
+ direction: rtl;
+ text-align: right;
+ border-bottom: 1px solid #dee2e6;
+}
+
+#walletModal .modal-title {
+ direction: rtl;
+ text-align: right;
+ font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
+ font-weight: 600;
+}
+
+#walletModal .btn-close {
+ margin-left: 0;
+ margin-right: auto;
+ order: -1;
+}
+
+#walletModal .modal-body {
+ direction: rtl;
+ text-align: right;
+ font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
+}
+
+#walletModal .modal-footer {
+ direction: rtl;
+ text-align: right;
+ border-top: 1px solid #dee2e6;
+}
+
+#walletModal .modal-footer .btn {
+ font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
+ direction: rtl;
+}
+
+#walletModal .alert {
+ direction: rtl;
+ text-align: right;
+ font-family: 'Yekan Bakh FaNum', 'Tahoma', 'Arial', sans-serif;
+}
+
+/* استایلهای backdrop برای RTL */
+.modal-backdrop {
+ direction: rtl;
+}
diff --git a/composer.json b/composer.json
index 562b54a..fe8940f 100644
--- a/composer.json
+++ b/composer.json
@@ -12,6 +12,7 @@
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"easycorp/easyadmin-bundle": "^4.20",
+ "erusev/parsedown": "*",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.0",
"symfony/apache-pack": "^1.0",
@@ -45,6 +46,7 @@
"symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "7.2.*",
"twig/extra-bundle": "^2.12|^3.0",
+ "twig/markdown-extra": "^3.21",
"twig/twig": "^2.12|^3.0"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 60e3723..a395382 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "bf4898b8b7af1fcb2f85b07fb389668f",
+ "content-hash": "3e1d441e93318caa54bc22a2b9fd60e6",
"packages": [
{
"name": "doctrine/cache",
@@ -1396,6 +1396,56 @@
],
"time": "2024-12-27T00:36:43+00:00"
},
+ {
+ "name": "erusev/parsedown",
+ "version": "1.7.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown.git",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Parsedown": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "Parser for Markdown.",
+ "homepage": "http://parsedown.org",
+ "keywords": [
+ "markdown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown/issues",
+ "source": "https://github.com/erusev/parsedown/tree/1.7.x"
+ },
+ "time": "2019-12-30T22:54:17+00:00"
+ },
{
"name": "monolog/monolog",
"version": "3.8.1",
@@ -7771,6 +7821,78 @@
],
"time": "2024-12-29T10:29:59+00:00"
},
+ {
+ "name": "twig/markdown-extra",
+ "version": "v3.21.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twigphp/markdown-extra.git",
+ "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/f4616e1dd375209dacf6026f846e6b537d036ce4",
+ "reference": "f4616e1dd375209dacf6026f846e6b537d036ce4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "twig/twig": "^3.13|^4.0"
+ },
+ "require-dev": {
+ "erusev/parsedown": "dev-master as 1.x-dev",
+ "league/commonmark": "^1.0|^2.0",
+ "league/html-to-markdown": "^4.8|^5.0",
+ "michelf/php-markdown": "^1.8|^2.0",
+ "symfony/phpunit-bridge": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Twig\\Extra\\Markdown\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com",
+ "homepage": "http://fabien.potencier.org",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "A Twig extension for Markdown",
+ "homepage": "https://twig.symfony.com",
+ "keywords": [
+ "html",
+ "markdown",
+ "twig"
+ ],
+ "support": {
+ "source": "https://github.com/twigphp/markdown-extra/tree/v3.21.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/twig/twig",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-31T20:45:36+00:00"
+ },
{
"name": "twig/twig",
"version": "v3.18.0",
@@ -10182,7 +10304,7 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
@@ -10190,6 +10312,6 @@
"ext-ctype": "*",
"ext-iconv": "*"
},
- "platform-dev": [],
+ "platform-dev": {},
"plugin-api-version": "2.6.0"
}
diff --git a/config/packages/csrf.yaml b/config/packages/csrf.yaml
index 40d4040..c102b17 100644
--- a/config/packages/csrf.yaml
+++ b/config/packages/csrf.yaml
@@ -9,3 +9,4 @@ framework:
- submit
- authenticate
- logout
+ enabled: true
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index a2080b4..09763d7 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -9,10 +9,6 @@ security:
entity:
class: App\Entity\User
property: email
- customer_provider:
- entity:
- class: App\Entity\Customer
- property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
@@ -29,7 +25,7 @@ security:
customer:
pattern: ^/customer
lazy: true
- provider: customer_provider
+ provider: app_user_provider
form_login:
login_path: customer_login
check_path: /customer/login_check
@@ -45,25 +41,14 @@ security:
path: customer_logout
target: app_home
main:
- pattern: ^/(?!customer)
- lazy: true
- provider: app_user_provider
- form_login:
- # "app_login" is the name of the route created previously
- login_path: login
- check_path: login
-
- logout:
- path: logout
- customer_global:
pattern: ^/
lazy: true
- provider: customer_provider
+ provider: app_user_provider
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week
path: /
- always_remember_me: false
+ always_remember_me: true
logout:
path: customer_logout
target: app_home
diff --git a/config/services.yaml b/config/services.yaml
index a3de766..82618cf 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -27,3 +27,7 @@ services:
twigFunctions:
class: App\Service\twigFunctions
arguments: [ "@doctrine.orm.entity_manager" ]
+
+ # Markdown extension for Twig
+ App\Twig\MarkdownExtension:
+ tags: ['twig.extension']
\ No newline at end of file
diff --git a/migrations/Version20250726075416.php b/migrations/Version20250726075416.php
deleted file mode 100644
index bfc2143..0000000
--- a/migrations/Version20250726075416.php
+++ /dev/null
@@ -1,45 +0,0 @@
-addSql('CREATE TABLE oauth_access_tokens (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, client_id INT NOT NULL, identifier VARCHAR(255) NOT NULL, expiry_date_time DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', scopes JSON NOT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_CA42527C772E836A (identifier), INDEX IDX_CA42527CA76ED395 (user_id), INDEX IDX_CA42527C19EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
- $this->addSql('CREATE TABLE oauth_authorization_codes (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, client_id INT NOT NULL, identifier VARCHAR(255) NOT NULL, expiry_date_time DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', scopes JSON NOT NULL, redirect_uri VARCHAR(255) NOT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_98A471C4772E836A (identifier), INDEX IDX_98A471C4A76ED395 (user_id), INDEX IDX_98A471C419EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
- $this->addSql('CREATE TABLE oauth_clients (id INT AUTO_INCREMENT NOT NULL, client_id VARCHAR(255) NOT NULL, client_secret VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, redirect_uris LONGTEXT NOT NULL, is_active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_13CE810119EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
- $this->addSql('ALTER TABLE oauth_access_tokens ADD CONSTRAINT FK_CA42527CA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
- $this->addSql('ALTER TABLE oauth_access_tokens ADD CONSTRAINT FK_CA42527C19EB6921 FOREIGN KEY (client_id) REFERENCES oauth_clients (id)');
- $this->addSql('ALTER TABLE oauth_authorization_codes ADD CONSTRAINT FK_98A471C4A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
- $this->addSql('ALTER TABLE oauth_authorization_codes ADD CONSTRAINT FK_98A471C419EB6921 FOREIGN KEY (client_id) REFERENCES oauth_clients (id)');
- $this->addSql('CREATE UNIQUE INDEX UNIQ_5A8A6C8DF47645AE ON post (url)');
- }
-
- public function down(Schema $schema): void
- {
- // this down() migration is auto-generated, please modify it to your needs
- $this->addSql('ALTER TABLE oauth_access_tokens DROP FOREIGN KEY FK_CA42527CA76ED395');
- $this->addSql('ALTER TABLE oauth_access_tokens DROP FOREIGN KEY FK_CA42527C19EB6921');
- $this->addSql('ALTER TABLE oauth_authorization_codes DROP FOREIGN KEY FK_98A471C4A76ED395');
- $this->addSql('ALTER TABLE oauth_authorization_codes DROP FOREIGN KEY FK_98A471C419EB6921');
- $this->addSql('DROP TABLE oauth_access_tokens');
- $this->addSql('DROP TABLE oauth_authorization_codes');
- $this->addSql('DROP TABLE oauth_clients');
- $this->addSql('DROP INDEX UNIQ_5A8A6C8DF47645AE ON post');
- }
-}
diff --git a/migrations/Version20250903164119.php b/migrations/Version20250903164119.php
deleted file mode 100644
index 0c5d865..0000000
--- a/migrations/Version20250903164119.php
+++ /dev/null
@@ -1,37 +0,0 @@
-addSql('CREATE TABLE customer (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, phone VARCHAR(20) NOT NULL, is_active TINYINT(1) NOT NULL, email_verified_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, last_login_at DATETIME DEFAULT NULL, subscription_type VARCHAR(50) DEFAULT NULL, subscription_expires_at DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_CUSTOMER_EMAIL (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
- $this->addSql('CREATE TABLE password_reset_token (id INT AUTO_INCREMENT NOT NULL, customer_id INT NOT NULL, token VARCHAR(255) NOT NULL, expires_at DATETIME NOT NULL, used_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, INDEX IDX_6B7BA4B69395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
- $this->addSql('ALTER TABLE password_reset_token ADD CONSTRAINT FK_6B7BA4B69395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id)');
- $this->addSql('CREATE UNIQUE INDEX UNIQ_5A8A6C8DF47645AE ON post (url)');
- }
-
- public function down(Schema $schema): void
- {
- // this down() migration is auto-generated, please modify it to your needs
- $this->addSql('ALTER TABLE password_reset_token DROP FOREIGN KEY FK_6B7BA4B69395C3F3');
- $this->addSql('DROP TABLE customer');
- $this->addSql('DROP TABLE password_reset_token');
- $this->addSql('DROP INDEX UNIQ_5A8A6C8DF47645AE ON post');
- }
-}
diff --git a/migrations/Version20250904051657.php b/migrations/Version20250904051657.php
new file mode 100644
index 0000000..09e6d17
--- /dev/null
+++ b/migrations/Version20250904051657.php
@@ -0,0 +1,59 @@
+addSql('CREATE TABLE answer (id INT AUTO_INCREMENT NOT NULL, question_id INT NOT NULL, author_id INT NOT NULL, content LONGTEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, is_accepted TINYINT(1) NOT NULL, votes INT NOT NULL, is_active TINYINT(1) NOT NULL, INDEX IDX_DADD4A251E27F6BF (question_id), INDEX IDX_DADD4A25F675F31B (author_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE answer_vote (id INT AUTO_INCREMENT NOT NULL, answer_id INT NOT NULL, user_id INT NOT NULL, is_upvote TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_43B66A4AA334807 (answer_id), INDEX IDX_43B66A4A76ED395 (user_id), UNIQUE INDEX unique_answer_user_vote (answer_id, user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question (id INT AUTO_INCREMENT NOT NULL, author_id INT NOT NULL, title VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, is_solved TINYINT(1) NOT NULL, views INT NOT NULL, votes INT NOT NULL, is_active TINYINT(1) NOT NULL, INDEX IDX_B6F7494EF675F31B (author_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_tag (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(50) NOT NULL, description VARCHAR(255) DEFAULT NULL, color VARCHAR(7) DEFAULT NULL, usage_count INT NOT NULL, is_active TINYINT(1) NOT NULL, UNIQUE INDEX UNIQ_339D56FB5E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_tag_relation (id INT AUTO_INCREMENT NOT NULL, question_id INT NOT NULL, tag_id INT NOT NULL, INDEX IDX_C752C7D01E27F6BF (question_id), INDEX IDX_C752C7D0BAD26311 (tag_id), UNIQUE INDEX unique_question_tag (question_id, tag_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_vote (id INT AUTO_INCREMENT NOT NULL, question_id INT NOT NULL, user_id INT NOT NULL, is_upvote TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_4FE688B1E27F6BF (question_id), INDEX IDX_4FE688BA76ED395 (user_id), UNIQUE INDEX unique_question_user_vote (question_id, user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_DADD4A251E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_DADD4A25F675F31B FOREIGN KEY (author_id) REFERENCES customer (id)');
+ $this->addSql('ALTER TABLE answer_vote ADD CONSTRAINT FK_43B66A4AA334807 FOREIGN KEY (answer_id) REFERENCES answer (id)');
+ $this->addSql('ALTER TABLE answer_vote ADD CONSTRAINT FK_43B66A4A76ED395 FOREIGN KEY (user_id) REFERENCES customer (id)');
+ $this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494EF675F31B FOREIGN KEY (author_id) REFERENCES customer (id)');
+ $this->addSql('ALTER TABLE question_tag_relation ADD CONSTRAINT FK_C752C7D01E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE question_tag_relation ADD CONSTRAINT FK_C752C7D0BAD26311 FOREIGN KEY (tag_id) REFERENCES question_tag (id)');
+ $this->addSql('ALTER TABLE question_vote ADD CONSTRAINT FK_4FE688B1E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE question_vote ADD CONSTRAINT FK_4FE688BA76ED395 FOREIGN KEY (user_id) REFERENCES customer (id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A251E27F6BF');
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A25F675F31B');
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4AA334807');
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4A76ED395');
+ $this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494EF675F31B');
+ $this->addSql('ALTER TABLE question_tag_relation DROP FOREIGN KEY FK_C752C7D01E27F6BF');
+ $this->addSql('ALTER TABLE question_tag_relation DROP FOREIGN KEY FK_C752C7D0BAD26311');
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688B1E27F6BF');
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688BA76ED395');
+ $this->addSql('DROP TABLE answer');
+ $this->addSql('DROP TABLE answer_vote');
+ $this->addSql('DROP TABLE question');
+ $this->addSql('DROP TABLE question_tag');
+ $this->addSql('DROP TABLE question_tag_relation');
+ $this->addSql('DROP TABLE question_vote');
+ }
+}
diff --git a/migrations/Version20250905042717.php b/migrations/Version20250905042717.php
new file mode 100644
index 0000000..3a7fc9b
--- /dev/null
+++ b/migrations/Version20250905042717.php
@@ -0,0 +1,110 @@
+addSql('ALTER TABLE `user` ADD phone VARCHAR(20) DEFAULT NULL, ADD is_active TINYINT(1) NOT NULL, ADD email_verified_at DATETIME DEFAULT NULL, ADD created_at DATETIME NOT NULL, ADD updated_at DATETIME DEFAULT NULL, ADD last_login_at DATETIME DEFAULT NULL, ADD subscription_type VARCHAR(50) DEFAULT NULL, ADD subscription_expires_at DATETIME DEFAULT NULL');
+
+ // Migrate customer data to user table (only if not already exists)
+ $this->addSql('INSERT IGNORE INTO `user` (email, roles, password, name, phone, is_active, email_verified_at, created_at, updated_at, last_login_at, subscription_type, subscription_expires_at)
+ SELECT email, roles, password, name, phone, is_active,
+ CASE WHEN email_verified_at = "0000-00-00 00:00:00" OR email_verified_at IS NULL THEN NULL ELSE email_verified_at END,
+ CASE WHEN created_at = "0000-00-00 00:00:00" OR created_at IS NULL THEN NOW() ELSE created_at END,
+ CASE WHEN updated_at = "0000-00-00 00:00:00" OR updated_at IS NULL THEN NULL ELSE updated_at END,
+ CASE WHEN last_login_at = "0000-00-00 00:00:00" OR last_login_at IS NULL THEN NULL ELSE last_login_at END,
+ subscription_type,
+ CASE WHEN subscription_expires_at = "0000-00-00 00:00:00" OR subscription_expires_at IS NULL THEN NULL ELSE subscription_expires_at END
+ FROM customer');
+
+ // Update question table to reference user instead of customer
+ $this->addSql('UPDATE question q
+ INNER JOIN customer c ON q.author_id = c.id
+ INNER JOIN `user` u ON c.email = u.email
+ SET q.author_id = u.id');
+
+ // Update answer table to reference user instead of customer
+ $this->addSql('UPDATE answer a
+ INNER JOIN customer c ON a.author_id = c.id
+ INNER JOIN `user` u ON c.email = u.email
+ SET a.author_id = u.id');
+
+ // Update question_vote table to reference user instead of customer
+ $this->addSql('UPDATE question_vote qv
+ INNER JOIN customer c ON qv.user_id = c.id
+ INNER JOIN `user` u ON c.email = u.email
+ SET qv.user_id = u.id');
+
+ // Update answer_vote table to reference user instead of customer
+ $this->addSql('UPDATE answer_vote av
+ INNER JOIN customer c ON av.user_id = c.id
+ INNER JOIN `user` u ON c.email = u.email
+ SET av.user_id = u.id');
+
+ // Update password_reset_token table to reference user instead of customer
+ $this->addSql('UPDATE password_reset_token prt
+ INNER JOIN customer c ON prt.customer_id = c.id
+ INNER JOIN `user` u ON c.email = u.email
+ SET prt.customer_id = u.id');
+
+ // Now update foreign key constraints
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A25F675F31B');
+ $this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_DADD4A25F675F31B FOREIGN KEY (author_id) REFERENCES `user` (id)');
+
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4A76ED395');
+ $this->addSql('ALTER TABLE answer_vote ADD CONSTRAINT FK_43B66A4A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+
+ $this->addSql('ALTER TABLE password_reset_token DROP FOREIGN KEY FK_6B7BA4B69395C3F3');
+ $this->addSql('DROP INDEX IDX_6B7BA4B69395C3F3 ON password_reset_token');
+ $this->addSql('ALTER TABLE password_reset_token CHANGE customer_id user_id INT NOT NULL');
+ $this->addSql('ALTER TABLE password_reset_token ADD CONSTRAINT FK_6B7BA4B6A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ $this->addSql('CREATE INDEX IDX_6B7BA4B6A76ED395 ON password_reset_token (user_id)');
+
+ $this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494EF675F31B');
+ $this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494EF675F31B FOREIGN KEY (author_id) REFERENCES `user` (id)');
+
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688BA76ED395');
+ $this->addSql('ALTER TABLE question_vote ADD CONSTRAINT FK_4FE688BA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // Revert foreign key constraints
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A25F675F31B');
+ $this->addSql('ALTER TABLE answer ADD CONSTRAINT FK_DADD4A25F675F31B FOREIGN KEY (author_id) REFERENCES customer (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
+
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4A76ED395');
+ $this->addSql('ALTER TABLE answer_vote ADD CONSTRAINT FK_43B66A4A76ED395 FOREIGN KEY (user_id) REFERENCES customer (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
+
+ $this->addSql('ALTER TABLE password_reset_token DROP FOREIGN KEY FK_6B7BA4B6A76ED395');
+ $this->addSql('DROP INDEX IDX_6B7BA4B6A76ED395 ON password_reset_token');
+ $this->addSql('ALTER TABLE password_reset_token CHANGE user_id customer_id INT NOT NULL');
+ $this->addSql('ALTER TABLE password_reset_token ADD CONSTRAINT FK_6B7BA4B69395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
+ $this->addSql('CREATE INDEX IDX_6B7BA4B69395C3F3 ON password_reset_token (customer_id)');
+
+ $this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494EF675F31B');
+ $this->addSql('ALTER TABLE question ADD CONSTRAINT FK_B6F7494EF675F31B FOREIGN KEY (author_id) REFERENCES customer (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
+
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688BA76ED395');
+ $this->addSql('ALTER TABLE question_vote ADD CONSTRAINT FK_4FE688BA76ED395 FOREIGN KEY (user_id) REFERENCES customer (id) ON UPDATE NO ACTION ON DELETE NO ACTION');
+
+ // Remove new columns from user table
+ $this->addSql('ALTER TABLE `user` DROP phone, DROP is_active, DROP email_verified_at, DROP created_at, DROP updated_at, DROP last_login_at, DROP subscription_type, DROP subscription_expires_at');
+ }
+}
\ No newline at end of file
diff --git a/migrations/Version20250905043249.php b/migrations/Version20250905043249.php
new file mode 100644
index 0000000..30cd295
--- /dev/null
+++ b/migrations/Version20250905043249.php
@@ -0,0 +1,349 @@
+addSql('CREATE TABLE answer (
+ id INT AUTO_INCREMENT NOT NULL,
+ question_id INT NOT NULL,
+ author_id INT NOT NULL,
+ content LONGTEXT NOT NULL,
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME DEFAULT NULL,
+ is_accepted TINYINT(1) NOT NULL,
+ votes INT NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ INDEX IDX_DADD4A251E27F6BF (question_id),
+ INDEX IDX_DADD4A25F675F31B (author_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE answer_vote (
+ id INT AUTO_INCREMENT NOT NULL,
+ answer_id INT NOT NULL,
+ user_id INT NOT NULL,
+ is_upvote TINYINT(1) NOT NULL,
+ created_at DATETIME NOT NULL,
+ INDEX IDX_43B66A4AA334807 (answer_id),
+ INDEX IDX_43B66A4A76ED395 (user_id),
+ UNIQUE INDEX unique_answer_user_vote (answer_id, user_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE cat (
+ id INT AUTO_INCREMENT NOT NULL,
+ label VARCHAR(255) NOT NULL,
+ code VARCHAR(255) NOT NULL,
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE comment (
+ id INT AUTO_INCREMENT NOT NULL,
+ post_id INT NOT NULL,
+ body LONGTEXT NOT NULL,
+ name VARCHAR(255) DEFAULT NULL,
+ email VARCHAR(255) DEFAULT NULL,
+ website VARCHAR(255) DEFAULT NULL,
+ date_submit VARCHAR(255) NOT NULL,
+ publish TINYINT(1) DEFAULT NULL,
+ INDEX IDX_9474526C4B89032C (post_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE customer (
+ id INT AUTO_INCREMENT NOT NULL,
+ email VARCHAR(180) NOT NULL,
+ roles JSON NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ phone VARCHAR(20) NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ email_verified_at DATETIME DEFAULT NULL,
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME DEFAULT NULL,
+ last_login_at DATETIME DEFAULT NULL,
+ subscription_type VARCHAR(50) DEFAULT NULL,
+ subscription_expires_at DATETIME DEFAULT NULL,
+ UNIQUE INDEX UNIQ_CUSTOMER_EMAIL (email),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE hsxorder (
+ id INT AUTO_INCREMENT NOT NULL,
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE password_reset_token (
+ id INT AUTO_INCREMENT NOT NULL,
+ user_id INT NOT NULL,
+ token VARCHAR(255) NOT NULL,
+ expires_at DATETIME NOT NULL,
+ used_at DATETIME DEFAULT NULL,
+ created_at DATETIME NOT NULL,
+ INDEX IDX_6B7BA4B6A76ED395 (user_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE post (
+ id INT AUTO_INCREMENT NOT NULL,
+ submitter_id INT DEFAULT NULL,
+ cat_id INT DEFAULT NULL,
+ title VARCHAR(255) DEFAULT NULL,
+ body LONGTEXT DEFAULT NULL,
+ date_submit VARCHAR(50) NOT NULL,
+ publish TINYINT(1) DEFAULT NULL,
+ url VARCHAR(255) DEFAULT NULL,
+ main_pic VARCHAR(255) DEFAULT NULL,
+ plain LONGTEXT DEFAULT NULL,
+ version VARCHAR(255) DEFAULT NULL,
+ keywords LONGTEXT DEFAULT NULL,
+ sort VARCHAR(255) DEFAULT NULL,
+ intro LONGTEXT DEFAULT NULL,
+ views VARCHAR(255) DEFAULT NULL,
+ UNIQUE INDEX UNIQ_5A8A6C8DF47645AE (url),
+ INDEX IDX_5A8A6C8D919E5513 (submitter_id),
+ INDEX IDX_5A8A6C8DE6ADA943 (cat_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE post_tree (
+ post_id INT NOT NULL,
+ tree_id INT NOT NULL,
+ INDEX IDX_7E6B39D74B89032C (post_id),
+ INDEX IDX_7E6B39D778B64A2 (tree_id),
+ PRIMARY KEY(post_id, tree_id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question (
+ id INT AUTO_INCREMENT NOT NULL,
+ author_id INT NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ content LONGTEXT NOT NULL,
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME DEFAULT NULL,
+ is_solved TINYINT(1) NOT NULL,
+ views INT NOT NULL,
+ votes INT NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ INDEX IDX_B6F7494EF675F31B (author_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_tag (
+ id INT AUTO_INCREMENT NOT NULL,
+ name VARCHAR(50) NOT NULL,
+ description VARCHAR(255) DEFAULT NULL,
+ color VARCHAR(7) DEFAULT NULL,
+ usage_count INT NOT NULL,
+ is_active TINYINT(1) NOT NULL,
+ UNIQUE INDEX UNIQ_339D56FB5E237E06 (name),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_tag_relation (
+ id INT AUTO_INCREMENT NOT NULL,
+ question_id INT NOT NULL,
+ tag_id INT NOT NULL,
+ INDEX IDX_C752C7D01E27F6BF (question_id),
+ INDEX IDX_C752C7D0BAD26311 (tag_id),
+ UNIQUE INDEX unique_question_tag (question_id, tag_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE question_vote (
+ id INT AUTO_INCREMENT NOT NULL,
+ question_id INT NOT NULL,
+ user_id INT NOT NULL,
+ is_upvote TINYINT(1) NOT NULL,
+ created_at DATETIME NOT NULL,
+ INDEX IDX_4FE688B1E27F6BF (question_id),
+ INDEX IDX_4FE688BA76ED395 (user_id),
+ UNIQUE INDEX unique_question_user_vote (question_id, user_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE settings (
+ id INT AUTO_INCREMENT NOT NULL,
+ des VARCHAR(255) DEFAULT NULL,
+ site_keywords LONGTEXT DEFAULT NULL,
+ bscscan_api VARCHAR(255) DEFAULT NULL,
+ bsc_priv VARCHAR(255) DEFAULT NULL,
+ bsc_pub VARCHAR(255) DEFAULT NULL,
+ zarinpal_chain VARCHAR(255) DEFAULT NULL,
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE tree (
+ id INT AUTO_INCREMENT NOT NULL,
+ cat_id INT NOT NULL,
+ label VARCHAR(255) NOT NULL,
+ code VARCHAR(255) NOT NULL,
+ sort VARCHAR(50) DEFAULT NULL,
+ INDEX IDX_B73E5EDCE6ADA943 (cat_id),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE `user` (
+ id INT AUTO_INCREMENT NOT NULL,
+ email VARCHAR(180) NOT NULL,
+ roles JSON NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ phone VARCHAR(20) DEFAULT NULL,
+ is_active TINYINT(1) NOT NULL,
+ email_verified_at DATETIME DEFAULT NULL,
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME DEFAULT NULL,
+ last_login_at DATETIME DEFAULT NULL,
+ subscription_type VARCHAR(50) DEFAULT NULL,
+ subscription_expires_at DATETIME DEFAULT NULL,
+ UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL (email),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('CREATE TABLE messenger_messages (
+ id BIGINT AUTO_INCREMENT NOT NULL,
+ body LONGTEXT NOT NULL,
+ headers LONGTEXT NOT NULL,
+ queue_name VARCHAR(190) NOT NULL,
+ created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
+ available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\',
+ delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\',
+ INDEX IDX_75EA56E0FB7336F0 (queue_name),
+ INDEX IDX_75EA56E0E3BD61CE (available_at),
+ INDEX IDX_75EA56E016BA31DB (delivered_at),
+ PRIMARY KEY(id)
+ ) DEFAULT CHARACTER
+ SET
+ utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('ALTER TABLE
+ answer
+ ADD
+ CONSTRAINT FK_DADD4A251E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE
+ answer
+ ADD
+ CONSTRAINT FK_DADD4A25F675F31B FOREIGN KEY (author_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE
+ answer_vote
+ ADD
+ CONSTRAINT FK_43B66A4AA334807 FOREIGN KEY (answer_id) REFERENCES answer (id)');
+ $this->addSql('ALTER TABLE
+ answer_vote
+ ADD
+ CONSTRAINT FK_43B66A4A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE
+ comment
+ ADD
+ CONSTRAINT FK_9474526C4B89032C FOREIGN KEY (post_id) REFERENCES post (id)');
+ $this->addSql('ALTER TABLE
+ password_reset_token
+ ADD
+ CONSTRAINT FK_6B7BA4B6A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE
+ post
+ ADD
+ CONSTRAINT FK_5A8A6C8D919E5513 FOREIGN KEY (submitter_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE post ADD CONSTRAINT FK_5A8A6C8DE6ADA943 FOREIGN KEY (cat_id) REFERENCES cat (id)');
+ $this->addSql('ALTER TABLE
+ post_tree
+ ADD
+ CONSTRAINT FK_7E6B39D74B89032C FOREIGN KEY (post_id) REFERENCES post (id) ON DELETE CASCADE');
+ $this->addSql('ALTER TABLE
+ post_tree
+ ADD
+ CONSTRAINT FK_7E6B39D778B64A2 FOREIGN KEY (tree_id) REFERENCES tree (id) ON DELETE CASCADE');
+ $this->addSql('ALTER TABLE
+ question
+ ADD
+ CONSTRAINT FK_B6F7494EF675F31B FOREIGN KEY (author_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE
+ question_tag_relation
+ ADD
+ CONSTRAINT FK_C752C7D01E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE
+ question_tag_relation
+ ADD
+ CONSTRAINT FK_C752C7D0BAD26311 FOREIGN KEY (tag_id) REFERENCES question_tag (id)');
+ $this->addSql('ALTER TABLE
+ question_vote
+ ADD
+ CONSTRAINT FK_4FE688B1E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
+ $this->addSql('ALTER TABLE
+ question_vote
+ ADD
+ CONSTRAINT FK_4FE688BA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ $this->addSql('ALTER TABLE tree ADD CONSTRAINT FK_B73E5EDCE6ADA943 FOREIGN KEY (cat_id) REFERENCES cat (id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A251E27F6BF');
+ $this->addSql('ALTER TABLE answer DROP FOREIGN KEY FK_DADD4A25F675F31B');
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4AA334807');
+ $this->addSql('ALTER TABLE answer_vote DROP FOREIGN KEY FK_43B66A4A76ED395');
+ $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C4B89032C');
+ $this->addSql('ALTER TABLE password_reset_token DROP FOREIGN KEY FK_6B7BA4B6A76ED395');
+ $this->addSql('ALTER TABLE post DROP FOREIGN KEY FK_5A8A6C8D919E5513');
+ $this->addSql('ALTER TABLE post DROP FOREIGN KEY FK_5A8A6C8DE6ADA943');
+ $this->addSql('ALTER TABLE post_tree DROP FOREIGN KEY FK_7E6B39D74B89032C');
+ $this->addSql('ALTER TABLE post_tree DROP FOREIGN KEY FK_7E6B39D778B64A2');
+ $this->addSql('ALTER TABLE question DROP FOREIGN KEY FK_B6F7494EF675F31B');
+ $this->addSql('ALTER TABLE question_tag_relation DROP FOREIGN KEY FK_C752C7D01E27F6BF');
+ $this->addSql('ALTER TABLE question_tag_relation DROP FOREIGN KEY FK_C752C7D0BAD26311');
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688B1E27F6BF');
+ $this->addSql('ALTER TABLE question_vote DROP FOREIGN KEY FK_4FE688BA76ED395');
+ $this->addSql('ALTER TABLE tree DROP FOREIGN KEY FK_B73E5EDCE6ADA943');
+ $this->addSql('DROP TABLE answer');
+ $this->addSql('DROP TABLE answer_vote');
+ $this->addSql('DROP TABLE cat');
+ $this->addSql('DROP TABLE comment');
+ $this->addSql('DROP TABLE customer');
+ $this->addSql('DROP TABLE hsxorder');
+ $this->addSql('DROP TABLE password_reset_token');
+ $this->addSql('DROP TABLE post');
+ $this->addSql('DROP TABLE post_tree');
+ $this->addSql('DROP TABLE question');
+ $this->addSql('DROP TABLE question_tag');
+ $this->addSql('DROP TABLE question_tag_relation');
+ $this->addSql('DROP TABLE question_vote');
+ $this->addSql('DROP TABLE settings');
+ $this->addSql('DROP TABLE tree');
+ $this->addSql('DROP TABLE `user`');
+ $this->addSql('DROP TABLE messenger_messages');
+ }
+}
diff --git a/migrations/Version20250905050647.php b/migrations/Version20250905050647.php
new file mode 100644
index 0000000..77ed50b
--- /dev/null
+++ b/migrations/Version20250905050647.php
@@ -0,0 +1,33 @@
+addSql('CREATE TABLE wallets (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, wallet_address VARCHAR(42) NOT NULL, wallet_type VARCHAR(50) NOT NULL, is_primary TINYINT(1) NOT NULL, is_active TINYINT(1) NOT NULL, connected_at DATETIME NOT NULL, last_used_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_967AAA6CA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+ $this->addSql('ALTER TABLE wallets ADD CONSTRAINT FK_967AAA6CA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE wallets DROP FOREIGN KEY FK_967AAA6CA76ED395');
+ $this->addSql('DROP TABLE wallets');
+ }
+}
diff --git a/public/fonts/yekanbakh/fontiran.css b/public/fonts/yekanbakh/fontiran.css
index 1ba9f12..f2ef12f 100644
--- a/public/fonts/yekanbakh/fontiran.css
+++ b/public/fonts/yekanbakh/fontiran.css
@@ -49,8 +49,8 @@ This set of fonts are used in this project under the license: (.....)
font-style: normal;
font-weight: 600;
font-stretch: normal;
- src: url('woff/YekanBakhFaNum-SemiBold.woff') format('woff'),
- url('woff2/YekanBakhFaNum-SemiBold.woff2') format('woff2');
+ src: url('woff/YekanBakhFaNum-Bold.woff') format('woff'),
+ url('woff2/YekanBakhFaNum-Bold.woff2') format('woff2');
}
@font-face {
diff --git a/public/img/icons/wallet.svg b/public/img/icons/wallet.svg
new file mode 100644
index 0000000..58e458c
--- /dev/null
+++ b/public/img/icons/wallet.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php
index a81d95f..0a46c92 100644
--- a/src/Controller/CustomerController.php
+++ b/src/Controller/CustomerController.php
@@ -2,13 +2,13 @@
namespace App\Controller;
-use App\Entity\Customer;
+use App\Entity\User;
use App\Entity\PasswordResetToken;
use App\Form\CustomerRegistrationFormType;
-
use App\Form\ForgotPasswordFormType;
use App\Form\ResetPasswordFormType;
-use App\Repository\CustomerRepository;
+use App\Form\WalletConnectFormType;
+use App\Repository\UserRepository;
use App\Repository\PasswordResetTokenRepository;
use App\Service\EmailService;
use Doctrine\ORM\EntityManagerInterface;
@@ -24,7 +24,7 @@ class CustomerController extends AbstractController
{
public function __construct(
private EntityManagerInterface $entityManager,
- private CustomerRepository $customerRepository,
+ private UserRepository $userRepository,
private PasswordResetTokenRepository $tokenRepository,
private UserPasswordHasherInterface $passwordHasher,
private EmailService $emailService
@@ -53,32 +53,35 @@ class CustomerController extends AbstractController
return $this->redirectToRoute('customer_dashboard');
}
- $customer = new Customer();
- $form = $this->createForm(CustomerRegistrationFormType::class, $customer);
+ $user = new User();
+ $form = $this->createForm(CustomerRegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// بررسی وجود ایمیل
- $existingCustomer = $this->customerRepository->findByEmail($customer->getEmail());
- if ($existingCustomer) {
+ $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
+ if ($existingUser) {
$this->addFlash('error', 'این ایمیل قبلاً ثبت شده است.');
return $this->render('customer/register.html.twig', [
'form' => $form->createView(),
]);
}
+ // تنظیم نقش مشتری
+ $user->setRoles(['ROLE_CUSTOMER']);
+
// هش کردن کلمه عبور
$plainPassword = $form->get('plainPassword')->getData();
- $customer->setPassword(
- $this->passwordHasher->hashPassword($customer, $plainPassword)
+ $user->setPassword(
+ $this->passwordHasher->hashPassword($user, $plainPassword)
);
- // ذخیره مشتری
- $this->entityManager->persist($customer);
+ // ذخیره کاربر
+ $this->entityManager->persist($user);
$this->entityManager->flush();
// ارسال ایمیل تایید
- $this->emailService->sendVerificationEmail($customer);
+ $this->emailService->sendVerificationEmail($user);
$this->addFlash('success', 'ثبتنام با موفقیت انجام شد. لطفاً ایمیل خود را بررسی کنید.');
return $this->redirectToRoute('customer_login');
@@ -94,21 +97,21 @@ class CustomerController extends AbstractController
{
// برای سادگی، از ایمیل به عنوان توکن استفاده میکنیم
// در آینده میتوان یک سیستم توکن جداگانه پیاده کرد
- $customer = $this->customerRepository->findOneBy(['email' => $token]);
+ $user = $this->userRepository->findOneBy(['email' => $token]);
- if (!$customer) {
+ if (!$user) {
$this->addFlash('error', 'لینک تایید نامعتبر است.');
return $this->redirectToRoute('customer_login');
}
- if ($customer->getEmailVerifiedAt()) {
+ if ($user->getEmailVerifiedAt()) {
$this->addFlash('info', 'ایمیل شما قبلاً تایید شده است.');
return $this->redirectToRoute('customer_login');
}
- $customer->setEmailVerifiedAt(new \DateTime());
- $customer->setIsActive(true);
- $customer->setUpdatedAt(new \DateTime());
+ $user->setEmailVerifiedAt(new \DateTime());
+ $user->setIsActive(true);
+ $user->setUpdatedAt(new \DateTime());
$this->entityManager->flush();
@@ -124,18 +127,18 @@ class CustomerController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData();
- $customer = $this->customerRepository->findByEmail($email);
+ $user = $this->userRepository->findOneBy(['email' => $email]);
- if ($customer) {
+ if ($user) {
// ایجاد توکن بازیابی
$resetToken = new PasswordResetToken();
- $resetToken->setCustomer($customer);
+ $resetToken->setUser($user);
$this->entityManager->persist($resetToken);
$this->entityManager->flush();
// ارسال ایمیل بازیابی
- $this->emailService->sendPasswordResetEmail($customer, $resetToken);
+ $this->emailService->sendPasswordResetEmail($user, $resetToken);
}
// همیشه پیام موفقیت نشان میدهیم (امنیت)
@@ -158,7 +161,7 @@ class CustomerController extends AbstractController
return $this->redirectToRoute('customer_forgot_password');
}
- $customer = $resetToken->getCustomer();
+ $user = $resetToken->getUser();
$form = $this->createForm(ResetPasswordFormType::class);
$form->handleRequest($request);
@@ -166,10 +169,10 @@ class CustomerController extends AbstractController
$newPassword = $form->get('plainPassword')->getData();
// تغییر کلمه عبور
- $customer->setPassword(
- $this->passwordHasher->hashPassword($customer, $newPassword)
+ $user->setPassword(
+ $this->passwordHasher->hashPassword($user, $newPassword)
);
- $customer->setUpdatedAt(new \DateTime());
+ $user->setUpdatedAt(new \DateTime());
// غیرفعال کردن توکن
$resetToken->setUsedAt(new \DateTime());
@@ -187,20 +190,24 @@ class CustomerController extends AbstractController
}
#[Route('/customer/dashboard', name: 'customer_dashboard')]
- public function dashboard(): Response
+ public function dashboard(Request $request): Response
{
$this->denyAccessUnlessGranted('ROLE_CUSTOMER');
- $customer = $this->getUser();
+ $user = $this->getUser();
// بهروزرسانی زمان آخرین ورود
- if ($customer instanceof Customer) {
- $customer->setLastLoginAt(new \DateTime());
+ if ($user instanceof User) {
+ $user->setLastLoginAt(new \DateTime());
$this->entityManager->flush();
}
+
+ // ایجاد پیام امضا برای کیف پول
+ $signMessage = 'Please sign this message to connect your wallet to Hesabix.';
return $this->render('customer/dashboard.html.twig', [
- 'customer' => $customer,
+ 'user' => $user,
+ 'signMessage' => $signMessage,
]);
}
diff --git a/src/Controller/GeneralController.php b/src/Controller/GeneralController.php
index 612be6f..2c546f7 100644
--- a/src/Controller/GeneralController.php
+++ b/src/Controller/GeneralController.php
@@ -3,6 +3,8 @@
namespace App\Controller;
use App\Entity\Post;
+use App\Entity\Question;
+use App\Entity\QuestionTag;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -25,8 +27,12 @@ class GeneralController extends AbstractController
$response = new Response();
$response->headers->set('Content-Type', 'text/xml');
$posts = $em->getRepository(Post::class)->findAll();
+ $questions = $em->getRepository(Question::class)->findActiveQuestions(1000, 0);
+ $tags = $em->getRepository(QuestionTag::class)->findActiveTags();
return $this->render('general/sitemap.html.twig', [
- 'posts' => $posts
+ 'posts' => $posts,
+ 'questions' => $questions,
+ 'tags' => $tags
], $response);
}
diff --git a/src/Controller/QA/QAController.php b/src/Controller/QA/QAController.php
new file mode 100644
index 0000000..adc8142
--- /dev/null
+++ b/src/Controller/QA/QAController.php
@@ -0,0 +1,429 @@
+query->get('page', 1));
+ $limit = 20;
+ $offset = ($page - 1) * $limit;
+
+ $filter = $request->query->get('filter', 'all');
+ $search = $request->query->get('search', '');
+ $tag = $request->query->get('tag', '');
+
+ $questions = [];
+ $totalCount = 0;
+
+ if ($search) {
+ $questions = $this->questionRepository->searchQuestions($search, $limit, $offset);
+ $totalCount = $this->questionRepository->countSearchQuestions($search);
+ } elseif ($tag) {
+ $questions = $this->questionRepository->findQuestionsByTag($tag, $limit, $offset);
+ $totalCount = $this->questionRepository->countQuestionsByTag($tag);
+ } else {
+ switch ($filter) {
+ case 'unsolved':
+ $questions = $this->questionRepository->findUnsolvedQuestions($limit, $offset);
+ $totalCount = $this->questionRepository->countUnsolvedQuestions();
+ break;
+ case 'solved':
+ $questions = $this->questionRepository->findSolvedQuestions($limit, $offset);
+ $totalCount = $this->questionRepository->countSolvedQuestions();
+ break;
+ case 'popular':
+ $questions = $this->questionRepository->findMostVotedQuestions($limit);
+ $totalCount = $this->questionRepository->countMostVotedQuestions();
+ break;
+ default:
+ $questions = $this->questionRepository->findActiveQuestions($limit, $offset);
+ $totalCount = $this->questionRepository->getTotalCount();
+ break;
+ }
+ }
+
+ $popularTags = $this->tagRepository->findPopularTags(20);
+
+ $totalPages = ceil($totalCount / $limit);
+
+ return $this->render('qa/index.html.twig', [
+ 'questions' => $questions,
+ 'popularTags' => $popularTags,
+ 'currentPage' => $page,
+ 'totalPages' => $totalPages,
+ 'currentFilter' => $filter,
+ 'currentSearch' => $search,
+ 'currentTag' => $tag,
+ ]);
+ }
+
+ #[Route('/question/{id}', name: 'question_show', methods: ['GET'])]
+ public function showQuestion(int $id, Request $request): Response
+ {
+ $question = $this->questionRepository->find($id);
+
+ if (!$question || !$question->isActive()) {
+ throw $this->createNotFoundException('سوال مورد نظر یافت نشد.');
+ }
+
+ // افزایش تعداد بازدید
+ $question->incrementViews();
+ $this->entityManager->flush();
+
+ // Pagination برای پاسخها
+ $page = max(1, (int) $request->query->get('page', 1));
+ $limit = 10; // تعداد پاسخها در هر صفحه
+ $offset = ($page - 1) * $limit;
+
+ $answers = $this->answerRepository->findAnswersByQuestion($id, $limit, $offset);
+ $totalAnswers = $this->answerRepository->countAnswersByQuestion($id);
+ $totalPages = ceil($totalAnswers / $limit);
+
+ return $this->render('qa/question_show.html.twig', [
+ 'question' => $question,
+ 'answers' => $answers,
+ 'currentPage' => $page,
+ 'totalPages' => $totalPages,
+ 'totalAnswers' => $totalAnswers,
+ ]);
+ }
+
+ #[Route('/ask', name: 'ask', methods: ['GET', 'POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function askQuestion(Request $request): Response
+ {
+ $question = new Question();
+ $question->setAuthor($this->getUser());
+
+ $form = $this->createForm(QuestionFormType::class, $question);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ // مدیریت تگها
+ $selectedTagIds = $request->request->all('question')['tags'] ?? [];
+ foreach ($selectedTagIds as $tagId) {
+ $tag = $this->tagRepository->find($tagId);
+ if ($tag) {
+ $tagRelation = new QuestionTagRelation();
+ $tagRelation->setQuestion($question);
+ $tagRelation->setTag($tag);
+ $this->entityManager->persist($tagRelation);
+
+ // افزایش تعداد استفاده از تگ
+ $tag->incrementUsageCount();
+ }
+ }
+
+ $this->entityManager->persist($question);
+ $this->entityManager->flush();
+
+ $this->addFlash('success', 'سوال شما با موفقیت ثبت شد.');
+ return $this->redirectToRoute('qa_question_show', ['id' => $question->getId()]);
+ }
+
+ $availableTags = $this->tagRepository->findActiveTags();
+
+ return $this->render('qa/ask_question.html.twig', [
+ 'form' => $form,
+ 'availableTags' => $availableTags,
+ ]);
+ }
+
+ #[Route('/question/{id}/answer', name: 'answer', methods: ['GET', 'POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function answerQuestion(int $id, Request $request): Response
+ {
+ $question = $this->questionRepository->find($id);
+
+ if (!$question || !$question->isActive()) {
+ throw $this->createNotFoundException('سوال مورد نظر یافت نشد.');
+ }
+
+ $answer = new Answer();
+ $answer->setQuestion($question);
+ $answer->setAuthor($this->getUser());
+
+ $form = $this->createForm(AnswerFormType::class, $answer);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->entityManager->persist($answer);
+ $this->entityManager->flush();
+
+ $this->addFlash('success', 'پاسخ شما با موفقیت ثبت شد.');
+ return $this->redirectToRoute('qa_question_show', ['id' => $question->getId()]);
+ }
+
+ return $this->render('qa/answer_question.html.twig', [
+ 'question' => $question,
+ 'form' => $form,
+ ]);
+ }
+
+ #[Route('/question/{id}/vote', name: 'question_vote', methods: ['POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function voteQuestion(int $id, Request $request): JsonResponse
+ {
+ $question = $this->questionRepository->find($id);
+
+ if (!$question || !$question->isActive()) {
+ return new JsonResponse(['error' => 'سوال یافت نشد.'], 404);
+ }
+
+ $isUpvote = $request->request->get('upvote') === 'true';
+ $user = $this->getUser();
+
+ // بررسی CSRF token
+ if (!$this->isCsrfTokenValid('vote', $request->request->get('_token'))) {
+ return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403);
+ }
+
+ // بررسی وجود رای قبلی
+ $existingVote = $this->questionVoteRepository->findVoteByUserAndQuestion($user->getId(), $id);
+
+ if ($existingVote) {
+ // اگر رای مشابه باشد، رای را حذف کن
+ if ($existingVote->isUpvote() === $isUpvote) {
+ $this->entityManager->remove($existingVote);
+ $question->setVotes($question->getVotes() + ($isUpvote ? -1 : 1));
+ $existingVote = null; // برای بازگشت null
+ } else {
+ // اگر رای متفاوت باشد، رای را تغییر بده
+ $oldVoteValue = $existingVote->isUpvote() ? 1 : -1;
+ $newVoteValue = $isUpvote ? 1 : -1;
+ $existingVote->setIsUpvote($isUpvote);
+ $question->setVotes($question->getVotes() - $oldVoteValue + $newVoteValue);
+ }
+ } else {
+ // رای جدید
+ $vote = new QuestionVote();
+ $vote->setQuestion($question);
+ $vote->setUser($user);
+ $vote->setIsUpvote($isUpvote);
+ $this->entityManager->persist($vote);
+ $question->setVotes($question->getVotes() + ($isUpvote ? 1 : -1));
+ }
+
+ $this->entityManager->flush();
+
+ // بررسی رای نهایی کاربر
+ $finalVote = $this->questionVoteRepository->findVoteByUserAndQuestion($user->getId(), $id);
+
+ return new JsonResponse([
+ 'votes' => $question->getVotes(),
+ 'userVote' => $finalVote ? ($finalVote->isUpvote() ? 'up' : 'down') : null
+ ]);
+ }
+
+ #[Route('/answer/{id}/vote', name: 'answer_vote', methods: ['POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function voteAnswer(int $id, Request $request): JsonResponse
+ {
+ $answer = $this->answerRepository->find($id);
+
+ if (!$answer || !$answer->isActive()) {
+ return new JsonResponse(['error' => 'پاسخ یافت نشد.'], 404);
+ }
+
+ $isUpvote = $request->request->get('upvote') === 'true';
+ $user = $this->getUser();
+
+ // بررسی CSRF token
+ if (!$this->isCsrfTokenValid('vote', $request->request->get('_token'))) {
+ return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403);
+ }
+
+ // بررسی وجود رای قبلی
+ $existingVote = $this->answerVoteRepository->findVoteByUserAndAnswer($user->getId(), $id);
+
+ if ($existingVote) {
+ // اگر رای مشابه باشد، رای را حذف کن
+ if ($existingVote->isUpvote() === $isUpvote) {
+ $this->entityManager->remove($existingVote);
+ $answer->setVotes($answer->getVotes() + ($isUpvote ? -1 : 1));
+ $existingVote = null; // برای بازگشت null
+ } else {
+ // اگر رای متفاوت باشد، رای را تغییر بده
+ $oldVoteValue = $existingVote->isUpvote() ? 1 : -1;
+ $newVoteValue = $isUpvote ? 1 : -1;
+ $existingVote->setIsUpvote($isUpvote);
+ $answer->setVotes($answer->getVotes() - $oldVoteValue + $newVoteValue);
+ }
+ } else {
+ // رای جدید
+ $vote = new AnswerVote();
+ $vote->setAnswer($answer);
+ $vote->setUser($user);
+ $vote->setIsUpvote($isUpvote);
+ $this->entityManager->persist($vote);
+ $answer->setVotes($answer->getVotes() + ($isUpvote ? 1 : -1));
+ }
+
+ $this->entityManager->flush();
+
+ // بررسی رای نهایی کاربر
+ $finalVote = $this->answerVoteRepository->findVoteByUserAndAnswer($user->getId(), $id);
+
+ return new JsonResponse([
+ 'votes' => $answer->getVotes(),
+ 'userVote' => $finalVote ? ($finalVote->isUpvote() ? 'up' : 'down') : null
+ ]);
+ }
+
+ #[Route('/answer/{id}/accept', name: 'answer_accept', methods: ['POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function acceptAnswer(int $id, Request $request): JsonResponse
+ {
+ $answer = $this->answerRepository->find($id);
+
+ if (!$answer || !$answer->isActive()) {
+ return new JsonResponse(['error' => 'پاسخ یافت نشد.'], 404);
+ }
+
+ $question = $answer->getQuestion();
+ $user = $this->getUser();
+
+ // بررسی CSRF token
+ if (!$this->isCsrfTokenValid('accept', $request->request->get('_token'))) {
+ return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403);
+ }
+
+ // فقط نویسنده سوال میتواند پاسخ را بپذیرد
+ if ($question->getAuthor() !== $user) {
+ return new JsonResponse(['error' => 'شما مجاز به انجام این عمل نیستید.'], 403);
+ }
+
+ // اگر پاسخ قبلاً پذیرفته شده، آن را لغو کن
+ if ($answer->isAccepted()) {
+ $answer->setIsAccepted(false);
+ $question->setIsSolved(false);
+ } else {
+ // همه پاسخهای قبلی را لغو کن
+ foreach ($question->getAnswers() as $existingAnswer) {
+ $existingAnswer->setIsAccepted(false);
+ }
+ // پاسخ جدید را بپذیر
+ $answer->setIsAccepted(true);
+ $question->setIsSolved(true);
+ }
+
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'accepted' => $answer->isAccepted(),
+ 'solved' => $question->isSolved()
+ ]);
+ }
+
+ #[Route('/tags', name: 'tags', methods: ['GET'])]
+ public function showTags(): Response
+ {
+ $tags = $this->tagRepository->findActiveTags();
+
+ return $this->render('qa/tags.html.twig', [
+ 'tags' => $tags,
+ ]);
+ }
+
+ #[Route('/tag/create', name: 'tag_create', methods: ['POST'])]
+ #[IsGranted('ROLE_CUSTOMER')]
+ public function createTag(Request $request): JsonResponse
+ {
+ // بررسی CSRF token
+ if (!$this->isCsrfTokenValid('vote', $request->request->get('_token'))) {
+ return new JsonResponse(['error' => 'CSRF token نامعتبر است.'], 403);
+ }
+
+ $tagName = trim($request->request->get('name', ''));
+
+ if (empty($tagName)) {
+ return new JsonResponse(['error' => 'نام تگ الزامی است.'], 400);
+ }
+
+ // بررسی وجود تگ
+ $existingTag = $this->tagRepository->findByName($tagName);
+ if ($existingTag) {
+ return new JsonResponse([
+ 'id' => $existingTag->getId(),
+ 'name' => $existingTag->getName(),
+ 'message' => 'تگ از قبل وجود دارد.'
+ ]);
+ }
+
+ // ایجاد تگ جدید
+ $tag = new QuestionTag();
+ $tag->setName($tagName);
+ $tag->setDescription('تگ ایجاد شده توسط کاربر');
+ $tag->setColor('#6c757d');
+ $tag->setUsageCount(0);
+ $tag->setIsActive(true);
+
+ $this->entityManager->persist($tag);
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'id' => $tag->getId(),
+ 'name' => $tag->getName(),
+ 'message' => 'تگ با موفقیت ایجاد شد.'
+ ]);
+ }
+
+ #[Route('/tag/{name}', name: 'tag_questions', methods: ['GET'])]
+ public function showTagQuestions(string $name, Request $request): Response
+ {
+ $tag = $this->tagRepository->findByName($name);
+
+ if (!$tag || !$tag->isActive()) {
+ throw $this->createNotFoundException('تگ مورد نظر یافت نشد.');
+ }
+
+ $page = max(1, (int) $request->query->get('page', 1));
+ $limit = 20;
+ $offset = ($page - 1) * $limit;
+
+ $questions = $this->questionRepository->findQuestionsByTag($name, $limit, $offset);
+ $totalCount = $this->questionRepository->countQuestionsByTag($name);
+ $totalPages = ceil($totalCount / $limit);
+
+ return $this->render('qa/tag_questions.html.twig', [
+ 'tag' => $tag,
+ 'questions' => $questions,
+ 'currentPage' => $page,
+ 'totalPages' => $totalPages,
+ ]);
+ }
+}
diff --git a/src/Controller/WalletController.php b/src/Controller/WalletController.php
new file mode 100644
index 0000000..780541e
--- /dev/null
+++ b/src/Controller/WalletController.php
@@ -0,0 +1,116 @@
+walletService = $walletService;
+ }
+
+ #[Route('/connect', name: 'connect', methods: ['POST'])]
+ public function connectWallet(Request $request): JsonResponse
+ {
+ try {
+ $data = json_decode($request->getContent(), true);
+
+ if (!$data) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'دادههای JSON نامعتبر است'
+ ], 400);
+ }
+
+ if (!isset($data['walletAddress']) || !isset($data['walletType']) || !isset($data['signature'])) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'اطلاعات ناقص است'
+ ], 400);
+ }
+
+ $user = $this->getUser();
+ if (!$user) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کاربر وارد نشده است'
+ ], 401);
+ }
+
+ return $this->walletService->connectWallet(
+ $user,
+ $data['walletAddress'],
+ $data['walletType'],
+ $data['signature']
+ );
+ } catch (\Exception $e) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'خطا در سرور: ' . $e->getMessage()
+ ], 500);
+ }
+ }
+
+ #[Route('/list', name: 'list', methods: ['GET'])]
+ public function listWallets(): JsonResponse
+ {
+ $user = $this->getUser();
+ $wallets = $this->walletService->getUserWallets($user);
+
+ return new JsonResponse([
+ 'success' => true,
+ 'wallets' => $wallets
+ ]);
+ }
+
+ #[Route('/{id}/set-primary', name: 'set_primary', methods: ['PUT'])]
+ public function setPrimaryWallet(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ return $this->walletService->setPrimaryWallet($user, $id);
+ }
+
+ #[Route('/{id}/toggle-status', name: 'toggle_status', methods: ['PUT'])]
+ public function toggleWalletStatus(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ return $this->walletService->toggleWalletStatus($user, $id);
+ }
+
+ #[Route('/{id}', name: 'delete', methods: ['DELETE'])]
+ public function deleteWallet(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ return $this->walletService->deleteWallet($user, $id);
+ }
+
+ #[Route('/sign-message', name: 'sign_message', methods: ['GET'])]
+ public function getSignMessage(): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کاربر وارد نشده است'
+ ], 401);
+ }
+
+ $message = $this->walletService->generateSignMessage($user);
+
+ return new JsonResponse([
+ 'success' => true,
+ 'message' => $message
+ ]);
+ }
+}
diff --git a/src/Entity/Answer.php b/src/Entity/Answer.php
new file mode 100644
index 0000000..377c47b
--- /dev/null
+++ b/src/Entity/Answer.php
@@ -0,0 +1,180 @@
+
+ */
+ #[ORM\OneToMany(targetEntity: AnswerVote::class, mappedBy: 'answer', orphanRemoval: true)]
+ private Collection $answerVotes;
+
+ public function __construct()
+ {
+ $this->answerVotes = new ArrayCollection();
+ $this->createdAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getContent(): ?string
+ {
+ return $this->content;
+ }
+
+ public function setContent(string $content): static
+ {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function getQuestion(): ?Question
+ {
+ return $this->question;
+ }
+
+ public function setQuestion(?Question $question): static
+ {
+ $this->question = $question;
+ return $this;
+ }
+
+ public function getAuthor(): ?User
+ {
+ return $this->author;
+ }
+
+ public function setAuthor(?User $author): static
+ {
+ $this->author = $author;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUpdatedAt(): ?\DateTimeInterface
+ {
+ return $this->updatedAt;
+ }
+
+ public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
+ {
+ $this->updatedAt = $updatedAt;
+ return $this;
+ }
+
+ public function isAccepted(): bool
+ {
+ return $this->isAccepted;
+ }
+
+ public function setIsAccepted(bool $isAccepted): static
+ {
+ $this->isAccepted = $isAccepted;
+ return $this;
+ }
+
+ public function getVotes(): int
+ {
+ return $this->votes;
+ }
+
+ public function setVotes(int $votes): static
+ {
+ $this->votes = $votes;
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getAnswerVotes(): Collection
+ {
+ return $this->answerVotes;
+ }
+
+ public function addAnswerVote(AnswerVote $answerVote): static
+ {
+ if (!$this->answerVotes->contains($answerVote)) {
+ $this->answerVotes->add($answerVote);
+ $answerVote->setAnswer($this);
+ }
+ return $this;
+ }
+
+ public function removeAnswerVote(AnswerVote $answerVote): static
+ {
+ if ($this->answerVotes->removeElement($answerVote)) {
+ if ($answerVote->getAnswer() === $this) {
+ $answerVote->setAnswer(null);
+ }
+ }
+ return $this;
+ }
+}
diff --git a/src/Entity/AnswerVote.php b/src/Entity/AnswerVote.php
new file mode 100644
index 0000000..3381379
--- /dev/null
+++ b/src/Entity/AnswerVote.php
@@ -0,0 +1,85 @@
+createdAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getAnswer(): ?Answer
+ {
+ return $this->answer;
+ }
+
+ public function setAnswer(?Answer $answer): static
+ {
+ $this->answer = $answer;
+ return $this;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): static
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function isUpvote(): bool
+ {
+ return $this->isUpvote;
+ }
+
+ public function setIsUpvote(bool $isUpvote): static
+ {
+ $this->isUpvote = $isUpvote;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+}
diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php
deleted file mode 100644
index e2c68ce..0000000
--- a/src/Entity/Customer.php
+++ /dev/null
@@ -1,216 +0,0 @@
-createdAt = new \DateTime();
- $this->roles = ['ROLE_CUSTOMER'];
- }
-
- public function getId(): ?int
- {
- return $this->id;
- }
-
- public function getEmail(): ?string
- {
- return $this->email;
- }
-
- public function setEmail(string $email): static
- {
- $this->email = $email;
- return $this;
- }
-
- public function getUserIdentifier(): string
- {
- return $this->email ?? '';
- }
-
- public function getRoles(): array
- {
- $roles = $this->roles;
- $roles[] = 'ROLE_CUSTOMER';
- return array_unique($roles);
- }
-
- public function setRoles(array $roles): static
- {
- $this->roles = $roles;
- return $this;
- }
-
- public function getPassword(): ?string
- {
- return $this->password;
- }
-
- public function setPassword(string $password): static
- {
- $this->password = $password;
- return $this;
- }
-
- public function eraseCredentials(): void
- {
- // If you store any temporary, sensitive data on the user, clear it here
- }
-
- public function getName(): ?string
- {
- return $this->name;
- }
-
- public function setName(string $name): static
- {
- $this->name = $name;
- return $this;
- }
-
- public function getPhone(): ?string
- {
- return $this->phone;
- }
-
- public function setPhone(string $phone): static
- {
- $this->phone = $phone;
- return $this;
- }
-
- public function isActive(): bool
- {
- return $this->isActive;
- }
-
- public function setIsActive(bool $isActive): static
- {
- $this->isActive = $isActive;
- return $this;
- }
-
- public function getEmailVerifiedAt(): ?\DateTimeInterface
- {
- return $this->emailVerifiedAt;
- }
-
- public function setEmailVerifiedAt(?\DateTimeInterface $emailVerifiedAt): static
- {
- $this->emailVerifiedAt = $emailVerifiedAt;
- return $this;
- }
-
- public function getCreatedAt(): ?\DateTimeInterface
- {
- return $this->createdAt;
- }
-
- public function setCreatedAt(\DateTimeInterface $createdAt): static
- {
- $this->createdAt = $createdAt;
- return $this;
- }
-
- public function getUpdatedAt(): ?\DateTimeInterface
- {
- return $this->updatedAt;
- }
-
- public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
- {
- $this->updatedAt = $updatedAt;
- return $this;
- }
-
- public function getLastLoginAt(): ?\DateTimeInterface
- {
- return $this->lastLoginAt;
- }
-
- public function setLastLoginAt(?\DateTimeInterface $lastLoginAt): static
- {
- $this->lastLoginAt = $lastLoginAt;
- return $this;
- }
-
- public function getSubscriptionType(): ?string
- {
- return $this->subscriptionType;
- }
-
- public function setSubscriptionType(?string $subscriptionType): static
- {
- $this->subscriptionType = $subscriptionType;
- return $this;
- }
-
- public function getSubscriptionExpiresAt(): ?\DateTimeInterface
- {
- return $this->subscriptionExpiresAt;
- }
-
- public function setSubscriptionExpiresAt(?\DateTimeInterface $subscriptionExpiresAt): static
- {
- $this->subscriptionExpiresAt = $subscriptionExpiresAt;
- return $this;
- }
-}
diff --git a/src/Entity/PasswordResetToken.php b/src/Entity/PasswordResetToken.php
index a6c67b5..ffdbca3 100644
--- a/src/Entity/PasswordResetToken.php
+++ b/src/Entity/PasswordResetToken.php
@@ -14,9 +14,9 @@ class PasswordResetToken
#[ORM\Column]
private ?int $id = null;
- #[ORM\ManyToOne(targetEntity: Customer::class)]
+ #[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
- private ?Customer $customer = null;
+ private ?User $user = null;
#[ORM\Column(length: 255)]
private ?string $token = null;
@@ -42,14 +42,14 @@ class PasswordResetToken
return $this->id;
}
- public function getCustomer(): ?Customer
+ public function getUser(): ?User
{
- return $this->customer;
+ return $this->user;
}
- public function setCustomer(?Customer $customer): static
+ public function setUser(?User $user): static
{
- $this->customer = $customer;
+ $this->user = $user;
return $this;
}
diff --git a/src/Entity/Question.php b/src/Entity/Question.php
new file mode 100644
index 0000000..57bfeb5
--- /dev/null
+++ b/src/Entity/Question.php
@@ -0,0 +1,284 @@
+
+ */
+ #[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', orphanRemoval: true, cascade: ['persist'])]
+ private Collection $answers;
+
+ /**
+ * @var Collection
+ */
+ #[ORM\OneToMany(targetEntity: QuestionVote::class, mappedBy: 'question', orphanRemoval: true)]
+ private Collection $questionVotes;
+
+ /**
+ * @var Collection
+ */
+ #[ORM\OneToMany(targetEntity: QuestionTagRelation::class, mappedBy: 'question', orphanRemoval: true, cascade: ['persist'])]
+ private Collection $tagRelations;
+
+ public function __construct()
+ {
+ $this->answers = new ArrayCollection();
+ $this->questionVotes = new ArrayCollection();
+ $this->tagRelations = new ArrayCollection();
+ $this->createdAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getTitle(): ?string
+ {
+ return $this->title;
+ }
+
+ public function setTitle(string $title): static
+ {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function getContent(): ?string
+ {
+ return $this->content;
+ }
+
+ public function setContent(string $content): static
+ {
+ $this->content = $content;
+ return $this;
+ }
+
+ public function getAuthor(): ?User
+ {
+ return $this->author;
+ }
+
+ public function setAuthor(?User $author): static
+ {
+ $this->author = $author;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUpdatedAt(): ?\DateTimeInterface
+ {
+ return $this->updatedAt;
+ }
+
+ public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
+ {
+ $this->updatedAt = $updatedAt;
+ return $this;
+ }
+
+ public function isSolved(): bool
+ {
+ return $this->isSolved;
+ }
+
+ public function setIsSolved(bool $isSolved): static
+ {
+ $this->isSolved = $isSolved;
+ return $this;
+ }
+
+ public function getViews(): int
+ {
+ return $this->views;
+ }
+
+ public function setViews(int $views): static
+ {
+ $this->views = $views;
+ return $this;
+ }
+
+ public function incrementViews(): static
+ {
+ $this->views++;
+ return $this;
+ }
+
+ public function getVotes(): int
+ {
+ return $this->votes;
+ }
+
+ public function setVotes(int $votes): static
+ {
+ $this->votes = $votes;
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getAnswers(): Collection
+ {
+ return $this->answers;
+ }
+
+ public function addAnswer(Answer $answer): static
+ {
+ if (!$this->answers->contains($answer)) {
+ $this->answers->add($answer);
+ $answer->setQuestion($this);
+ }
+ return $this;
+ }
+
+ public function removeAnswer(Answer $answer): static
+ {
+ if ($this->answers->removeElement($answer)) {
+ if ($answer->getQuestion() === $this) {
+ $answer->setQuestion(null);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getQuestionVotes(): Collection
+ {
+ return $this->questionVotes;
+ }
+
+ public function addQuestionVote(QuestionVote $questionVote): static
+ {
+ if (!$this->questionVotes->contains($questionVote)) {
+ $this->questionVotes->add($questionVote);
+ $questionVote->setQuestion($this);
+ }
+ return $this;
+ }
+
+ public function removeQuestionVote(QuestionVote $questionVote): static
+ {
+ if ($this->questionVotes->removeElement($questionVote)) {
+ if ($questionVote->getQuestion() === $this) {
+ $questionVote->setQuestion(null);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getTagRelations(): Collection
+ {
+ return $this->tagRelations;
+ }
+
+ public function addTagRelation(QuestionTagRelation $tagRelation): static
+ {
+ if (!$this->tagRelations->contains($tagRelation)) {
+ $this->tagRelations->add($tagRelation);
+ $tagRelation->setQuestion($this);
+ }
+ return $this;
+ }
+
+ public function removeTagRelation(QuestionTagRelation $tagRelation): static
+ {
+ if ($this->tagRelations->removeElement($tagRelation)) {
+ if ($tagRelation->getQuestion() === $this) {
+ $tagRelation->setQuestion(null);
+ }
+ }
+ return $this;
+ }
+
+ public function getAnswersCount(): int
+ {
+ return $this->answers->count();
+ }
+
+ public function getBestAnswer(): ?Answer
+ {
+ foreach ($this->answers as $answer) {
+ if ($answer->isAccepted()) {
+ return $answer;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/Entity/QuestionTag.php b/src/Entity/QuestionTag.php
new file mode 100644
index 0000000..a2266ef
--- /dev/null
+++ b/src/Entity/QuestionTag.php
@@ -0,0 +1,149 @@
+
+ */
+ #[ORM\OneToMany(targetEntity: QuestionTagRelation::class, mappedBy: 'tag', orphanRemoval: true)]
+ private Collection $tagRelations;
+
+ public function __construct()
+ {
+ $this->tagRelations = new ArrayCollection();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(?string $description): static
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getColor(): ?string
+ {
+ return $this->color;
+ }
+
+ public function setColor(?string $color): static
+ {
+ $this->color = $color;
+ return $this;
+ }
+
+ public function getUsageCount(): int
+ {
+ return $this->usageCount;
+ }
+
+ public function setUsageCount(int $usageCount): static
+ {
+ $this->usageCount = $usageCount;
+ return $this;
+ }
+
+ public function incrementUsageCount(): static
+ {
+ $this->usageCount++;
+ return $this;
+ }
+
+ public function decrementUsageCount(): static
+ {
+ if ($this->usageCount > 0) {
+ $this->usageCount--;
+ }
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getTagRelations(): Collection
+ {
+ return $this->tagRelations;
+ }
+
+ public function addTagRelation(QuestionTagRelation $tagRelation): static
+ {
+ if (!$this->tagRelations->contains($tagRelation)) {
+ $this->tagRelations->add($tagRelation);
+ $tagRelation->setTag($this);
+ }
+ return $this;
+ }
+
+ public function removeTagRelation(QuestionTagRelation $tagRelation): static
+ {
+ if ($this->tagRelations->removeElement($tagRelation)) {
+ if ($tagRelation->getTag() === $this) {
+ $tagRelation->setTag(null);
+ }
+ }
+ return $this;
+ }
+}
diff --git a/src/Entity/QuestionTagRelation.php b/src/Entity/QuestionTagRelation.php
new file mode 100644
index 0000000..4f6eb13
--- /dev/null
+++ b/src/Entity/QuestionTagRelation.php
@@ -0,0 +1,52 @@
+id;
+ }
+
+ public function getQuestion(): ?Question
+ {
+ return $this->question;
+ }
+
+ public function setQuestion(?Question $question): static
+ {
+ $this->question = $question;
+ return $this;
+ }
+
+ public function getTag(): ?QuestionTag
+ {
+ return $this->tag;
+ }
+
+ public function setTag(?QuestionTag $tag): static
+ {
+ $this->tag = $tag;
+ return $this;
+ }
+}
diff --git a/src/Entity/QuestionVote.php b/src/Entity/QuestionVote.php
new file mode 100644
index 0000000..56b1dce
--- /dev/null
+++ b/src/Entity/QuestionVote.php
@@ -0,0 +1,85 @@
+createdAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getQuestion(): ?Question
+ {
+ return $this->question;
+ }
+
+ public function setQuestion(?Question $question): static
+ {
+ $this->question = $question;
+ return $this;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): static
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function isUpvote(): bool
+ {
+ return $this->isUpvote;
+ }
+
+ public function setIsUpvote(bool $isUpvote): static
+ {
+ $this->isUpvote = $isUpvote;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 021b27d..b04c42c 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
+use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
@@ -20,6 +21,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?int $id = null;
#[ORM\Column(length: 180)]
+ #[Assert\NotBlank(message: 'ایمیل الزامی است')]
+ #[Assert\Email(message: 'فرمت ایمیل صحیح نیست')]
private ?string $email = null;
/**
@@ -35,18 +38,55 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
private ?string $password = null;
#[ORM\Column(length: 255)]
+ #[Assert\NotBlank(message: 'نام الزامی است')]
+ #[Assert\Length(min: 3, max: 60, minMessage: 'نام باید حداقل 3 کاراکتر باشد', maxMessage: 'نام نمیتواند بیش از 60 کاراکتر باشد')]
private ?string $name = null;
+ #[ORM\Column(length: 20, nullable: true)]
+ #[Assert\NotBlank(message: 'شماره موبایل الزامی است')]
+ #[Assert\Regex(pattern: '/^09[0-9]{9}$/', message: 'فرمت شماره موبایل صحیح نیست')]
+ private ?string $phone = null;
+
+ #[ORM\Column(type: 'boolean')]
+ private bool $isActive = true;
+
+ #[ORM\Column(type: 'datetime', nullable: true)]
+ private ?\DateTimeInterface $emailVerifiedAt = null;
+
+ #[ORM\Column(type: 'datetime')]
+ private ?\DateTimeInterface $createdAt = null;
+
+ #[ORM\Column(type: 'datetime', nullable: true)]
+ private ?\DateTimeInterface $updatedAt = null;
+
+ #[ORM\Column(type: 'datetime', nullable: true)]
+ private ?\DateTimeInterface $lastLoginAt = null;
+
+ #[ORM\Column(length: 50, nullable: true)]
+ private ?string $subscriptionType = null;
+
+ #[ORM\Column(type: 'datetime', nullable: true)]
+ private ?\DateTimeInterface $subscriptionExpiresAt = null;
+
/**
* @var Collection
*/
#[ORM\OneToMany(targetEntity: Post::class, mappedBy: 'submitter', orphanRemoval: true)]
private Collection $posts;
+ /**
+ * @var Collection
+ */
+ #[ORM\OneToMany(targetEntity: Wallet::class, mappedBy: 'user', orphanRemoval: true)]
+ private Collection $wallets;
+
public function __construct()
{
$this->posts = new ArrayCollection();
+ $this->wallets = new ArrayCollection();
+ $this->createdAt = new \DateTime();
+ $this->isActive = true;
}
public function getId(): ?int
@@ -166,4 +206,149 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
+ public function getPhone(): ?string
+ {
+ return $this->phone;
+ }
+
+ public function setPhone(?string $phone): static
+ {
+ $this->phone = $phone;
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ public function getEmailVerifiedAt(): ?\DateTimeInterface
+ {
+ return $this->emailVerifiedAt;
+ }
+
+ public function setEmailVerifiedAt(?\DateTimeInterface $emailVerifiedAt): static
+ {
+ $this->emailVerifiedAt = $emailVerifiedAt;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUpdatedAt(): ?\DateTimeInterface
+ {
+ return $this->updatedAt;
+ }
+
+ public function setUpdatedAt(?\DateTimeInterface $updatedAt): static
+ {
+ $this->updatedAt = $updatedAt;
+ return $this;
+ }
+
+ public function getLastLoginAt(): ?\DateTimeInterface
+ {
+ return $this->lastLoginAt;
+ }
+
+ public function setLastLoginAt(?\DateTimeInterface $lastLoginAt): static
+ {
+ $this->lastLoginAt = $lastLoginAt;
+ return $this;
+ }
+
+ public function getSubscriptionType(): ?string
+ {
+ return $this->subscriptionType;
+ }
+
+ public function setSubscriptionType(?string $subscriptionType): static
+ {
+ $this->subscriptionType = $subscriptionType;
+ return $this;
+ }
+
+ public function getSubscriptionExpiresAt(): ?\DateTimeInterface
+ {
+ return $this->subscriptionExpiresAt;
+ }
+
+ public function setSubscriptionExpiresAt(?\DateTimeInterface $subscriptionExpiresAt): static
+ {
+ $this->subscriptionExpiresAt = $subscriptionExpiresAt;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getWallets(): Collection
+ {
+ return $this->wallets;
+ }
+
+ public function addWallet(Wallet $wallet): static
+ {
+ if (!$this->wallets->contains($wallet)) {
+ $this->wallets->add($wallet);
+ $wallet->setUser($this);
+ }
+
+ return $this;
+ }
+
+ public function removeWallet(Wallet $wallet): static
+ {
+ if ($this->wallets->removeElement($wallet)) {
+ // set the owning side to null (unless already changed)
+ if ($wallet->getUser() === $this) {
+ $wallet->setUser(null);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get primary wallet
+ */
+ public function getPrimaryWallet(): ?Wallet
+ {
+ foreach ($this->wallets as $wallet) {
+ if ($wallet->isPrimary() && $wallet->isActive()) {
+ return $wallet;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get active wallets count
+ */
+ public function getActiveWalletsCount(): int
+ {
+ $count = 0;
+ foreach ($this->wallets as $wallet) {
+ if ($wallet->isActive()) {
+ $count++;
+ }
+ }
+ return $count;
+ }
+
}
diff --git a/src/Entity/Wallet.php b/src/Entity/Wallet.php
new file mode 100644
index 0000000..2153696
--- /dev/null
+++ b/src/Entity/Wallet.php
@@ -0,0 +1,177 @@
+connectedAt = new \DateTime();
+ $this->createdAt = new \DateTime();
+ $this->updatedAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): static
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function getWalletAddress(): ?string
+ {
+ return $this->walletAddress;
+ }
+
+ public function setWalletAddress(string $walletAddress): static
+ {
+ $this->walletAddress = $walletAddress;
+ return $this;
+ }
+
+ public function getWalletType(): ?string
+ {
+ return $this->walletType;
+ }
+
+ public function setWalletType(string $walletType): static
+ {
+ $this->walletType = $walletType;
+ return $this;
+ }
+
+ public function isPrimary(): bool
+ {
+ return $this->isPrimary;
+ }
+
+ public function setIsPrimary(bool $isPrimary): static
+ {
+ $this->isPrimary = $isPrimary;
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+ public function getConnectedAt(): ?\DateTimeInterface
+ {
+ return $this->connectedAt;
+ }
+
+ public function setConnectedAt(\DateTimeInterface $connectedAt): static
+ {
+ $this->connectedAt = $connectedAt;
+ return $this;
+ }
+
+ public function getLastUsedAt(): ?\DateTimeInterface
+ {
+ return $this->lastUsedAt;
+ }
+
+ public function setLastUsedAt(?\DateTimeInterface $lastUsedAt): static
+ {
+ $this->lastUsedAt = $lastUsedAt;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUpdatedAt(): ?\DateTimeInterface
+ {
+ return $this->updatedAt;
+ }
+
+ public function setUpdatedAt(\DateTimeInterface $updatedAt): static
+ {
+ $this->updatedAt = $updatedAt;
+ return $this;
+ }
+
+ public function getShortAddress(): string
+ {
+ if (!$this->walletAddress) {
+ return '';
+ }
+
+ return substr($this->walletAddress, 0, 6) . '...' . substr($this->walletAddress, -4);
+ }
+
+ public function updateLastUsed(): static
+ {
+ $this->lastUsedAt = new \DateTime();
+ $this->updatedAt = new \DateTime();
+ return $this;
+ }
+}
diff --git a/src/Form/CustomerRegistrationFormType.php b/src/Form/CustomerRegistrationFormType.php
index a3aaf1e..843ec57 100644
--- a/src/Form/CustomerRegistrationFormType.php
+++ b/src/Form/CustomerRegistrationFormType.php
@@ -2,7 +2,7 @@
namespace App\Form;
-use App\Entity\Customer;
+use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
@@ -106,7 +106,7 @@ class CustomerRegistrationFormType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
- 'data_class' => Customer::class,
+ 'data_class' => User::class,
]);
}
}
diff --git a/src/Form/QA/AnswerFormType.php b/src/Form/QA/AnswerFormType.php
new file mode 100644
index 0000000..2427308
--- /dev/null
+++ b/src/Form/QA/AnswerFormType.php
@@ -0,0 +1,40 @@
+add('content', TextareaType::class, [
+ 'label' => 'پاسخ شما',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 8,
+ 'placeholder' => 'پاسخ خود را وارد کنید...'
+ ],
+ 'constraints' => [
+ new Assert\NotBlank(['message' => 'متن پاسخ الزامی است']),
+ new Assert\Length([
+ 'min' => 10,
+ 'minMessage' => 'پاسخ باید حداقل 10 کاراکتر باشد'
+ ])
+ ]
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Answer::class,
+ ]);
+ }
+}
diff --git a/src/Form/QA/QuestionFormType.php b/src/Form/QA/QuestionFormType.php
new file mode 100644
index 0000000..42ff653
--- /dev/null
+++ b/src/Form/QA/QuestionFormType.php
@@ -0,0 +1,60 @@
+add('title', TextType::class, [
+ 'label' => 'عنوان سوال',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'placeholder' => 'عنوان سوال خود را وارد کنید...',
+ 'maxlength' => 255
+ ],
+ 'constraints' => [
+ new Assert\NotBlank(['message' => 'عنوان سوال الزامی است']),
+ new Assert\Length([
+ 'min' => 10,
+ 'max' => 255,
+ 'minMessage' => 'عنوان سوال باید حداقل 10 کاراکتر باشد',
+ 'maxMessage' => 'عنوان سوال نمیتواند بیش از 255 کاراکتر باشد'
+ ])
+ ]
+ ])
+ ->add('content', TextareaType::class, [
+ 'label' => 'متن سوال',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'rows' => 10,
+ 'placeholder' => 'سوال خود را به تفصیل شرح دهید...'
+ ],
+ 'constraints' => [
+ new Assert\NotBlank(['message' => 'متن سوال الزامی است']),
+ new Assert\Length([
+ 'min' => 20,
+ 'minMessage' => 'متن سوال باید حداقل 20 کاراکتر باشد'
+ ])
+ ]
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Question::class,
+ ]);
+ }
+}
diff --git a/src/Form/WalletConnectFormType.php b/src/Form/WalletConnectFormType.php
new file mode 100644
index 0000000..9613a62
--- /dev/null
+++ b/src/Form/WalletConnectFormType.php
@@ -0,0 +1,73 @@
+add('walletType', ChoiceType::class, [
+ 'label' => 'نوع کیف پول',
+ 'choices' => [
+ 'MetaMask' => 'MetaMask',
+ 'Trust Wallet' => 'Trust Wallet',
+ 'WalletConnect' => 'WalletConnect',
+ 'Coinbase Wallet' => 'Coinbase Wallet',
+ 'سایر' => 'Other'
+ ],
+ 'attr' => [
+ 'class' => 'form-control',
+ 'id' => 'walletType'
+ ],
+ 'constraints' => [
+ new Assert\NotBlank(['message' => 'لطفاً نوع کیف پول را انتخاب کنید'])
+ ]
+ ])
+ ->add('walletAddress', TextType::class, [
+ 'label' => 'آدرس کیف پول',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'id' => 'walletAddress',
+ 'placeholder' => '0x...',
+ 'readonly' => true,
+ 'data-turbo' => 'false'
+ ],
+ 'constraints' => [
+ new Assert\NotBlank(['message' => 'آدرس کیف پول الزامی است']),
+ new Assert\Regex([
+ 'pattern' => '/^0x[a-fA-F0-9]{40}$/',
+ 'message' => 'فرمت آدرس کیف پول صحیح نیست'
+ ])
+ ]
+ ])
+ ->add('signature', HiddenType::class, [
+ 'attr' => [
+ 'id' => 'signature'
+ ]
+ ])
+ ->add('connect', SubmitType::class, [
+ 'label' => 'اتصال کیف پول',
+ 'attr' => [
+ 'class' => 'btn btn-primary',
+ 'id' => 'connectWalletBtn'
+ ]
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => null,
+ ]);
+ }
+}
diff --git a/src/Repository/AnswerRepository.php b/src/Repository/AnswerRepository.php
new file mode 100644
index 0000000..f27bbc4
--- /dev/null
+++ b/src/Repository/AnswerRepository.php
@@ -0,0 +1,58 @@
+
+ */
+class AnswerRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Answer::class);
+ }
+
+ public function findAnswersByQuestion(int $questionId, int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('a')
+ ->where('a.question = :questionId')
+ ->andWhere('a.isActive = :active')
+ ->setParameter('questionId', $questionId)
+ ->setParameter('active', true)
+ ->orderBy('a.isAccepted', 'DESC')
+ ->addOrderBy('a.votes', 'DESC')
+ ->addOrderBy('a.createdAt', 'ASC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function countAnswersByQuestion(int $questionId): int
+ {
+ return $this->createQueryBuilder('a')
+ ->select('COUNT(a.id)')
+ ->where('a.question = :questionId')
+ ->andWhere('a.isActive = :active')
+ ->setParameter('questionId', $questionId)
+ ->setParameter('active', true)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function findMostVotedAnswers(int $limit = 10): array
+ {
+ return $this->createQueryBuilder('a')
+ ->where('a.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('a.votes', 'DESC')
+ ->addOrderBy('a.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
+ }
+}
diff --git a/src/Repository/AnswerVoteRepository.php b/src/Repository/AnswerVoteRepository.php
new file mode 100644
index 0000000..0401c24
--- /dev/null
+++ b/src/Repository/AnswerVoteRepository.php
@@ -0,0 +1,29 @@
+
+ */
+class AnswerVoteRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, AnswerVote::class);
+ }
+
+ public function findVoteByUserAndAnswer(int $userId, int $answerId): ?AnswerVote
+ {
+ return $this->createQueryBuilder('v')
+ ->where('v.user = :userId')
+ ->andWhere('v.answer = :answerId')
+ ->setParameter('userId', $userId)
+ ->setParameter('answerId', $answerId)
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+}
diff --git a/src/Repository/CustomerRepository.php b/src/Repository/CustomerRepository.php
deleted file mode 100644
index d7fe84f..0000000
--- a/src/Repository/CustomerRepository.php
+++ /dev/null
@@ -1,49 +0,0 @@
-
- */
-class CustomerRepository extends ServiceEntityRepository
-{
- public function __construct(ManagerRegistry $registry)
- {
- parent::__construct($registry, Customer::class);
- }
-
- public function save(Customer $entity, bool $flush = false): void
- {
- $this->getEntityManager()->persist($entity);
-
- if ($flush) {
- $this->getEntityManager()->flush();
- }
- }
-
- public function remove(Customer $entity, bool $flush = false): void
- {
- $this->getEntityManager()->remove($entity);
-
- if ($flush) {
- $this->getEntityManager()->flush();
- }
- }
-
- public function findByEmail(string $email): ?Customer
- {
- return $this->findOneBy(['email' => $email]);
- }
-
- public function findActiveByEmail(string $email): ?Customer
- {
- return $this->findOneBy([
- 'email' => $email,
- 'isActive' => true
- ]);
- }
-}
diff --git a/src/Repository/QuestionRepository.php b/src/Repository/QuestionRepository.php
new file mode 100644
index 0000000..c58b34b
--- /dev/null
+++ b/src/Repository/QuestionRepository.php
@@ -0,0 +1,170 @@
+
+ */
+class QuestionRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Question::class);
+ }
+
+ public function findActiveQuestions(int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('q')
+ ->where('q.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findQuestionsByTag(string $tagName, int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('q')
+ ->join('q.tagRelations', 'tr')
+ ->join('tr.tag', 't')
+ ->where('q.isActive = :active')
+ ->andWhere('t.name = :tagName')
+ ->setParameter('active', true)
+ ->setParameter('tagName', $tagName)
+ ->orderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function searchQuestions(string $searchTerm, int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('q')
+ ->where('q.isActive = :active')
+ ->andWhere('q.title LIKE :searchTerm OR q.content LIKE :searchTerm')
+ ->setParameter('active', true)
+ ->setParameter('searchTerm', '%' . $searchTerm . '%')
+ ->orderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findMostVotedQuestions(int $limit = 10): array
+ {
+ return $this->createQueryBuilder('q')
+ ->where('q.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('q.votes', 'DESC')
+ ->addOrderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findUnsolvedQuestions(int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('q')
+ ->where('q.isActive = :active')
+ ->andWhere('q.isSolved = :solved')
+ ->setParameter('active', true)
+ ->setParameter('solved', false)
+ ->orderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findSolvedQuestions(int $limit = 20, int $offset = 0): array
+ {
+ return $this->createQueryBuilder('q')
+ ->where('q.isActive = :active')
+ ->andWhere('q.isSolved = :solved')
+ ->setParameter('active', true)
+ ->setParameter('solved', true)
+ ->orderBy('q.createdAt', 'DESC')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function getTotalCount(): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->where('q.isActive = :active')
+ ->setParameter('active', true)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countSearchQuestions(string $searchTerm): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->where('q.isActive = :active')
+ ->andWhere('q.title LIKE :searchTerm OR q.content LIKE :searchTerm')
+ ->setParameter('active', true)
+ ->setParameter('searchTerm', '%' . $searchTerm . '%')
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countQuestionsByTag(string $tagName): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->join('q.tagRelations', 'tr')
+ ->join('tr.tag', 't')
+ ->where('q.isActive = :active')
+ ->andWhere('t.name = :tagName')
+ ->setParameter('active', true)
+ ->setParameter('tagName', $tagName)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countUnsolvedQuestions(): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->where('q.isActive = :active')
+ ->andWhere('q.isSolved = :solved')
+ ->setParameter('active', true)
+ ->setParameter('solved', false)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countSolvedQuestions(): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->where('q.isActive = :active')
+ ->andWhere('q.isSolved = :solved')
+ ->setParameter('active', true)
+ ->setParameter('solved', true)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ public function countMostVotedQuestions(): int
+ {
+ return $this->createQueryBuilder('q')
+ ->select('COUNT(q.id)')
+ ->where('q.isActive = :active')
+ ->setParameter('active', true)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+}
diff --git a/src/Repository/QuestionTagRelationRepository.php b/src/Repository/QuestionTagRelationRepository.php
new file mode 100644
index 0000000..2b7accd
--- /dev/null
+++ b/src/Repository/QuestionTagRelationRepository.php
@@ -0,0 +1,18 @@
+
+ */
+class QuestionTagRelationRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, QuestionTagRelation::class);
+ }
+}
diff --git a/src/Repository/QuestionTagRepository.php b/src/Repository/QuestionTagRepository.php
new file mode 100644
index 0000000..061a02e
--- /dev/null
+++ b/src/Repository/QuestionTagRepository.php
@@ -0,0 +1,62 @@
+
+ */
+class QuestionTagRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, QuestionTag::class);
+ }
+
+ public function findActiveTags(): array
+ {
+ return $this->createQueryBuilder('t')
+ ->where('t.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('t.usageCount', 'DESC')
+ ->addOrderBy('t.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findPopularTags(int $limit = 20): array
+ {
+ return $this->createQueryBuilder('t')
+ ->where('t.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('t.usageCount', 'DESC')
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findByName(string $name): ?QuestionTag
+ {
+ return $this->createQueryBuilder('t')
+ ->where('t.name = :name')
+ ->setParameter('name', $name)
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+
+ public function searchTags(string $searchTerm, int $limit = 10): array
+ {
+ return $this->createQueryBuilder('t')
+ ->where('t.isActive = :active')
+ ->andWhere('t.name LIKE :searchTerm')
+ ->setParameter('active', true)
+ ->setParameter('searchTerm', '%' . $searchTerm . '%')
+ ->orderBy('t.usageCount', 'DESC')
+ ->setMaxResults($limit)
+ ->getQuery()
+ ->getResult();
+ }
+}
diff --git a/src/Repository/QuestionVoteRepository.php b/src/Repository/QuestionVoteRepository.php
new file mode 100644
index 0000000..85e023c
--- /dev/null
+++ b/src/Repository/QuestionVoteRepository.php
@@ -0,0 +1,29 @@
+
+ */
+class QuestionVoteRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, QuestionVote::class);
+ }
+
+ public function findVoteByUserAndQuestion(int $userId, int $questionId): ?QuestionVote
+ {
+ return $this->createQueryBuilder('v')
+ ->where('v.user = :userId')
+ ->andWhere('v.question = :questionId')
+ ->setParameter('userId', $userId)
+ ->setParameter('questionId', $questionId)
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+}
diff --git a/src/Repository/WalletRepository.php b/src/Repository/WalletRepository.php
new file mode 100644
index 0000000..3594d26
--- /dev/null
+++ b/src/Repository/WalletRepository.php
@@ -0,0 +1,125 @@
+
+ */
+class WalletRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Wallet::class);
+ }
+
+ /**
+ * Find all active wallets for a user
+ */
+ public function findActiveWalletsByUser(User $user): array
+ {
+ return $this->createQueryBuilder('w')
+ ->andWhere('w.user = :user')
+ ->andWhere('w.isActive = :active')
+ ->setParameter('user', $user)
+ ->setParameter('active', true)
+ ->orderBy('w.isPrimary', 'DESC')
+ ->addOrderBy('w.connectedAt', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ /**
+ * Find primary wallet for a user
+ */
+ public function findPrimaryWalletByUser(User $user): ?Wallet
+ {
+ return $this->createQueryBuilder('w')
+ ->andWhere('w.user = :user')
+ ->andWhere('w.isPrimary = :primary')
+ ->andWhere('w.isActive = :active')
+ ->setParameter('user', $user)
+ ->setParameter('primary', true)
+ ->setParameter('active', true)
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+
+ /**
+ * Check if wallet address already exists for any user
+ */
+ public function isWalletAddressExists(string $walletAddress): bool
+ {
+ $result = $this->createQueryBuilder('w')
+ ->select('COUNT(w.id)')
+ ->andWhere('w.walletAddress = :address')
+ ->setParameter('address', $walletAddress)
+ ->getQuery()
+ ->getSingleScalarResult();
+
+ return $result > 0;
+ }
+
+ /**
+ * Check if wallet address exists for specific user
+ */
+ public function isWalletAddressExistsForUser(User $user, string $walletAddress): bool
+ {
+ $result = $this->createQueryBuilder('w')
+ ->select('COUNT(w.id)')
+ ->andWhere('w.user = :user')
+ ->andWhere('w.walletAddress = :address')
+ ->setParameter('user', $user)
+ ->setParameter('address', $walletAddress)
+ ->getQuery()
+ ->getSingleScalarResult();
+
+ return $result > 0;
+ }
+
+ /**
+ * Count active wallets for a user
+ */
+ public function countActiveWalletsByUser(User $user): int
+ {
+ return $this->createQueryBuilder('w')
+ ->select('COUNT(w.id)')
+ ->andWhere('w.user = :user')
+ ->andWhere('w.isActive = :active')
+ ->setParameter('user', $user)
+ ->setParameter('active', true)
+ ->getQuery()
+ ->getSingleScalarResult();
+ }
+
+ /**
+ * Find wallet by address
+ */
+ public function findByWalletAddress(string $walletAddress): ?Wallet
+ {
+ return $this->createQueryBuilder('w')
+ ->andWhere('w.walletAddress = :address')
+ ->setParameter('address', $walletAddress)
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+
+ /**
+ * Set all wallets as non-primary for a user
+ */
+ public function unsetAllPrimaryWalletsForUser(User $user): void
+ {
+ $this->createQueryBuilder('w')
+ ->update()
+ ->set('w.isPrimary', ':primary')
+ ->andWhere('w.user = :user')
+ ->setParameter('primary', false)
+ ->setParameter('user', $user)
+ ->getQuery()
+ ->execute();
+ }
+}
diff --git a/src/Service/MarkdownService.php b/src/Service/MarkdownService.php
new file mode 100644
index 0000000..0ed217c
--- /dev/null
+++ b/src/Service/MarkdownService.php
@@ -0,0 +1,21 @@
+parsedown = new Parsedown();
+ $this->parsedown->setSafeMode(true);
+ }
+
+ public function parse(string $text): string
+ {
+ return $this->parsedown->text($text);
+ }
+}
diff --git a/src/Service/WalletService.php b/src/Service/WalletService.php
new file mode 100644
index 0000000..40cce6b
--- /dev/null
+++ b/src/Service/WalletService.php
@@ -0,0 +1,232 @@
+entityManager = $entityManager;
+ $this->walletRepository = $walletRepository;
+ }
+
+ /**
+ * Connect a new wallet to user
+ */
+ public function connectWallet(User $user, string $walletAddress, string $walletType, string $signature): JsonResponse
+ {
+ // Validate wallet address format
+ if (!$this->isValidWalletAddress($walletAddress)) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'آدرس کیف پول نامعتبر است'
+ ], 400);
+ }
+
+ // Check if wallet already exists
+ if ($this->walletRepository->isWalletAddressExists($walletAddress)) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'این کیف پول قبلاً متصل شده است'
+ ], 400);
+ }
+
+ // Check if user has reached maximum wallet limit
+ if ($this->walletRepository->countActiveWalletsByUser($user) >= 5) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'حداکثر 5 کیف پول میتوانید متصل کنید'
+ ], 400);
+ }
+
+ // Verify signature (simplified - in real implementation, verify with Web3)
+ if (!$this->verifySignature($walletAddress, $signature)) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'امضای دیجیتال نامعتبر است'
+ ], 400);
+ }
+
+ // Create new wallet
+ $wallet = new Wallet();
+ $wallet->setUser($user);
+ $wallet->setWalletAddress($walletAddress);
+ $wallet->setWalletType($walletType);
+ $wallet->setIsPrimary($this->walletRepository->countActiveWalletsByUser($user) === 0);
+ $wallet->setIsActive(true);
+
+ $this->entityManager->persist($wallet);
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'success' => true,
+ 'message' => 'کیف پول با موفقیت متصل شد',
+ 'wallet' => [
+ 'id' => $wallet->getId(),
+ 'address' => $wallet->getWalletAddress(),
+ 'shortAddress' => $wallet->getShortAddress(),
+ 'type' => $wallet->getWalletType(),
+ 'isPrimary' => $wallet->isPrimary(),
+ 'connectedAt' => $wallet->getConnectedAt()->format('Y-m-d H:i:s')
+ ]
+ ]);
+ }
+
+ /**
+ * Set wallet as primary
+ */
+ public function setPrimaryWallet(User $user, int $walletId): JsonResponse
+ {
+ $wallet = $this->walletRepository->find($walletId);
+
+ if (!$wallet || $wallet->getUser() !== $user) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کیف پول یافت نشد'
+ ], 404);
+ }
+
+ if (!$wallet->isActive()) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کیف پول غیرفعال است'
+ ], 400);
+ }
+
+ // Unset all primary wallets for this user
+ $this->walletRepository->unsetAllPrimaryWalletsForUser($user);
+
+ // Set this wallet as primary
+ $wallet->setIsPrimary(true);
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'success' => true,
+ 'message' => 'کیف پول به عنوان اصلی تنظیم شد'
+ ]);
+ }
+
+ /**
+ * Toggle wallet status (active/inactive)
+ */
+ public function toggleWalletStatus(User $user, int $walletId): JsonResponse
+ {
+ $wallet = $this->walletRepository->find($walletId);
+
+ if (!$wallet || $wallet->getUser() !== $user) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کیف پول یافت نشد'
+ ], 404);
+ }
+
+ $wallet->setIsActive(!$wallet->isActive());
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'success' => true,
+ 'message' => $wallet->isActive() ? 'کیف پول فعال شد' : 'کیف پول غیرفعال شد'
+ ]);
+ }
+
+ /**
+ * Delete wallet
+ */
+ public function deleteWallet(User $user, int $walletId): JsonResponse
+ {
+ $wallet = $this->walletRepository->find($walletId);
+
+ if (!$wallet || $wallet->getUser() !== $user) {
+ return new JsonResponse([
+ 'success' => false,
+ 'message' => 'کیف پول یافت نشد'
+ ], 404);
+ }
+
+ $this->entityManager->remove($wallet);
+ $this->entityManager->flush();
+
+ return new JsonResponse([
+ 'success' => true,
+ 'message' => 'کیف پول حذف شد'
+ ]);
+ }
+
+ /**
+ * Get user's wallets
+ */
+ public function getUserWallets(User $user): array
+ {
+ $wallets = $this->walletRepository->findActiveWalletsByUser($user);
+
+ return array_map(function (Wallet $wallet) {
+ return [
+ 'id' => $wallet->getId(),
+ 'address' => $wallet->getWalletAddress(),
+ 'shortAddress' => $wallet->getShortAddress(),
+ 'type' => $wallet->getWalletType(),
+ 'isPrimary' => $wallet->isPrimary(),
+ 'isActive' => $wallet->isActive(),
+ 'connectedAt' => $wallet->getConnectedAt()->format('Y-m-d H:i:s'),
+ 'lastUsedAt' => $wallet->getLastUsedAt() ? $wallet->getLastUsedAt()->format('Y-m-d H:i:s') : null
+ ];
+ }, $wallets);
+ }
+
+ /**
+ * Update wallet last used time
+ */
+ public function updateWalletLastUsed(Wallet $wallet): void
+ {
+ $wallet->updateLastUsed();
+ $this->entityManager->flush();
+ }
+
+ /**
+ * Validate wallet address format
+ */
+ private function isValidWalletAddress(string $address): bool
+ {
+ // Basic Ethereum address validation
+ $isValid = preg_match('/^0x[a-fA-F0-9]{40}$/', $address) === 1;
+
+ // Log for debugging
+ error_log("Wallet address validation: $address - " . ($isValid ? 'valid' : 'invalid'));
+
+ return $isValid;
+ }
+
+ /**
+ * Verify digital signature (simplified implementation)
+ * In real implementation, this should verify the signature using Web3
+ */
+ private function verifySignature(string $walletAddress, string $signature): bool
+ {
+ // This is a simplified implementation
+ // In real implementation, you should verify the signature using Web3 libraries
+ return !empty($signature) && strlen($signature) > 10;
+ }
+
+ /**
+ * Generate message for signing
+ */
+ public function generateSignMessage(User $user): string
+ {
+ $timestamp = time();
+ $message = "Connect wallet to Hesabix\n";
+ $message .= "User: " . $user->getEmail() . "\n";
+ $message .= "Timestamp: " . $timestamp . "\n";
+ $message .= "Nonce: " . bin2hex(random_bytes(16));
+
+ return $message;
+ }
+}
diff --git a/src/Twig/MarkdownExtension.php b/src/Twig/MarkdownExtension.php
new file mode 100644
index 0000000..14bc4e7
--- /dev/null
+++ b/src/Twig/MarkdownExtension.php
@@ -0,0 +1,40 @@
+ ['html']]),
+ ];
+ }
+
+ public function parseMarkdown(string $text): string
+ {
+ // تبدیل Markdown ساده به HTML
+ $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+
+ // Bold: **text** -> text
+ $text = preg_replace('/\*\*(.*?)\*\*/', '$1 ', $text);
+
+ // Italic: *text* -> text
+ $text = preg_replace('/\*(.*?)\*/', '$1 ', $text);
+
+ // Code: `code` -> code
+ $text = preg_replace('/`(.*?)`/', '$1', $text);
+
+ // Line breaks: \n ->
+ $text = nl2br($text);
+
+ // Lists: - item -> item
+ $text = preg_replace('/^- (.+)$/m', '$1 ', $text);
+ $text = preg_replace('/(.*<\/li>)/s', '', $text);
+
+ return $text;
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index 2a77d30..557673a 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -1,4 +1,13 @@
{
+ "doctrine/deprecations": {
+ "version": "1.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "87424683adc81d7dc305eefec1fced883084aab9"
+ }
+ },
"doctrine/doctrine-bundle": {
"version": "2.13",
"recipe": {
diff --git a/templates/base.html.twig b/templates/base.html.twig
index ed5ca14..c5e8fdd 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -77,11 +77,105 @@ gtag('config', 'G-K1R1SYQY8E');
background-color: #f8d7da;
color: #721c24;
}
+
+ /* استایلهای گواهیهای اعتماد */
+ .trust-seal-loading {
+ animation: pulse 1.5s ease-in-out infinite;
+ }
+
+ .trust-seals-content img {
+ transition: opacity 0.3s ease-in-out;
+ max-height: 50px;
+ width: auto;
+ }
+
+ .trust-seals-content img:hover {
+ opacity: 0.8;
+ transform: scale(1.05);
+ }
+
+ @keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ 100% { opacity: 1; }
+ }
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
+
+
+
{% endblock %}
@@ -105,18 +199,28 @@ gtag('config', 'G-K1R1SYQY8E');
وبلاگ
+
+ سوالات
+
حامیان مالی
-
- داستان حسابیکس
-
-
- تماس با ما
+
+
+ درباره ما
+
+
- {% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
+ {% if app.user and app.user.roles is defined and 'ROLE_CUSTOMER' in app.user.roles %}
{# کاربر وارد شده - نمایش منوی داشبورد #}
@@ -152,12 +256,23 @@ gtag('config', 'G-K1R1SYQY8E');
{% block body %}{% endblock %}
-
-
-
-
-
-
+
+
+
+
+
در حال بارگذاری گواهیهای اعتماد...
+
+
+
+
diff --git a/templates/customer/dashboard.html.twig b/templates/customer/dashboard.html.twig
index 8c9e083..3810e2e 100644
--- a/templates/customer/dashboard.html.twig
+++ b/templates/customer/dashboard.html.twig
@@ -200,6 +200,90 @@
direction: rtl;
text-align: right;
}
+
+ /* استایلهای کیف پول */
+ .wallet-connect-form {
+ background: #ffffff;
+ padding: 25px;
+ border-radius: 12px;
+ margin-bottom: 25px;
+ border: 1px solid #e9ecef;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
+ }
+
+ .wallet-connect-form h5 {
+ color: #0d6efd;
+ margin-bottom: 20px;
+ direction: rtl;
+ text-align: right;
+ font-weight: 600;
+ }
+
+ #walletAddress {
+ font-family: 'Courier New', monospace;
+ font-size: 14px;
+ direction: ltr;
+ text-align: left;
+ }
+
+ #walletAddress:read-only {
+ background-color: #f8f9fa;
+ cursor: not-allowed;
+ border: 1px solid #e9ecef;
+ }
+
+ .wallet-connect-form .input-group-text {
+ background-color: #e9ecef;
+ border: 1px solid #e9ecef;
+ color: #6c757d;
+ }
+
+ .wallet-connect-form .btn-lg {
+ padding: 12px 24px;
+ font-size: 1.1rem;
+ font-weight: 600;
+ }
+
+ .wallet-connect-form .btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ .wallets-list table {
+ direction: rtl;
+ margin-top: 20px;
+ }
+
+ .wallets-list th,
+ .wallets-list td {
+ text-align: right;
+ vertical-align: middle;
+ padding: 12px;
+ }
+
+ .wallets-list th {
+ background-color: #f8f9fa;
+ font-weight: 600;
+ border-bottom: 2px solid #dee2e6;
+ }
+
+ .wallets-list code {
+ font-family: 'Courier New', monospace;
+ font-size: 12px;
+ background: #f8f9fa;
+ padding: 4px 8px;
+ border-radius: 6px;
+ border: 1px solid #e9ecef;
+ }
+
+ .btn-group-sm .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ }
+
+ .btn-group-sm .btn i {
+ margin-left: 4px;
+ }
{% endblock %}
@@ -218,23 +302,23 @@
نام و نام خانوادگی:
- {{ customer.name }}
+ {{ user.name }}
پست الکترونیکی:
- {{ customer.email }}
+ {{ user.email }}
شماره موبایل:
- {{ customer.phone }}
+ {{ user.phone }}
وضعیت حساب:
-
- {{ customer.isActive ? 'فعال' : 'غیرفعال' }}
+
+ {{ user.isActive ? 'فعال' : 'غیرفعال' }}
@@ -246,33 +330,161 @@
تاریخ عضویت:
- {{ customer.createdAt|date('Y/m/d') }}
+ {{ user.createdAt|date('Y/m/d') }}
آخرین ورود:
- {{ customer.lastLoginAt ? customer.lastLoginAt|date('Y/m/d H:i') : 'هنوز وارد نشده' }}
+ {{ user.lastLoginAt ? user.lastLoginAt|date('Y/m/d H:i') : 'هنوز وارد نشده' }}
تایید ایمیل:
-
- {{ customer.emailVerifiedAt ? 'تایید شده' : 'تایید نشده' }}
+
+ {{ user.emailVerifiedAt ? 'تایید شده' : 'تایید نشده' }}
- {% if customer.subscriptionType %}
+ {% if user.subscriptionType %}
نوع اشتراک:
- {{ customer.subscriptionType }}
+ {{ user.subscriptionType }}
{% endif %}
+
+
+
+
+
مدیریت کیف پولها
+
+
+
+
+
+
+
کیف پولهای متصل ({{ user.wallets|length }}/5)
+
+ {% if user.wallets|length > 0 %}
+
+
+
+
+ آدرس کیف پول
+ نوع
+ وضعیت
+ تاریخ اتصال
+ عملیات
+
+
+
+ {% for wallet in user.wallets %}
+
+
+ {{ wallet.shortAddress }}
+ {% if wallet.isPrimary %}
+ اصلی
+ {% endif %}
+
+
+ {{ wallet.walletType }}
+
+
+
+ {{ wallet.isActive ? 'فعال' : 'غیرفعال' }}
+
+
+ {{ wallet.connectedAt|date('Y/m/d H:i') }}
+
+
+ {% if not wallet.isPrimary and wallet.isActive %}
+
+ اصلی
+
+ {% endif %}
+
+
+
+ {{ wallet.isActive ? 'غیرفعال' : 'فعال' }}
+
+
+
+ حذف
+
+
+
+
+ {% endfor %}
+
+
+
+ {% else %}
+
+
+
هنوز هیچ کیف پولی متصل نشده است
+
برای شروع، نوع کیف پول خود را انتخاب کنید و روی دکمه اتصال کلیک کنید
+
+ {% endif %}
+
+
+
+
+
{% endblock %}
+
+{% block javascripts %}
+ {{ parent() }}
+{% endblock %}
diff --git a/templates/general/home.html.twig b/templates/general/home.html.twig
index 94fceb8..ce57621 100644
--- a/templates/general/home.html.twig
+++ b/templates/general/home.html.twig
@@ -599,15 +599,19 @@
}
{% endblock %}
diff --git a/templates/general/sitemap.html.twig b/templates/general/sitemap.html.twig
index 209ecef..876e63b 100644
--- a/templates/general/sitemap.html.twig
+++ b/templates/general/sitemap.html.twig
@@ -17,6 +17,16 @@
2025-01-09T06:58:03+00:00
1.00
+
+ {{ absolute_url(path('qa_index')) }}
+ 2025-01-09T06:58:03+00:00
+ 0.90
+
+
+ {{ absolute_url(path('qa_tags')) }}
+ 2025-01-09T06:58:03+00:00
+ 0.80
+
{% for post in posts %}
{% if post.cat.code == 'plain' %}
@@ -44,4 +54,18 @@
{% endif %}
{% endfor %}
+ {% for question in questions %}
+
+ {{ absolute_url(path('qa_question_show', {'id': question.id})) }}
+ {{ question.createdAt|date('c') }}
+ 0.70
+
+ {% endfor %}
+ {% for tag in tags %}
+
+ {{ absolute_url(path('qa_tag_questions', {'name': tag.name})) }}
+ 2025-01-09T06:58:03+00:00
+ 0.60
+
+ {% endfor %}
diff --git a/templates/installation/index.html.twig b/templates/installation/index.html.twig
index f622b7a..a9952cd 100644
--- a/templates/installation/index.html.twig
+++ b/templates/installation/index.html.twig
@@ -407,7 +407,7 @@
{{ parent() }}
{% endblock %}
\ No newline at end of file
diff --git a/templates/qa/answer_question.html.twig b/templates/qa/answer_question.html.twig
new file mode 100644
index 0000000..e764ecc
--- /dev/null
+++ b/templates/qa/answer_question.html.twig
@@ -0,0 +1,218 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}پاسخ به سوال: {{ question.title }} - پرسش و پاسخ{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+
{{ question.title }}
+
+ {{ question.content|nl2br }}
+
+
+
+ {% for tagRelation in question.tagRelations %}
+
+ {{ tagRelation.tag.name }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
+
+
+ {{ form_label(form.content) }}
+
+ {{ form_widget(form.content, {'attr': {'class': 'form-control editor-textarea', 'rows': 8}}) }}
+ {{ form_errors(form.content) }}
+
+ پاسخ خود را به صورت کامل و مفصل ارائه دهید. هرچه پاسخ شما دقیقتر باشد، برای دیگران مفیدتر خواهد بود.
+ میتوانید از دکمههای بالا برای فرمت کردن متن استفاده کنید.
+
+
+
+
+
+ انصراف
+
+
+ ارسال پاسخ
+
+
+
+ {{ form_end(form) }}
+
+
+
+
+
+
+
+
+
+
✅ پاسخ خوب:
+
+ مستقیماً به سوال پاسخ دهید
+ از مثالهای عملی استفاده کنید
+ منابع و لینکهای مفید ارائه دهید
+ کد و نمونههای کد ارائه دهید
+
+
+
+
❌ پاسخ ضعیف:
+
+ پاسخ مبهم و کلی
+ عدم ارائه راهحل عملی
+ تکرار پاسخهای قبلی
+ پاسخ نامربوط
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/templates/qa/ask_question.html.twig b/templates/qa/ask_question.html.twig
new file mode 100644
index 0000000..6b1a7d9
--- /dev/null
+++ b/templates/qa/ask_question.html.twig
@@ -0,0 +1,469 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}پرسیدن سوال جدید - پرسش و پاسخ{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+ {{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
+
+
+ {{ form_label(form.title) }}
+ {{ form_widget(form.title) }}
+ {{ form_errors(form.title) }}
+
+ عنوان سوال باید واضح و مختصر باشد. سعی کنید مشکل خود را در یک جمله خلاصه کنید.
+
+
+
+
+ {{ form_label(form.content) }}
+
+ {{ form_widget(form.content, {'attr': {'class': 'form-control editor-textarea', 'rows': 10}}) }}
+ {{ form_errors(form.content) }}
+
+ سوال خود را به تفصیل شرح دهید. هرچه جزئیات بیشتری ارائه دهید، پاسخهای بهتری دریافت خواهید کرد.
+ میتوانید از دکمههای بالا برای فرمت کردن متن استفاده کنید.
+
+
+
+
+
تگها
+
+
+ تگهای مرتبط را انتخاب کنید تا دیگران راحتتر بتوانند سوال شما را پیدا کنند.
+ میتوانید تگ جدید ایجاد کنید یا از تگهای موجود انتخاب کنید.
+
+
+
+
+
+
+
+
+ انصراف
+
+
+
+
+
+ ارسال سوال
+
+
+
+ {{ form_end(form) }}
+
+
+
+
+
+
+
+
+
+
✅ کارهای درست:
+
+ عنوان واضح و مختصر
+ شرح کامل مشکل
+ انتخاب تگهای مناسب
+ استفاده از کلمات کلیدی
+
+
+
+
❌ کارهای نادرست:
+
+ عنوان مبهم
+ شرح ناکافی
+ عدم انتخاب تگ
+ سوال تکراری
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/templates/qa/index.html.twig b/templates/qa/index.html.twig
new file mode 100644
index 0000000..b6cdce8
--- /dev/null
+++ b/templates/qa/index.html.twig
@@ -0,0 +1,306 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}پرسش و پاسخ - حسابیکس{% endblock %}
+
+{% block body %}
+
+
+
+
+
پرسش و پاسخ
+ {% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
+
+
+
+
+ سوال جدید
+
+ {% else %}
+
+
+
+
+
+ ورود برای پرسیدن سوال
+
+ {% endif %}
+
+
+
+
+
+
+
+
+ {% if questions is empty %}
+
+
+
+
سوالی یافت نشد
+
هنوز سوالی در این دستهبندی وجود ندارد.
+ {% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
+
اولین سوال را بپرسید
+ {% endif %}
+
+
+ {% else %}
+ {% for question in questions %}
+
+
+
+
+
+
+ {{ question.votes }}
+
+
رای
+
+
+
+
+
+ {{ question.answers|length }}
+
+
پاسخ
+
+
+
+
+
+ {% if question.isSolved %}
+
+ حل شده
+
+ {% endif %}
+
+
+
+ {% set content = question.content|markdown|raw %}
+ {% set plainText = content|striptags %}
+ {% if plainText|length > 200 %}
+ {{ plainText|slice(0, 200) }}...
+ {% else %}
+ {{ plainText }}
+ {% endif %}
+
+
+
+
+
+ {{ question.author.name }}
+ {{ question.createdAt|date('Y/m/d H:i') }}
+ {{ question.views }}
+
+
+
+
+
+
+ {% endfor %}
+
+
+ {% if totalPages > 1 %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ questions|length }}
+
سوال
+
+
+
{{ popularTags|length }}
+
تگ
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/templates/qa/question_show.html.twig b/templates/qa/question_show.html.twig
new file mode 100644
index 0000000..46cad6b
--- /dev/null
+++ b/templates/qa/question_show.html.twig
@@ -0,0 +1,341 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}{{ question.title }} - پرسش و پاسخ{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ question.votes }}
+
+
+
+
+
+
+
+
+
+
+
{{ question.title }}
+ {% if question.isSolved %}
+
+ حل شده
+
+ {% endif %}
+
+
+
+ {{ question.content|markdown|raw }}
+
+
+
+
+
+ {{ question.author.name }}
+ {{ question.createdAt|date('Y/m/d H:i') }}
+ {{ question.views }} بازدید
+
+
+
+
+
+
+
+
+
+
+
+ پاسخها
+ {{ totalAnswers }}
+
+ {% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
+
+ پاسخ دهید
+
+ {% else %}
+
+ ورود برای پاسخ دادن
+
+ {% endif %}
+
+
+ {% if answers is empty %}
+
+
+
+
هنوز پاسخی داده نشده
+
اولین کسی باشید که به این سوال پاسخ میدهد.
+
+
+ {% else %}
+ {% for answer in answers %}
+
+
+
+
+
+
+
+
+
+
+
+ {{ answer.votes }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ answer.content|markdown|raw }}
+
+ {% if answer.isAccepted %}
+
+ پاسخ پذیرفته شده
+
+ {% endif %}
+
+
+
+
+ {{ answer.author.name }}
+ {{ answer.createdAt|date('Y/m/d H:i') }}
+
+
+ {% if app.user and app.user == question.author and not answer.isAccepted %}
+
+
+
+ پذیرفتن پاسخ
+
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+
+
+ {% if totalPages > 1 %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/templates/qa/tag_questions.html.twig b/templates/qa/tag_questions.html.twig
new file mode 100644
index 0000000..e6d5d6a
--- /dev/null
+++ b/templates/qa/tag_questions.html.twig
@@ -0,0 +1,237 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}سوالات تگ {{ tag.name }} - پرسش و پاسخ{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+ سوالات تگ "{{ tag.name }}"
+
+ {% if tag.description %}
+
{{ tag.description }}
+ {% endif %}
+
+
+ بازگشت به همه سوالات
+
+
+
+
+
+ {% if questions is empty %}
+
+
+
+
سوالی یافت نشد
+
هنوز سوالی با تگ "{{ tag.name }}" وجود ندارد.
+ {% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
+
اولین سوال را بپرسید
+ {% endif %}
+
+
+ {% else %}
+ {% for question in questions %}
+
+
+
+
+
+
+ {{ question.votes }}
+
+
رای
+
+
+
+
+
+ {{ question.answers|length }}
+
+
پاسخ
+
+
+
+
+
+ {% if question.isSolved %}
+
+ حل شده
+
+ {% endif %}
+
+
+
+ {% set content = question.content|markdown|raw %}
+ {% set plainText = content|striptags %}
+ {% if plainText|length > 200 %}
+ {{ plainText|slice(0, 200) }}...
+ {% else %}
+ {{ plainText }}
+ {% endif %}
+
+
+
+
+
+ {{ question.author.name }}
+ {{ question.createdAt|date('Y/m/d H:i') }}
+ {{ question.views }}
+
+
+
+
+
+
+ {% endfor %}
+
+
+ {% if totalPages > 1 %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
{{ tag.name }}
+ {% if tag.color %}
+
+ {% endif %}
+
+
+ {% if tag.description %}
+
{{ tag.description }}
+ {% endif %}
+
+
+
+
{{ questions|length }}
+
سوال
+
+
+
{{ tag.usageCount }}
+
استفاده
+
+
+
+
+
+
+
+
+
+
تگهای مشابه که ممکن است برای شما مفید باشند:
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/templates/qa/tags.html.twig b/templates/qa/tags.html.twig
new file mode 100644
index 0000000..4dff0f4
--- /dev/null
+++ b/templates/qa/tags.html.twig
@@ -0,0 +1,87 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}تگها - پرسش و پاسخ{% endblock %}
+
+{% block body %}
+
+
+
+
+
+ {% if tags is empty %}
+
+
+
+
تگی یافت نشد
+
هنوز تگی در سیستم ثبت نشده است.
+
+
+ {% else %}
+
+ {% for tag in tags %}
+
+
+
+
+
+
{{ tag.usageCount }}
+
+
+ {% if tag.description %}
+
+ {{ tag.description }}
+
+ {% endif %}
+
+
+
+ مشاهده سوالات
+
+ {% if tag.color %}
+
+ {% endif %}
+
+
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}