This commit is contained in:
parent
1467e83ccf
commit
be126d506b
10
config/packages/easy_admin.yaml
Normal file
10
config/packages/easy_admin.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
easy_admin:
|
||||||
|
site_name: 'پیشخوان ادمین'
|
||||||
|
site_url: '/admin'
|
||||||
|
favicon_path: 'favicon/favicon.ico'
|
||||||
|
default_locale: 'fa'
|
||||||
|
locales: ['fa', 'en']
|
||||||
|
format_datetime: 'Y-m-d H:i:s'
|
||||||
|
format_date: 'Y-m-d'
|
||||||
|
format_time: 'H:i:s'
|
||||||
|
timezone: 'Asia/Tehran'
|
||||||
|
|
@ -19,7 +19,9 @@ security:
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
form_login:
|
form_login:
|
||||||
login_path: login
|
login_path: login
|
||||||
check_path: login
|
check_path: login_check
|
||||||
|
enable_csrf: true
|
||||||
|
default_target_path: admin
|
||||||
logout:
|
logout:
|
||||||
path: logout
|
path: logout
|
||||||
customer:
|
customer:
|
||||||
|
|
@ -61,6 +63,10 @@ security:
|
||||||
# Easy way to control access for large sections of your site
|
# Easy way to control access for large sections of your site
|
||||||
# Note: Only the *first* access control that matches will be used
|
# Note: Only the *first* access control that matches will be used
|
||||||
access_control:
|
access_control:
|
||||||
|
# اجازه دسترسی عمومی به صفحه ورود و خروج ادمین
|
||||||
|
- { path: ^/admin/login$, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/admin/logout$, roles: PUBLIC_ACCESS }
|
||||||
|
# محافظت از سایر مسیرهای /admin
|
||||||
- { path: ^/admin, roles: ROLE_ADMIN }
|
- { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
- { path: ^/customer/dashboard, roles: ROLE_CUSTOMER }
|
- { path: ^/customer/dashboard, roles: ROLE_CUSTOMER }
|
||||||
# - { path: ^/profile, roles: ROLE_USER }
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# config/routes/easyadmin.yaml
|
|
||||||
easyadmin:
|
|
||||||
resource: .
|
|
||||||
type: easyadmin.routes
|
|
||||||
|
|
@ -31,3 +31,15 @@ services:
|
||||||
# Markdown extension for Twig
|
# Markdown extension for Twig
|
||||||
App\Twig\MarkdownExtension:
|
App\Twig\MarkdownExtension:
|
||||||
tags: ['twig.extension']
|
tags: ['twig.extension']
|
||||||
|
|
||||||
|
# Jdate extension for Twig
|
||||||
|
App\Twig\JdateExtension:
|
||||||
|
tags: ['twig.extension']
|
||||||
|
|
||||||
|
# Attachment service
|
||||||
|
App\Service\AttachmentService:
|
||||||
|
arguments:
|
||||||
|
$targetDirectory: '%kernel.project_dir%/public/uploads/attachments'
|
||||||
|
|
||||||
|
# Email notification service
|
||||||
|
App\Service\EmailNotificationService: ~
|
||||||
31
migrations/Version20250905092845.php
Normal file
31
migrations/Version20250905092845.php
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250905092845 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE comment CHANGE name name VARCHAR(255) NOT NULL, CHANGE publish publish TINYINT(1) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE comment CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE publish publish TINYINT(1) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20250905122328.php
Normal file
34
migrations/Version20250905122328.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250905122328 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
// First, convert existing timestamp values to datetime format
|
||||||
|
$this->addSql('UPDATE post SET date_submit = FROM_UNIXTIME(CAST(date_submit AS UNSIGNED)) WHERE date_submit REGEXP "^[0-9]+$"');
|
||||||
|
// Then change the column type
|
||||||
|
$this->addSql('ALTER TABLE post CHANGE date_submit date_submit DATETIME NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE post CHANGE date_submit date_submit VARCHAR(50) NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20250905122520.php
Normal file
34
migrations/Version20250905122520.php
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250905122520 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
// First, convert existing timestamp values to datetime format
|
||||||
|
$this->addSql('UPDATE comment SET date_submit = FROM_UNIXTIME(CAST(date_submit AS UNSIGNED)) WHERE date_submit REGEXP "^[0-9]+$"');
|
||||||
|
// Then change the column type
|
||||||
|
$this->addSql('ALTER TABLE comment CHANGE date_submit date_submit DATETIME NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE comment CHANGE date_submit date_submit VARCHAR(255) NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
migrations/Version20250905124108.php
Normal file
39
migrations/Version20250905124108.php
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250905124108 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('CREATE TABLE attachment (id INT AUTO_INCREMENT NOT NULL, question_id INT DEFAULT NULL, answer_id INT DEFAULT NULL, uploaded_by_id INT NOT NULL, filename VARCHAR(255) NOT NULL, original_filename VARCHAR(255) NOT NULL, mime_type VARCHAR(100) NOT NULL, size INT NOT NULL, path VARCHAR(500) NOT NULL, uploaded_at DATETIME NOT NULL, INDEX IDX_795FD9BB1E27F6BF (question_id), INDEX IDX_795FD9BBAA334807 (answer_id), INDEX IDX_795FD9BBA2B28FE8 (uploaded_by_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||||
|
$this->addSql('ALTER TABLE attachment ADD CONSTRAINT FK_795FD9BB1E27F6BF FOREIGN KEY (question_id) REFERENCES question (id)');
|
||||||
|
$this->addSql('ALTER TABLE attachment ADD CONSTRAINT FK_795FD9BBAA334807 FOREIGN KEY (answer_id) REFERENCES answer (id)');
|
||||||
|
$this->addSql('ALTER TABLE attachment ADD CONSTRAINT FK_795FD9BBA2B28FE8 FOREIGN KEY (uploaded_by_id) REFERENCES `user` (id)');
|
||||||
|
$this->addSql('ALTER TABLE question ADD notify_on_answer TINYINT(1) NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE attachment DROP FOREIGN KEY FK_795FD9BB1E27F6BF');
|
||||||
|
$this->addSql('ALTER TABLE attachment DROP FOREIGN KEY FK_795FD9BBAA334807');
|
||||||
|
$this->addSql('ALTER TABLE attachment DROP FOREIGN KEY FK_795FD9BBA2B28FE8');
|
||||||
|
$this->addSql('DROP TABLE attachment');
|
||||||
|
$this->addSql('ALTER TABLE question DROP notify_on_answer');
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/Controller/Admin/CommentCrudController.php
Normal file
63
src/Controller/Admin/CommentCrudController.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Config\Filters;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\BooleanField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Filter\BooleanFilter;
|
||||||
|
use EasyCorp\Bundle\EasyAdminBundle\Filter\EntityFilter;
|
||||||
|
|
||||||
|
class CommentCrudController extends AbstractCrudController
|
||||||
|
{
|
||||||
|
public static function getEntityFqcn(): string
|
||||||
|
{
|
||||||
|
return Comment::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFields(string $pageName): iterable
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
IdField::new('id', 'شناسه')->hideOnForm(),
|
||||||
|
AssociationField::new('post', 'پست مربوطه'),
|
||||||
|
TextField::new('name', 'نام'),
|
||||||
|
EmailField::new('email', 'ایمیل'),
|
||||||
|
UrlField::new('website', 'وبسایت'),
|
||||||
|
TextareaField::new('body', 'متن کامنت')
|
||||||
|
->setMaxLength(500)
|
||||||
|
->hideOnIndex(),
|
||||||
|
DateTimeField::new('dateSubmit', 'تاریخ ارسال')
|
||||||
|
->setFormat('Y/m/d H:i:s')
|
||||||
|
->hideOnForm(),
|
||||||
|
BooleanField::new('publish', 'تایید شده')
|
||||||
|
->setHelp('کامنتهای تایید شده در سایت نمایش داده میشوند'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureCrud(Crud $crud): Crud
|
||||||
|
{
|
||||||
|
return $crud
|
||||||
|
->setEntityLabelInSingular('کامنت')
|
||||||
|
->setEntityLabelInPlural('کامنتها')
|
||||||
|
->setDefaultSort(['dateSubmit' => 'DESC'])
|
||||||
|
->setPaginatorPageSize(20)
|
||||||
|
->setSearchFields(['name', 'email', 'body'])
|
||||||
|
->setHelp('index', 'کامنتهای جدید نیاز به تایید دارند تا در سایت نمایش داده شوند');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureFilters(Filters $filters): Filters
|
||||||
|
{
|
||||||
|
return $filters
|
||||||
|
->add(EntityFilter::new('post', 'پست مربوطه'))
|
||||||
|
->add(BooleanFilter::new('publish', 'وضعیت تایید'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Controller\Admin;
|
namespace App\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Entity\Comment;
|
||||||
use App\Entity\Post;
|
use App\Entity\Post;
|
||||||
use App\Entity\Tree;
|
use App\Entity\Tree;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
|
@ -14,8 +15,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
class DashboardController extends AbstractDashboardController
|
class DashboardController extends AbstractDashboardController
|
||||||
{
|
{
|
||||||
#[Route('/admin/{_locale}', name: 'admin')]
|
#[Route('/admin', name: 'admin')]
|
||||||
public function index($_locale = 'fa'): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
//return parent::index();
|
//return parent::index();
|
||||||
|
|
||||||
|
|
@ -45,10 +46,7 @@ class DashboardController extends AbstractDashboardController
|
||||||
->renderContentMaximized()
|
->renderContentMaximized()
|
||||||
->setDefaultColorScheme('dark')
|
->setDefaultColorScheme('dark')
|
||||||
->generateRelativeUrls()
|
->generateRelativeUrls()
|
||||||
->setLocales([
|
;
|
||||||
'fa' => Locale::new('fa', 'فارسی', 'fa_IR'), // زبان پیشفرض
|
|
||||||
'en' => Locale::new('en', 'English', 'en_US'),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureMenuItems(): iterable
|
public function configureMenuItems(): iterable
|
||||||
|
|
@ -59,6 +57,7 @@ class DashboardController extends AbstractDashboardController
|
||||||
MenuItem::section('پست بلاگ'),
|
MenuItem::section('پست بلاگ'),
|
||||||
MenuItem::linkToCrud('دسته بندی', 'fa fa-tags', Tree::class),
|
MenuItem::linkToCrud('دسته بندی', 'fa fa-tags', Tree::class),
|
||||||
MenuItem::linkToCrud('محتوا', 'fa fa-file-text', Post::class),
|
MenuItem::linkToCrud('محتوا', 'fa fa-file-text', Post::class),
|
||||||
|
MenuItem::linkToCrud('کامنتها', 'fa fa-comments', Comment::class),
|
||||||
|
|
||||||
MenuItem::section('کاربران'),
|
MenuItem::section('کاربران'),
|
||||||
MenuItem::linkToCrud('کاربران', 'fa fa-user', User::class),
|
MenuItem::linkToCrud('کاربران', 'fa fa-user', User::class),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Cat;
|
use App\Entity\Cat;
|
||||||
|
use App\Entity\Comment;
|
||||||
use App\Entity\Post;
|
use App\Entity\Post;
|
||||||
use App\Entity\Tree;
|
use App\Entity\Tree;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -10,6 +11,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
|
||||||
|
|
||||||
class PageController extends AbstractController
|
class PageController extends AbstractController
|
||||||
|
|
@ -20,6 +22,16 @@ class PageController extends AbstractController
|
||||||
$item = $entityManagerInterface->getRepository(Post::class)->findByUrlFilterCat($url, 'plain');
|
$item = $entityManagerInterface->getRepository(Post::class)->findByUrlFilterCat($url, 'plain');
|
||||||
if (!$item)
|
if (!$item)
|
||||||
throw $this->createNotFoundException();
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
// افزایش آمار بازدید
|
||||||
|
if (!$item->getViews())
|
||||||
|
$item->setViews(1);
|
||||||
|
else
|
||||||
|
$item->setViews($item->getViews() + 1);
|
||||||
|
|
||||||
|
$entityManagerInterface->persist($item);
|
||||||
|
$entityManagerInterface->flush();
|
||||||
|
|
||||||
return $this->render('post/page.html.twig', [
|
return $this->render('post/page.html.twig', [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
]);
|
]);
|
||||||
|
|
@ -76,15 +88,24 @@ class PageController extends AbstractController
|
||||||
$item = $entityManagerInterface->getRepository(Post::class)->findByUrlFilterCat($url, 'blog');
|
$item = $entityManagerInterface->getRepository(Post::class)->findByUrlFilterCat($url, 'blog');
|
||||||
if (!$item)
|
if (!$item)
|
||||||
throw $this->createNotFoundException();
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
// افزایش آمار بازدید
|
||||||
if (!$item->getViews())
|
if (!$item->getViews())
|
||||||
$item->setViews(1);
|
$item->setViews(1);
|
||||||
else
|
else
|
||||||
$item->setViews($item->getViews() + 1);
|
$item->setViews($item->getViews() + 1);
|
||||||
|
|
||||||
$entityManagerInterface->persist($item);
|
$entityManagerInterface->persist($item);
|
||||||
|
$entityManagerInterface->flush(); // ذخیره تغییرات در دیتابیس
|
||||||
|
|
||||||
|
// دریافت کامنتهای تایید شده
|
||||||
|
$comments = $entityManagerInterface->getRepository(Comment::class)
|
||||||
|
->findBy(['post' => $item, 'publish' => true], ['dateSubmit' => 'DESC']);
|
||||||
|
|
||||||
return $this->render('post/blog_post.html.twig', [
|
return $this->render('post/blog_post.html.twig', [
|
||||||
'item' => $item,
|
'item' => $item,
|
||||||
'posts' => $entityManagerInterface->getRepository(Post::class)->findByCat('blog',3),
|
'posts' => $entityManagerInterface->getRepository(Post::class)->findByCat('blog',3),
|
||||||
|
'comments' => $comments,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +116,15 @@ class PageController extends AbstractController
|
||||||
if (!$item)
|
if (!$item)
|
||||||
throw $this->createNotFoundException();
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
// افزایش آمار بازدید
|
||||||
|
if (!$item->getViews())
|
||||||
|
$item->setViews(1);
|
||||||
|
else
|
||||||
|
$item->setViews($item->getViews() + 1);
|
||||||
|
|
||||||
|
$entityManagerInterface->persist($item);
|
||||||
|
$entityManagerInterface->flush();
|
||||||
|
|
||||||
//get list of trees
|
//get list of trees
|
||||||
$tress = $entityManagerInterface->getRepository(Tree::class)->findAllByCat('api');
|
$tress = $entityManagerInterface->getRepository(Tree::class)->findAllByCat('api');
|
||||||
return $this->render('post/api_docs.html.twig', [
|
return $this->render('post/api_docs.html.twig', [
|
||||||
|
|
@ -110,6 +140,15 @@ class PageController extends AbstractController
|
||||||
if (!$item)
|
if (!$item)
|
||||||
throw $this->createNotFoundException();
|
throw $this->createNotFoundException();
|
||||||
|
|
||||||
|
// افزایش آمار بازدید
|
||||||
|
if (!$item->getViews())
|
||||||
|
$item->setViews(1);
|
||||||
|
else
|
||||||
|
$item->setViews($item->getViews() + 1);
|
||||||
|
|
||||||
|
$entityManagerInterface->persist($item);
|
||||||
|
$entityManagerInterface->flush();
|
||||||
|
|
||||||
//get list of trees
|
//get list of trees
|
||||||
$tress = $entityManagerInterface->getRepository(Tree::class)->findAllByCat('guide');
|
$tress = $entityManagerInterface->getRepository(Tree::class)->findAllByCat('guide');
|
||||||
return $this->render('post/guide.html.twig', [
|
return $this->render('post/guide.html.twig', [
|
||||||
|
|
@ -118,6 +157,39 @@ class PageController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/blog/post/{url}/comment', name: 'app_blog_post_comment', methods: ['POST'])]
|
||||||
|
public function app_blog_post_comment(EntityManagerInterface $entityManagerInterface, Request $request, string $url): Response
|
||||||
|
{
|
||||||
|
$item = $entityManagerInterface->getRepository(Post::class)->findByUrlFilterCat($url, 'blog');
|
||||||
|
if (!$item) {
|
||||||
|
throw $this->createNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// در Symfony 6، get فقط مقدارهای اسکالر میپذیرد؛ برای آرایه باید از all استفاده شود
|
||||||
|
$commentData = $request->request->all('comment');
|
||||||
|
|
||||||
|
if (!$commentData || !isset($commentData['name']) || !isset($commentData['body'])) {
|
||||||
|
$this->addFlash('error', 'نام و متن کامنت الزامی است.');
|
||||||
|
return $this->redirectToRoute('app_blog_post', ['url' => $url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment = new Comment();
|
||||||
|
$comment->setPost($item);
|
||||||
|
$comment->setName(trim($commentData['name']));
|
||||||
|
$comment->setBody(trim($commentData['body']));
|
||||||
|
$comment->setEmail(trim($commentData['email'] ?? ''));
|
||||||
|
$comment->setWebsite(trim($commentData['website'] ?? ''));
|
||||||
|
$comment->setDateSubmit(date('Y-m-d H:i:s'));
|
||||||
|
$comment->setPublish(false); // نیاز به تایید مدیر
|
||||||
|
|
||||||
|
$entityManagerInterface->persist($comment);
|
||||||
|
$entityManagerInterface->flush();
|
||||||
|
|
||||||
|
$this->addFlash('success', 'نظر شما با موفقیت ارسال شد و پس از تایید مدیر نمایش داده خواهد شد.');
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_blog_post', ['url' => $url]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/changes', name: 'app_changes')]
|
#[Route('/changes', name: 'app_changes')]
|
||||||
public function app_changes(EntityManagerInterface $entityManagerInterface): Response
|
public function app_changes(EntityManagerInterface $entityManagerInterface): Response
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ use App\Repository\QuestionRepository;
|
||||||
use App\Repository\QuestionTagRepository;
|
use App\Repository\QuestionTagRepository;
|
||||||
use App\Repository\QuestionVoteRepository;
|
use App\Repository\QuestionVoteRepository;
|
||||||
use App\Repository\AnswerVoteRepository;
|
use App\Repository\AnswerVoteRepository;
|
||||||
|
use App\Service\AttachmentService;
|
||||||
|
use App\Service\EmailNotificationService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
@ -32,7 +34,9 @@ class QAController extends AbstractController
|
||||||
private AnswerRepository $answerRepository,
|
private AnswerRepository $answerRepository,
|
||||||
private QuestionTagRepository $tagRepository,
|
private QuestionTagRepository $tagRepository,
|
||||||
private QuestionVoteRepository $questionVoteRepository,
|
private QuestionVoteRepository $questionVoteRepository,
|
||||||
private AnswerVoteRepository $answerVoteRepository
|
private AnswerVoteRepository $answerVoteRepository,
|
||||||
|
private AttachmentService $attachmentService,
|
||||||
|
private EmailNotificationService $emailNotificationService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route('', name: 'index', methods: ['GET'])]
|
#[Route('', name: 'index', methods: ['GET'])]
|
||||||
|
|
@ -148,6 +152,15 @@ class QAController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// مدیریت پیوست فایلها
|
||||||
|
$attachments = $form->get('attachments')->getData();
|
||||||
|
if ($attachments) {
|
||||||
|
$uploadedAttachments = $this->attachmentService->uploadAttachments($attachments, $this->getUser(), $question);
|
||||||
|
foreach ($uploadedAttachments as $attachment) {
|
||||||
|
$question->addAttachment($attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($question);
|
$this->entityManager->persist($question);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
|
@ -181,9 +194,21 @@ class QAController extends AbstractController
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
// مدیریت پیوست فایلها
|
||||||
|
$attachments = $form->get('attachments')->getData();
|
||||||
|
if ($attachments) {
|
||||||
|
$uploadedAttachments = $this->attachmentService->uploadAttachments($attachments, $this->getUser(), null, $answer);
|
||||||
|
foreach ($uploadedAttachments as $attachment) {
|
||||||
|
$answer->addAttachment($attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($answer);
|
$this->entityManager->persist($answer);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// ارسال ایمیل اطلاعرسانی
|
||||||
|
$this->emailNotificationService->sendAnswerNotification($answer);
|
||||||
|
|
||||||
$this->addFlash('success', 'پاسخ شما با موفقیت ثبت شد.');
|
$this->addFlash('success', 'پاسخ شما با موفقیت ثبت شد.');
|
||||||
return $this->redirectToRoute('qa_question_show', ['id' => $question->getId()]);
|
return $this->redirectToRoute('qa_question_show', ['id' => $question->getId()]);
|
||||||
}
|
}
|
||||||
|
|
@ -343,6 +368,11 @@ class QAController extends AbstractController
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
// ارسال ایمیل اطلاعرسانی در صورت پذیرش پاسخ
|
||||||
|
if ($answer->isAccepted()) {
|
||||||
|
$this->emailNotificationService->sendAnswerAcceptedNotification($answer);
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'accepted' => $answer->isAccepted(),
|
'accepted' => $answer->isAccepted(),
|
||||||
'solved' => $question->isSolved()
|
'solved' => $question->isSolved()
|
||||||
|
|
|
||||||
|
|
@ -8,30 +8,25 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
|
||||||
class SecurityController extends AbstractController
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/hs/login', name: 'login')]
|
#[Route('/admin/login', name: 'login')]
|
||||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
$error = $authenticationUtils->getLastAuthenticationError();
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
$lastUsername = $authenticationUtils->getLastUsername();
|
$lastUsername = $authenticationUtils->getLastUsername();
|
||||||
|
|
||||||
return $this->render('/admin/login.html.twig', [
|
return $this->render('admin/login.html.twig', [
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
'last_username' => $lastUsername,
|
'last_username' => $lastUsername,
|
||||||
'translation_domain' => 'admin',
|
|
||||||
'page_title' => 'ورود',
|
|
||||||
'csrf_token_intention' => 'authenticate',
|
|
||||||
'target_path' => $this->generateUrl('admin', ['_locale' => 'fa']),
|
|
||||||
'username_label' => 'پست الکترونیکی',
|
|
||||||
'password_label' => 'کلمه عبور',
|
|
||||||
'sign_in_label' => 'ورود',
|
|
||||||
'forgot_password_enabled' => false,
|
|
||||||
'remember_me_enabled' => true,
|
|
||||||
'remember_me_checked' => true,
|
|
||||||
'remember_me_label' => 'مرا به یاد داشته باش',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/logout', name: 'logout')]
|
#[Route('/admin/login_check', name: 'login_check')]
|
||||||
|
public function loginCheck(): void
|
||||||
|
{
|
||||||
|
// This method can be blank - it will be intercepted by the logout key on your firewall.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/admin/logout', name: 'logout')]
|
||||||
public function logout(AuthenticationUtils $authenticationUtils): Response
|
public function logout(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
return $this->redirectToRoute('app_home');
|
return $this->redirectToRoute('app_home');
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,16 @@ class Answer
|
||||||
#[ORM\OneToMany(targetEntity: AnswerVote::class, mappedBy: 'answer', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: AnswerVote::class, mappedBy: 'answer', orphanRemoval: true)]
|
||||||
private Collection $answerVotes;
|
private Collection $answerVotes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Attachment>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Attachment::class, mappedBy: 'answer', orphanRemoval: true, cascade: ['persist'])]
|
||||||
|
private Collection $attachments;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->answerVotes = new ArrayCollection();
|
$this->answerVotes = new ArrayCollection();
|
||||||
|
$this->attachments = new ArrayCollection();
|
||||||
$this->createdAt = new \DateTime();
|
$this->createdAt = new \DateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,4 +184,36 @@ class Answer
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Attachment>
|
||||||
|
*/
|
||||||
|
public function getAttachments(): Collection
|
||||||
|
{
|
||||||
|
return $this->attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAttachment(Attachment $attachment): static
|
||||||
|
{
|
||||||
|
if (!$this->attachments->contains($attachment)) {
|
||||||
|
$this->attachments->add($attachment);
|
||||||
|
$attachment->setAnswer($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAttachment(Attachment $attachment): static
|
||||||
|
{
|
||||||
|
if ($this->attachments->removeElement($attachment)) {
|
||||||
|
if ($attachment->getAnswer() === $this) {
|
||||||
|
$attachment->setAnswer(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttachmentsCount(): int
|
||||||
|
{
|
||||||
|
return $this->attachments->count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
195
src/Entity/Attachment.php
Normal file
195
src/Entity/Attachment.php
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\AttachmentRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: AttachmentRepository::class)]
|
||||||
|
#[ORM\Table(name: 'attachment')]
|
||||||
|
class Attachment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $filename = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $originalFilename = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 100)]
|
||||||
|
private ?string $mimeType = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::INTEGER)]
|
||||||
|
private ?int $size = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 500)]
|
||||||
|
private ?string $path = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime')]
|
||||||
|
private ?\DateTimeInterface $uploadedAt = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Question::class, inversedBy: 'attachments')]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Question $question = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Answer::class, inversedBy: 'attachments')]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Answer $answer = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?User $uploadedBy = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->uploadedAt = new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilename(): ?string
|
||||||
|
{
|
||||||
|
return $this->filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFilename(string $filename): static
|
||||||
|
{
|
||||||
|
$this->filename = $filename;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginalFilename(): ?string
|
||||||
|
{
|
||||||
|
return $this->originalFilename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOriginalFilename(string $originalFilename): static
|
||||||
|
{
|
||||||
|
$this->originalFilename = $originalFilename;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMimeType(): ?string
|
||||||
|
{
|
||||||
|
return $this->mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMimeType(string $mimeType): static
|
||||||
|
{
|
||||||
|
$this->mimeType = $mimeType;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSize(): ?int
|
||||||
|
{
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSize(int $size): static
|
||||||
|
{
|
||||||
|
$this->size = $size;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPath(): ?string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPath(string $path): static
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUploadedAt(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->uploadedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUploadedAt(\DateTimeInterface $uploadedAt): static
|
||||||
|
{
|
||||||
|
$this->uploadedAt = $uploadedAt;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQuestion(): ?Question
|
||||||
|
{
|
||||||
|
return $this->question;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setQuestion(?Question $question): static
|
||||||
|
{
|
||||||
|
$this->question = $question;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAnswer(): ?Answer
|
||||||
|
{
|
||||||
|
return $this->answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAnswer(?Answer $answer): static
|
||||||
|
{
|
||||||
|
$this->answer = $answer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUploadedBy(): ?User
|
||||||
|
{
|
||||||
|
return $this->uploadedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUploadedBy(?User $uploadedBy): static
|
||||||
|
{
|
||||||
|
$this->uploadedBy = $uploadedBy;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormattedSize(): string
|
||||||
|
{
|
||||||
|
$bytes = $this->size;
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
|
||||||
|
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isImage(): bool
|
||||||
|
{
|
||||||
|
return str_starts_with($this->mimeType ?? '', 'image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPdf(): bool
|
||||||
|
{
|
||||||
|
return $this->mimeType === 'application/pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDocument(): bool
|
||||||
|
{
|
||||||
|
$documentTypes = [
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
];
|
||||||
|
|
||||||
|
return in_array($this->mimeType, $documentTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
||||||
use App\Repository\CommentRepository;
|
use App\Repository\CommentRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: CommentRepository::class)]
|
#[ORM\Entity(repositoryClass: CommentRepository::class)]
|
||||||
class Comment
|
class Comment
|
||||||
|
|
@ -19,22 +20,33 @@ class Comment
|
||||||
private ?Post $post = null;
|
private ?Post $post = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT)]
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
|
#[Assert\NotBlank(message: 'متن کامنت الزامی است')]
|
||||||
|
#[Assert\Length(min: 10, minMessage: 'متن کامنت باید حداقل 10 کاراکتر باشد')]
|
||||||
private ?string $body = null;
|
private ?string $body = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Assert\NotBlank(message: 'نام الزامی است')]
|
||||||
|
#[Assert\Length(min: 2, max: 255, minMessage: 'نام باید حداقل 2 کاراکتر باشد', maxMessage: 'نام نمیتواند بیش از 255 کاراکتر باشد')]
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
#[Assert\Email(message: 'ایمیل معتبر نیست')]
|
||||||
private ?string $email = null;
|
private ?string $email = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
#[Assert\Url(message: 'آدرس وبسایت معتبر نیست')]
|
||||||
private ?string $website = null;
|
private ?string $website = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(type: 'datetime')]
|
||||||
private ?string $dateSubmit = null;
|
private ?\DateTimeInterface $dateSubmit = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column]
|
||||||
private ?bool $publish = null;
|
private bool $publish = false;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
|
|
@ -101,24 +113,24 @@ class Comment
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDateSubmit(): ?string
|
public function getDateSubmit(): ?\DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->dateSubmit;
|
return $this->dateSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDateSubmit(string $dateSubmit): static
|
public function setDateSubmit(?\DateTimeInterface $dateSubmit): static
|
||||||
{
|
{
|
||||||
$this->dateSubmit = $dateSubmit;
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPublish(): ?bool
|
public function isPublish(): bool
|
||||||
{
|
{
|
||||||
return $this->publish;
|
return $this->publish;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPublish(?bool $publish): static
|
public function setPublish(bool $publish): static
|
||||||
{
|
{
|
||||||
$this->publish = $publish;
|
$this->publish = $publish;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ class Post
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $body = null;
|
private ?string $body = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 50)]
|
#[ORM\Column(type: 'datetime')]
|
||||||
private ?string $dateSubmit = null;
|
private ?\DateTimeInterface $dateSubmit = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $publish = null;
|
private ?bool $publish = null;
|
||||||
|
|
@ -82,6 +82,7 @@ class Post
|
||||||
{
|
{
|
||||||
$this->tree = new ArrayCollection();
|
$this->tree = new ArrayCollection();
|
||||||
$this->comments = new ArrayCollection();
|
$this->comments = new ArrayCollection();
|
||||||
|
$this->dateSubmit = new \DateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|
@ -122,12 +123,12 @@ class Post
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDateSubmit(): ?string
|
public function getDateSubmit(): ?\DateTimeInterface
|
||||||
{
|
{
|
||||||
return $this->dateSubmit;
|
return $this->dateSubmit;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDateSubmit(string $dateSubmit): static
|
public function setDateSubmit(?\DateTimeInterface $dateSubmit): static
|
||||||
{
|
{
|
||||||
$this->dateSubmit = $dateSubmit;
|
$this->dateSubmit = $dateSubmit;
|
||||||
return $this;
|
return $this;
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,21 @@ class Question
|
||||||
#[ORM\Column(type: 'boolean')]
|
#[ORM\Column(type: 'boolean')]
|
||||||
private bool $isActive = true;
|
private bool $isActive = true;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'boolean')]
|
||||||
|
private bool $notifyOnAnswer = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, Answer>
|
* @var Collection<int, Answer>
|
||||||
*/
|
*/
|
||||||
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', orphanRemoval: true, cascade: ['persist'])]
|
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', orphanRemoval: true, cascade: ['persist'])]
|
||||||
private Collection $answers;
|
private Collection $answers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Attachment>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Attachment::class, mappedBy: 'question', orphanRemoval: true, cascade: ['persist'])]
|
||||||
|
private Collection $attachments;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<int, QuestionVote>
|
* @var Collection<int, QuestionVote>
|
||||||
*/
|
*/
|
||||||
|
|
@ -73,6 +82,7 @@ class Question
|
||||||
$this->answers = new ArrayCollection();
|
$this->answers = new ArrayCollection();
|
||||||
$this->questionVotes = new ArrayCollection();
|
$this->questionVotes = new ArrayCollection();
|
||||||
$this->tagRelations = new ArrayCollection();
|
$this->tagRelations = new ArrayCollection();
|
||||||
|
$this->attachments = new ArrayCollection();
|
||||||
$this->createdAt = new \DateTime();
|
$this->createdAt = new \DateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,4 +291,47 @@ class Question
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isNotifyOnAnswer(): bool
|
||||||
|
{
|
||||||
|
return $this->notifyOnAnswer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotifyOnAnswer(bool $notifyOnAnswer): static
|
||||||
|
{
|
||||||
|
$this->notifyOnAnswer = $notifyOnAnswer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Attachment>
|
||||||
|
*/
|
||||||
|
public function getAttachments(): Collection
|
||||||
|
{
|
||||||
|
return $this->attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAttachment(Attachment $attachment): static
|
||||||
|
{
|
||||||
|
if (!$this->attachments->contains($attachment)) {
|
||||||
|
$this->attachments->add($attachment);
|
||||||
|
$attachment->setQuestion($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeAttachment(Attachment $attachment): static
|
||||||
|
{
|
||||||
|
if ($this->attachments->removeElement($attachment)) {
|
||||||
|
if ($attachment->getQuestion() === $this) {
|
||||||
|
$attachment->setQuestion(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttachmentsCount(): int
|
||||||
|
{
|
||||||
|
return $this->attachments->count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Form\QA;
|
||||||
|
|
||||||
use App\Entity\Answer;
|
use App\Entity\Answer;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
@ -28,6 +29,43 @@ class AnswerFormType extends AbstractType
|
||||||
'minMessage' => 'پاسخ باید حداقل 10 کاراکتر باشد'
|
'minMessage' => 'پاسخ باید حداقل 10 کاراکتر باشد'
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
])
|
||||||
|
->add('attachments', FileType::class, [
|
||||||
|
'label' => 'پیوست فایل (حداکثر 3 فایل، هر کدام 4 مگابایت)',
|
||||||
|
'mapped' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'accept' => '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt',
|
||||||
|
'multiple' => true
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new Assert\Count([
|
||||||
|
'max' => 3,
|
||||||
|
'maxMessage' => 'حداکثر 3 فایل میتوانید پیوست کنید'
|
||||||
|
]),
|
||||||
|
new Assert\All([
|
||||||
|
new Assert\File([
|
||||||
|
'maxSize' => '4M',
|
||||||
|
'maxSizeMessage' => 'حجم هر فایل نمیتواند بیش از 4 مگابایت باشد',
|
||||||
|
'mimeTypes' => [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
],
|
||||||
|
'mimeTypesMessage' => 'فرمت فایل مجاز نیست. فرمتهای مجاز: JPG, PNG, GIF, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT'
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ use App\Entity\Question;
|
||||||
use App\Entity\QuestionTag;
|
use App\Entity\QuestionTag;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
@ -48,6 +50,70 @@ class QuestionFormType extends AbstractType
|
||||||
'minMessage' => 'متن سوال باید حداقل 20 کاراکتر باشد'
|
'minMessage' => 'متن سوال باید حداقل 20 کاراکتر باشد'
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
])
|
||||||
|
->add('notifyOnAnswer', CheckboxType::class, [
|
||||||
|
'label' => 'اطلاعرسانی ایمیل هنگام دریافت پاسخ',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-check-input'
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->add('tags', EntityType::class, [
|
||||||
|
'class' => QuestionTag::class,
|
||||||
|
'choice_label' => 'name',
|
||||||
|
'multiple' => true,
|
||||||
|
'expanded' => false,
|
||||||
|
'required' => false,
|
||||||
|
'mapped' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'style' => 'display: none;' // مخفی کردن چون با JavaScript مدیریت میشود
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new Assert\Count([
|
||||||
|
'min' => 1,
|
||||||
|
'max' => 5,
|
||||||
|
'minMessage' => 'حداقل 1 تگ باید انتخاب کنید',
|
||||||
|
'maxMessage' => 'حداکثر 5 تگ میتوانید انتخاب کنید'
|
||||||
|
])
|
||||||
|
]
|
||||||
|
])
|
||||||
|
->add('attachments', FileType::class, [
|
||||||
|
'label' => 'پیوست فایل (حداکثر 3 فایل، هر کدام 4 مگابایت)',
|
||||||
|
'mapped' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'class' => 'form-control',
|
||||||
|
'accept' => '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt',
|
||||||
|
'multiple' => true
|
||||||
|
],
|
||||||
|
'constraints' => [
|
||||||
|
new Assert\Count([
|
||||||
|
'max' => 3,
|
||||||
|
'maxMessage' => 'حداکثر 3 فایل میتوانید پیوست کنید'
|
||||||
|
]),
|
||||||
|
new Assert\All([
|
||||||
|
new Assert\File([
|
||||||
|
'maxSize' => '4M',
|
||||||
|
'maxSizeMessage' => 'حجم هر فایل نمیتواند بیش از 4 مگابایت باشد',
|
||||||
|
'mimeTypes' => [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
],
|
||||||
|
'mimeTypesMessage' => 'فرمت فایل مجاز نیست. فرمتهای مجاز: JPG, PNG, GIF, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT'
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
69
src/Repository/AttachmentRepository.php
Normal file
69
src/Repository/AttachmentRepository.php
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Attachment;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<Attachment>
|
||||||
|
*/
|
||||||
|
class AttachmentRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, Attachment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find attachments by question
|
||||||
|
*/
|
||||||
|
public function findByQuestion(int $questionId): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('a')
|
||||||
|
->where('a.question = :questionId')
|
||||||
|
->setParameter('questionId', $questionId)
|
||||||
|
->orderBy('a.uploadedAt', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find attachments by answer
|
||||||
|
*/
|
||||||
|
public function findByAnswer(int $answerId): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('a')
|
||||||
|
->where('a.answer = :answerId')
|
||||||
|
->setParameter('answerId', $answerId)
|
||||||
|
->orderBy('a.uploadedAt', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find orphaned attachments (not linked to any question or answer)
|
||||||
|
*/
|
||||||
|
public function findOrphanedAttachments(): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('a')
|
||||||
|
->where('a.question IS NULL')
|
||||||
|
->andWhere('a.answer IS NULL')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count attachments by user
|
||||||
|
*/
|
||||||
|
public function countByUser(int $userId): int
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('a')
|
||||||
|
->select('COUNT(a.id)')
|
||||||
|
->where('a.uploadedBy = :userId')
|
||||||
|
->setParameter('userId', $userId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/Service/AttachmentService.php
Normal file
149
src/Service/AttachmentService.php
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Attachment;
|
||||||
|
use App\Entity\Answer;
|
||||||
|
use App\Entity\Question;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
|
|
||||||
|
class AttachmentService
|
||||||
|
{
|
||||||
|
private string $targetDirectory;
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
private SluggerInterface $slugger;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $targetDirectory,
|
||||||
|
EntityManagerInterface $entityManager,
|
||||||
|
SluggerInterface $slugger
|
||||||
|
) {
|
||||||
|
$this->targetDirectory = $targetDirectory;
|
||||||
|
$this->entityManager = $entityManager;
|
||||||
|
$this->slugger = $slugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload multiple files and create attachment entities
|
||||||
|
*/
|
||||||
|
public function uploadAttachments(array $uploadedFiles, User $user, ?Question $question = null, ?Answer $answer = null): array
|
||||||
|
{
|
||||||
|
$attachments = [];
|
||||||
|
|
||||||
|
foreach ($uploadedFiles as $uploadedFile) {
|
||||||
|
if ($uploadedFile instanceof UploadedFile) {
|
||||||
|
$attachment = $this->uploadSingleFile($uploadedFile, $user, $question, $answer);
|
||||||
|
if ($attachment) {
|
||||||
|
$attachments[] = $attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a single file and create attachment entity
|
||||||
|
*/
|
||||||
|
public function uploadSingleFile(UploadedFile $file, User $user, ?Question $question = null, ?Answer $answer = null): ?Attachment
|
||||||
|
{
|
||||||
|
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
||||||
|
$safeFilename = $this->slugger->slug($originalFilename);
|
||||||
|
$fileName = $safeFilename . '-' . uniqid() . '.' . $file->guessExtension();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$file->move($this->targetDirectory, $fileName);
|
||||||
|
} catch (FileException $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = new Attachment();
|
||||||
|
$attachment->setFilename($fileName);
|
||||||
|
$attachment->setOriginalFilename($file->getClientOriginalName());
|
||||||
|
$attachment->setMimeType($file->getMimeType());
|
||||||
|
$attachment->setSize($file->getSize());
|
||||||
|
$attachment->setPath($this->targetDirectory . '/' . $fileName);
|
||||||
|
$attachment->setUploadedBy($user);
|
||||||
|
|
||||||
|
if ($question) {
|
||||||
|
$attachment->setQuestion($question);
|
||||||
|
}
|
||||||
|
if ($answer) {
|
||||||
|
$attachment->setAnswer($answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->persist($attachment);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete attachment file and entity
|
||||||
|
*/
|
||||||
|
public function deleteAttachment(Attachment $attachment): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Delete file from filesystem
|
||||||
|
if (file_exists($attachment->getPath())) {
|
||||||
|
unlink($attachment->getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from database
|
||||||
|
$this->entityManager->remove($attachment);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attachment URL for display
|
||||||
|
*/
|
||||||
|
public function getAttachmentUrl(Attachment $attachment): string
|
||||||
|
{
|
||||||
|
return '/uploads/attachments/' . $attachment->getFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get allowed file types
|
||||||
|
*/
|
||||||
|
public function getAllowedMimeTypes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max file size in bytes
|
||||||
|
*/
|
||||||
|
public function getMaxFileSize(): int
|
||||||
|
{
|
||||||
|
return 4 * 1024 * 1024; // 4MB
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max number of files
|
||||||
|
*/
|
||||||
|
public function getMaxFiles(): int
|
||||||
|
{
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/Service/EmailNotificationService.php
Normal file
95
src/Service/EmailNotificationService.php
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Answer;
|
||||||
|
use App\Entity\Question;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Mime\Email;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class EmailNotificationService
|
||||||
|
{
|
||||||
|
private MailerInterface $mailer;
|
||||||
|
private Environment $twig;
|
||||||
|
|
||||||
|
public function __construct(MailerInterface $mailer, Environment $twig)
|
||||||
|
{
|
||||||
|
$this->mailer = $mailer;
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email notification when a new answer is posted
|
||||||
|
*/
|
||||||
|
public function sendAnswerNotification(Answer $answer): bool
|
||||||
|
{
|
||||||
|
$question = $answer->getQuestion();
|
||||||
|
|
||||||
|
// Only send notification if question author wants to be notified
|
||||||
|
if (!$question->isNotifyOnAnswer()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$questionAuthor = $question->getAuthor();
|
||||||
|
$answerAuthor = $answer->getAuthor();
|
||||||
|
|
||||||
|
// Don't send notification if the answer is from the same person who asked the question
|
||||||
|
if ($questionAuthor->getId() === $answerAuthor->getId()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$email = (new Email())
|
||||||
|
->from('noreply@hesabix.ir')
|
||||||
|
->to($questionAuthor->getEmail())
|
||||||
|
->subject('پاسخ جدید برای سوال شما: ' . $question->getTitle())
|
||||||
|
->html($this->twig->render('emails/answer_notification.html.twig', [
|
||||||
|
'question' => $question,
|
||||||
|
'answer' => $answer,
|
||||||
|
'questionAuthor' => $questionAuthor,
|
||||||
|
'answerAuthor' => $answerAuthor
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error if needed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send email notification when a question is accepted
|
||||||
|
*/
|
||||||
|
public function sendAnswerAcceptedNotification(Answer $answer): bool
|
||||||
|
{
|
||||||
|
$question = $answer->getQuestion();
|
||||||
|
$answerAuthor = $answer->getAuthor();
|
||||||
|
$questionAuthor = $question->getAuthor();
|
||||||
|
|
||||||
|
// Don't send notification if the answer is from the same person who asked the question
|
||||||
|
if ($questionAuthor->getId() === $answerAuthor->getId()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$email = (new Email())
|
||||||
|
->from('noreply@hesabix.ir')
|
||||||
|
->to($answerAuthor->getEmail())
|
||||||
|
->subject('پاسخ شما پذیرفته شد: ' . $question->getTitle())
|
||||||
|
->html($this->twig->render('emails/answer_accepted_notification.html.twig', [
|
||||||
|
'question' => $question,
|
||||||
|
'answer' => $answer,
|
||||||
|
'questionAuthor' => $questionAuthor,
|
||||||
|
'answerAuthor' => $answerAuthor
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log error if needed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Twig/JdateExtension.php
Normal file
37
src/Twig/JdateExtension.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Twig;
|
||||||
|
|
||||||
|
use App\Service\Jdate;
|
||||||
|
use Twig\Extension\AbstractExtension;
|
||||||
|
use Twig\TwigFilter;
|
||||||
|
|
||||||
|
class JdateExtension extends AbstractExtension
|
||||||
|
{
|
||||||
|
private Jdate $jdate;
|
||||||
|
|
||||||
|
public function __construct(Jdate $jdate)
|
||||||
|
{
|
||||||
|
$this->jdate = $jdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilters(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new TwigFilter('jdate', [$this, 'formatJdate']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function formatJdate($timestamp, string $format = 'Y/m/d H:i'): string
|
||||||
|
{
|
||||||
|
if (is_string($timestamp)) {
|
||||||
|
$timestamp = (int) $timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$timestamp) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->jdate->jdate($format, $timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,68 @@
|
||||||
{# templates/easy_admin/page/login.html.twig #}
|
{% extends 'customer/base.html.twig' %}
|
||||||
{% extends '@!EasyAdmin/page/login.html.twig' %}
|
|
||||||
|
|
||||||
{% block head %}
|
{% block page_title %}ورود به پنل مدیریت{% endblock %}
|
||||||
{{ parent() }}
|
{% block page_subtitle %}وارد پنل مدیریت حسابیکس شوید{% endblock %}
|
||||||
<link rel="stylesheet" href="/css/login.css">
|
|
||||||
|
{% block auth_content %}
|
||||||
|
{% if error %}
|
||||||
|
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-right" role="alert">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation ml-2">
|
||||||
|
{{ error.messageKey|trans(error.messageData, 'security', 'fa') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{{ path('login_check') }}" class="space-y-6">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="inputEmail" class="block text-sm font-medium text-gray-700 mb-2">پست الکترونیکی</label>
|
||||||
|
<input type="email"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left"
|
||||||
|
id="inputEmail"
|
||||||
|
name="_username"
|
||||||
|
value="{{ last_username }}"
|
||||||
|
placeholder="ایمیل خود را وارد کنید"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
dir="ltr">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="inputPassword" class="block text-sm font-medium text-gray-700 mb-2">کلمه عبور</label>
|
||||||
|
<input type="password"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left"
|
||||||
|
id="inputPassword"
|
||||||
|
name="_password"
|
||||||
|
placeholder="کلمه عبور خود را وارد کنید"
|
||||||
|
required
|
||||||
|
dir="ltr">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 ml-3"
|
||||||
|
type="checkbox"
|
||||||
|
id="remember_me"
|
||||||
|
name="_remember_me"
|
||||||
|
checked>
|
||||||
|
<label for="remember_me" class="text-sm text-gray-700">
|
||||||
|
مرا به یاد داشته باش
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
|
||||||
|
<button class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center gap-2" type="submit">
|
||||||
|
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in">
|
||||||
|
ورود به پنل مدیریت
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-8 text-center space-y-4">
|
||||||
|
<a href="{{ path('app_home') }}" class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
|
<img src="{{ asset('/img/icons/home.svg') }}" alt="خانه" class="icon-svg icon-home ml-2">
|
||||||
|
بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -398,6 +398,18 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm">داشبورد</span>
|
<span class="text-sm">داشبورد</span>
|
||||||
</a>
|
</a>
|
||||||
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
|
<a href="{{ path('admin') }}"
|
||||||
|
class="flex items-center space-x-3 space-x-reverse px-4 py-3 text-gray-700 hover:bg-green-50 hover:text-green-600 transition-colors duration-200">
|
||||||
|
<div class="w-7 h-7 bg-green-100 rounded-lg flex items-center justify-center">
|
||||||
|
<svg class="w-3.5 h-3.5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm">پنل مدیریت</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<div class="px-4 py-2">
|
<div class="px-4 py-2">
|
||||||
<a href="{{ path('customer_logout') }}"
|
<a href="{{ path('customer_logout') }}"
|
||||||
class="flex items-center space-x-3 space-x-reverse px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors duration-200">
|
class="flex items-center space-x-3 space-x-reverse px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors duration-200">
|
||||||
|
|
@ -470,6 +482,27 @@
|
||||||
class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 rounded-lg transition-colors duration-200">
|
class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 rounded-lg transition-colors duration-200">
|
||||||
تماس با ما
|
تماس با ما
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
{% if app.user and app.user.roles is defined and 'ROLE_CUSTOMER' in app.user.roles %}
|
||||||
|
<div class="border-t border-gray-200 pt-4 mt-4">
|
||||||
|
<div class="px-4 py-2 text-sm font-medium text-gray-900">{{ app.user.name }}</div>
|
||||||
|
<div class="px-4 py-1 text-xs text-gray-500 mb-2">عضو باشگاه مشتریان</div>
|
||||||
|
<a href="{{ path('customer_dashboard') }}"
|
||||||
|
class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-600 rounded-lg transition-colors duration-200">
|
||||||
|
داشبورد
|
||||||
|
</a>
|
||||||
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
|
<a href="{{ path('admin') }}"
|
||||||
|
class="block px-4 py-3 text-gray-700 hover:bg-green-50 hover:text-green-600 rounded-lg transition-colors duration-200">
|
||||||
|
پنل مدیریت
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ path('customer_logout') }}"
|
||||||
|
class="block px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors duration-200">
|
||||||
|
خروج
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,194 +5,6 @@
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
.customer-auth-container {
|
|
||||||
min-height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
padding: 20px 0;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 450px;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-header {
|
|
||||||
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: bold;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-header p {
|
|
||||||
margin: 10px 0 0 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customer-auth-body {
|
|
||||||
padding: 40px 30px;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-floating {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-floating label {
|
|
||||||
color: #6c757d;
|
|
||||||
right: 0.75rem;
|
|
||||||
left: auto;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: #0d6efd;
|
|
||||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* فیلدهای انگلیسی (ایمیل و کلمه عبور) */
|
|
||||||
.form-control[type="email"],
|
|
||||||
.form-control[type="password"] {
|
|
||||||
direction: ltr;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control[type="email"] + label,
|
|
||||||
.form-control[type="password"] + label {
|
|
||||||
right: auto;
|
|
||||||
left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(13, 110, 253, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check {
|
|
||||||
margin: 20px 0;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-input:checked {
|
|
||||||
background-color: #0d6efd;
|
|
||||||
border-color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-check-label {
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-links {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 30px;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-links a {
|
|
||||||
color: #0d6efd;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-links a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert {
|
|
||||||
border-radius: 10px;
|
|
||||||
border: none;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background-color: #d1edff;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-danger {
|
|
||||||
background-color: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-info {
|
|
||||||
background-color: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hesabix-logo {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-to-home {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
opacity: 0.9;
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-to-home:hover {
|
|
||||||
color: white;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* بهبود نمایش آیکونهای SVG */
|
/* بهبود نمایش آیکونهای SVG */
|
||||||
.icon-svg {
|
.icon-svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|
@ -238,44 +50,52 @@
|
||||||
.icon-svg-large.icon-heart svg { fill: #e74c3c; }
|
.icon-svg-large.icon-heart svg { fill: #e74c3c; }
|
||||||
.icon-svg-large.icon-key svg { fill: #9b59b6; }
|
.icon-svg-large.icon-key svg { fill: #9b59b6; }
|
||||||
.icon-svg-large.icon-lock svg { fill: #95a5a6; }
|
.icon-svg-large.icon-lock svg { fill: #95a5a6; }
|
||||||
|
|
||||||
/* بهبود نمایش متنهای فارسی */
|
|
||||||
.text-muted {
|
|
||||||
direction: rtl;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="customer-auth-container">
|
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-500 via-purple-600 to-indigo-700 py-20 px-4" dir="rtl">
|
||||||
<div class="customer-auth-card">
|
<div class="w-full max-w-md">
|
||||||
<div class="customer-auth-header">
|
<div class="bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
<img src="{{ asset('/favicon/favicon.svg') }}" alt="حسابیکس" class="hesabix-logo">
|
<!-- هدر -->
|
||||||
<h1>باشگاه مشتریان حسابیکس</h1>
|
<div class="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-8 text-center">
|
||||||
<p>{{ block('page_subtitle') }}</p>
|
<img src="{{ asset('/favicon/favicon.svg') }}" alt="حسابیکس" class="w-16 h-16 mx-auto mb-4">
|
||||||
</div>
|
<h1 class="text-2xl font-bold mb-2">باشگاه مشتریان حسابیکس</h1>
|
||||||
|
<p class="text-blue-100">{{ block('page_subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="customer-auth-body">
|
<!-- محتوا -->
|
||||||
{% for message in app.flashes('success') %}
|
<div class="p-8">
|
||||||
<div class="alert alert-success" role="alert">
|
<!-- پیامهای فلش -->
|
||||||
<img src="{{ asset('/img/icons/check-circle.svg') }}" alt="موفقیت" class="icon-svg icon-check"> {{ message }}
|
{% for message in app.flashes('success') %}
|
||||||
</div>
|
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl text-green-800 text-right" role="alert">
|
||||||
{% endfor %}
|
<div class="flex items-center">
|
||||||
|
<img src="{{ asset('/img/icons/check-circle.svg') }}" alt="موفقیت" class="icon-svg icon-check ml-2">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% for message in app.flashes('error') %}
|
{% for message in app.flashes('error') %}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-right" role="alert">
|
||||||
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation"> {{ message }}
|
<div class="flex items-center">
|
||||||
</div>
|
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation ml-2">
|
||||||
{% endfor %}
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% for message in app.flashes('info') %}
|
{% for message in app.flashes('info') %}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-xl text-blue-800 text-right" role="alert">
|
||||||
<img src="{{ asset('/img/icons/info-circle.svg') }}" alt="اطلاعات" class="icon-svg icon-info"> {{ message }}
|
<div class="flex items-center">
|
||||||
</div>
|
<img src="{{ asset('/img/icons/info-circle.svg') }}" alt="اطلاعات" class="icon-svg icon-info ml-2">
|
||||||
{% endfor %}
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% block auth_content %}{% endblock %}
|
{% block auth_content %}{% endblock %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@
|
||||||
|
|
||||||
<div class="flex justify-between items-center py-4 border-b border-gray-200">
|
<div class="flex justify-between items-center py-4 border-b border-gray-200">
|
||||||
<span class="font-semibold text-gray-700">تاریخ عضویت:</span>
|
<span class="font-semibold text-gray-700">تاریخ عضویت:</span>
|
||||||
<span class="text-gray-600">{{ user.createdAt|date('Y/m/d') }}</span>
|
<span class="text-gray-600">{{ user.createdAt|date('U')|jdate('Y/m/d') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center py-4">
|
<div class="flex justify-between items-center py-4">
|
||||||
<span class="font-semibold text-gray-700">آخرین ورود:</span>
|
<span class="font-semibold text-gray-700">آخرین ورود:</span>
|
||||||
<span class="text-gray-600">
|
<span class="text-gray-600">
|
||||||
{{ user.lastLoginAt ? user.lastLoginAt|date('Y/m/d H:i') : 'هنوز وارد نشده' }}
|
{{ user.lastLoginAt ? user.lastLoginAt|date('U')|jdate('Y/m/d H:i') : 'هنوز وارد نشده' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -97,8 +97,8 @@
|
||||||
<h5 class="text-lg font-semibold mb-4">اتصال کیف پول جدید</h5>
|
<h5 class="text-lg font-semibold mb-4">اتصال کیف پول جدید</h5>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="walletType" class="form-label">نوع کیف پول</label>
|
<label for="walletType" class="block text-sm font-medium text-gray-700 mb-2">نوع کیف پول</label>
|
||||||
<select class="form-control" id="walletType" data-wallet-connect-target="walletType" data-action="change->wallet-connect#onWalletTypeChange">
|
<select class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" id="walletType" data-wallet-connect-target="walletType" data-action="change->wallet-connect#onWalletTypeChange">
|
||||||
<option value="">انتخاب کنید...</option>
|
<option value="">انتخاب کنید...</option>
|
||||||
<option value="metamask">MetaMask</option>
|
<option value="metamask">MetaMask</option>
|
||||||
<option value="trust">Trust Wallet</option>
|
<option value="trust">Trust Wallet</option>
|
||||||
|
|
@ -108,13 +108,13 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="form-label">آدرس کیف پول</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">آدرس کیف پول</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||||||
<i class="fas fa-wallet text-gray-400"></i>
|
<i class="fas fa-wallet text-gray-400"></i>
|
||||||
</div>
|
</div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control pr-10"
|
class="w-full px-4 py-3 pr-10 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
||||||
id="walletAddress"
|
id="walletAddress"
|
||||||
data-wallet-connect-target="walletAddress"
|
data-wallet-connect-target="walletAddress"
|
||||||
placeholder="آدرس کیف پول پس از اتصال نمایش داده میشود"
|
placeholder="آدرس کیف پول پس از اتصال نمایش داده میشود"
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn-primary w-full py-3 text-lg"
|
class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||||
id="connectBtn"
|
id="connectBtn"
|
||||||
data-wallet-connect-target="connectBtn"
|
data-wallet-connect-target="connectBtn"
|
||||||
data-action="click->wallet-connect#connectWallet"
|
data-action="click->wallet-connect#connectWallet"
|
||||||
|
|
@ -169,28 +169,28 @@
|
||||||
{{ wallet.isActive ? 'فعال' : 'غیرفعال' }}
|
{{ wallet.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ wallet.connectedAt|date('Y/m/d H:i') }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ wallet.connectedAt|date('U')|jdate('Y/m/d H:i') }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{% if not wallet.isPrimary and wallet.isActive %}
|
{% if not wallet.isPrimary and wallet.isActive %}
|
||||||
<button class="btn-outline-primary text-xs px-3 py-1"
|
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors duration-200"
|
||||||
data-wallet-id="{{ wallet.id }}"
|
data-wallet-id="{{ wallet.id }}"
|
||||||
data-action="click->wallet-connect#setPrimaryWallet">
|
data-action="click->wallet-connect#setPrimaryWallet">
|
||||||
<i class="fas fa-star"></i> اصلی
|
<i class="fas fa-star ml-1"></i> اصلی
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button class="btn-outline-warning text-xs px-3 py-1"
|
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-yellow-600 bg-yellow-50 border border-yellow-200 rounded-lg hover:bg-yellow-100 transition-colors duration-200"
|
||||||
data-wallet-id="{{ wallet.id }}"
|
data-wallet-id="{{ wallet.id }}"
|
||||||
data-action="click->wallet-connect#toggleWalletStatus">
|
data-action="click->wallet-connect#toggleWalletStatus">
|
||||||
<i class="fas fa-{{ wallet.isActive ? 'pause' : 'play' }}"></i>
|
<i class="fas fa-{{ wallet.isActive ? 'pause' : 'play' }} ml-1"></i>
|
||||||
{{ wallet.isActive ? 'غیرفعال' : 'فعال' }}
|
{{ wallet.isActive ? 'غیرفعال' : 'فعال' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn-outline-danger text-xs px-3 py-1"
|
<button class="inline-flex items-center px-3 py-1 text-xs font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors duration-200"
|
||||||
data-wallet-id="{{ wallet.id }}"
|
data-wallet-id="{{ wallet.id }}"
|
||||||
data-action="click->wallet-connect#deleteWallet">
|
data-action="click->wallet-connect#deleteWallet">
|
||||||
<i class="fas fa-trash"></i> حذف
|
<i class="fas fa-trash ml-1"></i> حذف
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -218,15 +218,21 @@
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 justify-center">
|
<div class="flex flex-wrap gap-4 justify-center">
|
||||||
<a href="{{ path('customer_forgot_password') }}" class="btn-primary flex items-center gap-2">
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
|
<a href="{{ path('admin') }}" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-xl font-medium hover:from-green-700 hover:to-emerald-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl gap-2">
|
||||||
|
<img src="{{ asset('/img/icons/cogs.svg') }}" alt="مدیریت" class="w-4 h-4"> پنل مدیریت
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ path('customer_forgot_password') }}" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl font-medium hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl gap-2">
|
||||||
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="w-4 h-4"> بازیابی کلمه عبور
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="w-4 h-4"> بازیابی کلمه عبور
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{{ path('app_home') }}" class="btn-primary flex items-center gap-2">
|
<a href="{{ path('app_home') }}" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl font-medium hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl gap-2">
|
||||||
<img src="{{ asset('/img/icons/home.svg') }}" alt="خانه" class="w-4 h-4"> بازگشت به صفحه اصلی
|
<img src="{{ asset('/img/icons/home.svg') }}" alt="خانه" class="w-4 h-4"> بازگشت به صفحه اصلی
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{{ path('customer_logout') }}" class="bg-gradient-to-r from-red-500 to-red-600 text-white px-6 py-3 rounded-lg font-medium transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 flex items-center gap-2">
|
<a href="{{ path('customer_logout') }}" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-red-500 to-red-600 text-white rounded-xl font-medium hover:from-red-600 hover:to-red-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl gap-2">
|
||||||
<img src="{{ asset('/img/icons/sign-out.svg') }}" alt="خروج" class="w-4 h-4"> خروج
|
<img src="{{ asset('/img/icons/sign-out.svg') }}" alt="خروج" class="w-4 h-4"> خروج
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,39 @@
|
||||||
{% block page_subtitle %}کلمه عبور خود را بازیابی کنید{% endblock %}
|
{% block page_subtitle %}کلمه عبور خود را بازیابی کنید{% endblock %}
|
||||||
|
|
||||||
{% block auth_content %}
|
{% block auth_content %}
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-8">
|
||||||
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg-large icon-key text-primary">
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg-large icon-key text-blue-600">
|
||||||
<p class="text-muted">ایمیل خود را وارد کنید تا لینک بازیابی کلمه عبور برای شما ارسال شود.</p>
|
<p class="text-gray-600 mt-4">ایمیل خود را وارد کنید تا لینک بازیابی کلمه عبور برای شما ارسال شود.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-6'}}) }}
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div>
|
||||||
{{ form_widget(form.email, {'attr': {'class': 'form-control', 'placeholder': 'ایمیل خود را وارد کنید'}}) }}
|
{{ form_label(form.email, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
{{ form_label(form.email) }}
|
{{ form_widget(form.email, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'ایمیل خود را وارد کنید'}}) }}
|
||||||
{{ form_errors(form.email) }}
|
{{ form_errors(form.email) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center gap-2" type="submit">
|
||||||
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="ارسال" class="icon-svg icon-key">
|
||||||
|
ارسال لینک بازیابی
|
||||||
|
</button>
|
||||||
|
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
|
||||||
<div class="auth-links">
|
<div class="mt-8 text-center space-y-4">
|
||||||
<a href="{{ path('customer_login') }}">
|
<a href="{{ path('customer_login') }}" class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left"> بازگشت به صفحه ورود
|
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left ml-2">
|
||||||
|
بازگشت به صفحه ورود
|
||||||
</a>
|
</a>
|
||||||
<br><br>
|
|
||||||
<p>حساب کاربری ندارید؟
|
<div class="text-gray-600">
|
||||||
<a href="{{ path('customer_register') }}">
|
<p>حساب کاربری ندارید؟
|
||||||
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
<a href="{{ path('customer_register') }}" class="text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
</a>
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus ml-1">
|
||||||
</p>
|
عضویت در باشگاه
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -5,59 +5,73 @@
|
||||||
|
|
||||||
{% block auth_content %}
|
{% block auth_content %}
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-danger" role="alert">
|
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-800 text-right" role="alert">
|
||||||
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation"> {{ error.messageKey|trans(error.messageData, 'security', 'fa') }}
|
<div class="flex items-center">
|
||||||
|
<img src="{{ asset('/img/icons/exclamation-circle.svg') }}" alt="خطا" class="icon-svg icon-exclamation ml-2">
|
||||||
|
{{ error.messageKey|trans(error.messageData, 'security', 'fa') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{{ path('customer_login_check') }}">
|
<form method="post" action="{{ path('customer_login_check') }}" class="space-y-6">
|
||||||
<div class="form-floating mb-3">
|
<div class="space-y-4">
|
||||||
<input type="email"
|
<div>
|
||||||
class="form-control"
|
<label for="inputEmail" class="block text-sm font-medium text-gray-700 mb-2">پست الکترونیکی</label>
|
||||||
id="inputEmail"
|
<input type="email"
|
||||||
name="_username"
|
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left"
|
||||||
value="{{ last_username }}"
|
id="inputEmail"
|
||||||
placeholder="ایمیل خود را وارد کنید"
|
name="_username"
|
||||||
required
|
value="{{ last_username }}"
|
||||||
autofocus
|
placeholder="ایمیل خود را وارد کنید"
|
||||||
dir="ltr">
|
required
|
||||||
<label for="inputEmail">پست الکترونیکی</label>
|
autofocus
|
||||||
|
dir="ltr">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="inputPassword" class="block text-sm font-medium text-gray-700 mb-2">کلمه عبور</label>
|
||||||
|
<input type="password"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left"
|
||||||
|
id="inputPassword"
|
||||||
|
name="_password"
|
||||||
|
placeholder="کلمه عبور خود را وارد کنید"
|
||||||
|
required
|
||||||
|
dir="ltr">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="flex items-center">
|
||||||
<input type="password"
|
<input class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 ml-3"
|
||||||
class="form-control"
|
type="checkbox"
|
||||||
id="inputPassword"
|
id="remember_me"
|
||||||
name="_password"
|
name="_remember_me"
|
||||||
placeholder="کلمه عبور خود را وارد کنید"
|
checked>
|
||||||
required
|
<label for="remember_me" class="text-sm text-gray-700">
|
||||||
dir="ltr">
|
|
||||||
<label for="inputPassword">کلمه عبور</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input class="form-check-input" type="checkbox" id="remember_me" name="_remember_me" checked>
|
|
||||||
<label class="form-check-label" for="remember_me">
|
|
||||||
مرا به یاد داشته باش
|
مرا به یاد داشته باش
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||||
|
|
||||||
<button class="btn btn-primary w-100 mb-3" type="submit">
|
<button class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center gap-2" type="submit">
|
||||||
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in"> ورود
|
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in">
|
||||||
|
ورود
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="auth-links">
|
<div class="mt-8 text-center space-y-4">
|
||||||
<a href="{{ path('customer_forgot_password') }}">
|
<a href="{{ path('customer_forgot_password') }}" class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg icon-key"> فراموشی کلمه عبور
|
<img src="{{ asset('/img/icons/key.svg') }}" alt="کلید" class="icon-svg icon-key ml-2">
|
||||||
|
فراموشی کلمه عبور
|
||||||
</a>
|
</a>
|
||||||
<br><br>
|
|
||||||
<p>حساب کاربری ندارید؟
|
<div class="text-gray-600">
|
||||||
<a href="{{ path('customer_register') }}">
|
<p>حساب کاربری ندارید؟
|
||||||
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
<a href="{{ path('customer_register') }}" class="text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
</a>
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus ml-1">
|
||||||
</p>
|
عضویت در باشگاه
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -4,56 +4,61 @@
|
||||||
{% block page_subtitle %}به باشگاه مشتریان حسابیکس بپیوندید{% endblock %}
|
{% block page_subtitle %}به باشگاه مشتریان حسابیکس بپیوندید{% endblock %}
|
||||||
|
|
||||||
{% block auth_content %}
|
{% block auth_content %}
|
||||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-6'}}) }}
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="space-y-4">
|
||||||
{{ form_widget(form.name, {'attr': {'class': 'form-control', 'placeholder': 'نام و نام خانوادگی خود را وارد کنید'}}) }}
|
<div>
|
||||||
{{ form_label(form.name) }}
|
{{ form_label(form.name, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
{{ form_errors(form.name) }}
|
{{ form_widget(form.name, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-right', 'placeholder': 'نام و نام خانوادگی خود را وارد کنید'}}) }}
|
||||||
|
{{ form_errors(form.name) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ form_label(form.email, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
|
{{ form_widget(form.email, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'example@domain.com'}}) }}
|
||||||
|
{{ form_errors(form.email) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ form_label(form.phone, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
|
{{ form_widget(form.phone, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': '09123456789'}}) }}
|
||||||
|
{{ form_errors(form.phone) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ form_label(form.plainPassword.first, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
|
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'کلمه عبور خود را وارد کنید'}}) }}
|
||||||
|
{{ form_errors(form.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ form_label(form.plainPassword.second, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
|
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'کلمه عبور را مجدداً وارد کنید'}}) }}
|
||||||
|
{{ form_errors(form.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="flex items-start">
|
||||||
{{ form_widget(form.email, {'attr': {'class': 'form-control', 'placeholder': 'example@domain.com'}}) }}
|
{{ form_widget(form.agreeTerms, {'attr': {'class': 'w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2 mt-1 ml-3'}}) }}
|
||||||
{{ form_label(form.email) }}
|
<label class="text-sm text-gray-700" for="{{ form.agreeTerms.vars.id }}">
|
||||||
{{ form_errors(form.email) }}
|
<a href="{{ path('app_page', {'url': 'terms'}) }}" target="_blank" class="text-blue-600 hover:text-blue-700 font-medium">قوانین و مقررات</a> را میپذیرم
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
{{ form_widget(form.phone, {'attr': {'class': 'form-control', 'placeholder': '09123456789'}}) }}
|
|
||||||
{{ form_label(form.phone) }}
|
|
||||||
{{ form_errors(form.phone) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور خود را وارد کنید'}}) }}
|
|
||||||
{{ form_label(form.plainPassword.first) }}
|
|
||||||
{{ form_errors(form.plainPassword.first) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
|
||||||
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور را مجدداً وارد کنید'}}) }}
|
|
||||||
{{ form_label(form.plainPassword.second) }}
|
|
||||||
{{ form_errors(form.plainPassword.second) }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
{{ form_widget(form.agreeTerms, {'attr': {'class': 'form-check-input'}}) }}
|
|
||||||
<label class="form-check-label" for="{{ form.agreeTerms.vars.id }}">
|
|
||||||
<a href="{{ path('app_page', {'url': 'terms'}) }}" target="_blank">قوانین و مقررات</a> را میپذیرم
|
|
||||||
</label>
|
</label>
|
||||||
{{ form_errors(form.agreeTerms) }}
|
{{ form_errors(form.agreeTerms) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary w-100 mb-3" type="submit">
|
<button class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center gap-2" type="submit">
|
||||||
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus"> عضویت در باشگاه
|
<img src="{{ asset('/img/icons/user-plus.svg') }}" alt="عضویت" class="icon-svg icon-user-plus">
|
||||||
|
عضویت در باشگاه
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
|
||||||
<div class="auth-links">
|
<div class="mt-8 text-center">
|
||||||
<p>قبلاً عضو شدهاید؟
|
<p class="text-gray-600">
|
||||||
<a href="{{ path('customer_login') }}">
|
قبلاً عضو شدهاید؟
|
||||||
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in"> ورود
|
<a href="{{ path('customer_login') }}" class="text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
|
<img src="{{ asset('/img/icons/sign-in.svg') }}" alt="ورود" class="icon-svg icon-sign-in ml-1">
|
||||||
|
ورود
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,38 @@
|
||||||
{% block page_subtitle %}کلمه عبور جدید خود را وارد کنید{% endblock %}
|
{% block page_subtitle %}کلمه عبور جدید خود را وارد کنید{% endblock %}
|
||||||
|
|
||||||
{% block auth_content %}
|
{% block auth_content %}
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-8">
|
||||||
<img src="{{ asset('/img/icons/lock.svg') }}" alt="قفل" class="icon-svg-large icon-lock text-primary">
|
<img src="{{ asset('/img/icons/lock.svg') }}" alt="قفل" class="icon-svg-large icon-lock text-blue-600">
|
||||||
<p class="text-muted">کلمه عبور جدید خود را وارد کنید.</p>
|
<p class="text-gray-600 mt-4">کلمه عبور جدید خود را وارد کنید.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-6'}}) }}
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="space-y-4">
|
||||||
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور جدید خود را وارد کنید'}}) }}
|
<div>
|
||||||
{{ form_label(form.plainPassword.first) }}
|
{{ form_label(form.plainPassword.first, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
{{ form_errors(form.plainPassword.first) }}
|
{{ form_widget(form.plainPassword.first, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'کلمه عبور جدید خود را وارد کنید'}}) }}
|
||||||
|
{{ form_errors(form.plainPassword.first) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ form_label(form.plainPassword.second, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
|
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-left', 'placeholder': 'کلمه عبور جدید را مجدداً وارد کنید'}}) }}
|
||||||
|
{{ form_errors(form.plainPassword.second) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<button class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 px-6 rounded-xl font-semibold hover:from-blue-700 hover:to-purple-700 transform hover:-translate-y-0.5 transition-all duration-200 shadow-lg hover:shadow-xl flex items-center justify-center gap-2" type="submit">
|
||||||
{{ form_widget(form.plainPassword.second, {'attr': {'class': 'form-control', 'placeholder': 'کلمه عبور جدید را مجدداً وارد کنید'}}) }}
|
<img src="{{ asset('/img/icons/lock.svg') }}" alt="تغییر" class="icon-svg icon-lock">
|
||||||
{{ form_label(form.plainPassword.second) }}
|
تغییر کلمه عبور
|
||||||
{{ form_errors(form.plainPassword.second) }}
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ form_end(form) }}
|
{{ form_end(form) }}
|
||||||
|
|
||||||
<div class="auth-links">
|
<div class="mt-8 text-center">
|
||||||
<a href="{{ path('customer_login') }}">
|
<a href="{{ path('customer_login') }}" class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors duration-200">
|
||||||
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left"> بازگشت به صفحه ورود
|
<img src="{{ asset('/img/icons/arrow-left.svg') }}" alt="بازگشت" class="icon-svg icon-arrow-left ml-2">
|
||||||
|
بازگشت به صفحه ورود
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
127
templates/emails/answer_accepted_notification.html.twig
Normal file
127
templates/emails/answer_accepted_notification.html.twig
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir="rtl" lang="fa">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>پاسخ شما پذیرفته شد</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Tahoma', 'Arial', sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.success-icon {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 48px;
|
||||||
|
color: #28a745;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.question-box {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 4px solid #007bff;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.question-content {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.answer-box {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-right: 4px solid #28a745;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.answer-content {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>پاسخ شما پذیرفته شد</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="success-icon">✓</div>
|
||||||
|
|
||||||
|
<p>سلام {{ answerAuthor.name }}،</p>
|
||||||
|
|
||||||
|
<p>تبریک! پاسخ شما برای سوال زیر به عنوان بهترین پاسخ انتخاب شده است:</p>
|
||||||
|
|
||||||
|
<div class="question-box">
|
||||||
|
<div class="question-title">{{ question.title }}</div>
|
||||||
|
<div class="question-content">{{ question.content|markdown|raw }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="answer-box">
|
||||||
|
<div class="answer-content">{{ answer.content|markdown|raw }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<a href="{{ url('qa_question_show', {'id': question.id}) }}" class="btn">
|
||||||
|
مشاهده سوال و پاسخ
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||||
|
<p>© {{ "now"|date("Y") }} حسابیکس - سیستم حسابداری آنلاین</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
125
templates/emails/answer_notification.html.twig
Normal file
125
templates/emails/answer_notification.html.twig
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir="rtl" lang="fa">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>پاسخ جدید برای سوال شما</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Tahoma', 'Arial', sans-serif;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
.question-box {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 4px solid #007bff;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.question-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.question-content {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.answer-box {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-right: 4px solid #28a745;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.answer-author {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.answer-content {
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>پاسخ جدید برای سوال شما</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<p>سلام {{ questionAuthor.name }}،</p>
|
||||||
|
|
||||||
|
<p>برای سوال شما پاسخ جدیدی دریافت کردهاید:</p>
|
||||||
|
|
||||||
|
<div class="question-box">
|
||||||
|
<div class="question-title">{{ question.title }}</div>
|
||||||
|
<div class="question-content">{{ question.content|markdown|raw }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="answer-box">
|
||||||
|
<div class="answer-author">پاسخ از: {{ answerAuthor.name }}</div>
|
||||||
|
<div class="answer-content">{{ answer.content|markdown|raw }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="text-align: center;">
|
||||||
|
<a href="{{ url('qa_question_show', {'id': question.id}) }}" class="btn">
|
||||||
|
مشاهده پاسخ کامل
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>این ایمیل به صورت خودکار ارسال شده است. لطفاً به آن پاسخ ندهید.</p>
|
||||||
|
<p>© {{ "now"|date("Y") }} حسابیکس - سیستم حسابداری آنلاین</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -108,42 +108,45 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6">
|
<!-- حامیان مالی -->
|
||||||
<div class="container mx-auto bg-gray-100 rounded-2xl p-6">
|
<div class="w-full mb-16">
|
||||||
<h4 class="text-center mb-4 text-primary-600 font-bold text-xl">حامیان مالی حسابیکس</h4>
|
<div class="container mx-auto px-4">
|
||||||
<p class="text-center text-gray-600 mb-6">حسابیکس با مشارکت حامیان مالی و حمایت کاربران عزیز، به صورت مستمر در حال توسعه و بهبود است. هر یک از شما با استفاده از خدمات حسابیکس، در پیشرفت این پلتفرم نقش دارید.</p>
|
<div class="text-center mb-12">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 justify-center">
|
<h2 class="text-3xl md:text-4xl font-bold text-gray-800 mb-4">
|
||||||
<div class="w-full">
|
حامیان حسابیکس
|
||||||
<div class="card h-full hover:shadow-lg transition-shadow duration-300">
|
</h2>
|
||||||
<a href="https://parspack.com" target="_blank" class="block">
|
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||||
<div class="p-4 text-center">
|
حسابیکس با مشارکت حامیان مالی و حمایت کاربران عزیز، به صورت مستمر در حال توسعه و بهبود است.
|
||||||
<img src="{{asset('img/sp/parspack-logo.png')}}" class="w-full p-2" alt="پارس پک">
|
</p>
|
||||||
<p class="text-gray-600 mt-2 mb-0">پارس پک</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="card h-full hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<a href="https://melipayamak.com" target="_blank" class="block">
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<img src="{{asset('img/sp/melipayamak.png')}}" class="w-full p-2" alt="ملی پیامک">
|
|
||||||
<p class="text-gray-600 mt-2 mb-0">ملی پیامک</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="card h-full hover:shadow-lg transition-shadow duration-300">
|
|
||||||
<a href="https://irmotorbargh.com" target="_blank" class="block">
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<img src="{{asset('img/sp/irmr.png')}}" class="w-full p-2" alt="راد دیتا">
|
|
||||||
<p class="text-gray-600 mt-2 mb-0">ایران موتوربرق</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<!-- پارس پک -->
|
||||||
|
<a href="https://parspack.com" target="_blank"
|
||||||
|
class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 p-8 text-center hover:scale-105 cursor-pointer">
|
||||||
|
<img src="{{asset('img/sp/parspack-logo.png')}}"
|
||||||
|
class="w-24 h-24 mx-auto object-contain"
|
||||||
|
alt="پارس پک">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ملی پیامک -->
|
||||||
|
<a href="https://melipayamak.com" target="_blank"
|
||||||
|
class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 p-8 text-center hover:scale-105 cursor-pointer">
|
||||||
|
<img src="{{asset('img/sp/melipayamak.png')}}"
|
||||||
|
class="w-24 h-24 mx-auto object-contain"
|
||||||
|
alt="ملی پیامک">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- ایران موتوربرق -->
|
||||||
|
<a href="https://irmotorbargh.com" target="_blank"
|
||||||
|
class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 p-8 text-center hover:scale-105 cursor-pointer">
|
||||||
|
<img src="{{asset('img/sp/irmr.png')}}"
|
||||||
|
class="w-24 h-24 mx-auto object-contain"
|
||||||
|
alt="ایران موتوربرق">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Features Section با طراحی مدرن و تعاملی -->
|
<!-- Features Section با طراحی مدرن و تعاملی -->
|
||||||
|
|
@ -220,16 +223,9 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center mb-3">
|
<h3 class="text-xl font-bold text-gray-800 group-hover:text-blue-600 transition-colors duration-300 mb-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-r from-blue-100 to-blue-200 rounded-xl flex items-center justify-center ml-3 group-hover:scale-110 transition-transform duration-300">
|
ثبتنام و ساخت پنل
|
||||||
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</h3>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 group-hover:text-blue-600 transition-colors duration-300">
|
|
||||||
ثبتنام و ساخت پنل
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 leading-relaxed">
|
<p class="text-gray-600 leading-relaxed">
|
||||||
در حسابیکس عضو شوید و پنل کسب و کار خود را به سادگی بسازید. هر تعداد کسبوکار که داشته باشید میتوانید از طریق یک داشبورد واحد آنها را مدیریت کنید.
|
در حسابیکس عضو شوید و پنل کسب و کار خود را به سادگی بسازید. هر تعداد کسبوکار که داشته باشید میتوانید از طریق یک داشبورد واحد آنها را مدیریت کنید.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -261,17 +257,9 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center mb-3">
|
<h3 class="text-xl font-bold text-gray-800 group-hover:text-purple-600 transition-colors duration-300 mb-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-r from-purple-100 to-purple-200 rounded-xl flex items-center justify-center ml-3 group-hover:scale-110 transition-transform duration-300">
|
تنظیمات و راهاندازی
|
||||||
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</h3>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 group-hover:text-purple-600 transition-colors duration-300">
|
|
||||||
تنظیمات و راهاندازی
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 leading-relaxed">
|
<p class="text-gray-600 leading-relaxed">
|
||||||
اطلاعات کسب و کار خود را وارد کنید و تنظیمات اولیه را انجام دهید. حسابیکس آماده و در اختیار شماست. در صورت نیاز به راهنمایی، تیم پشتیبانی پاسخگوی شماست.
|
اطلاعات کسب و کار خود را وارد کنید و تنظیمات اولیه را انجام دهید. حسابیکس آماده و در اختیار شماست. در صورت نیاز به راهنمایی، تیم پشتیبانی پاسخگوی شماست.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -301,16 +289,9 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center mb-3">
|
<h3 class="text-xl font-bold text-gray-800 group-hover:text-green-600 transition-colors duration-300 mb-3">
|
||||||
<div class="w-10 h-10 bg-gradient-to-r from-green-100 to-green-200 rounded-xl flex items-center justify-center ml-3 group-hover:scale-110 transition-transform duration-300">
|
دسترسی همهجا
|
||||||
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
</h3>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-800 group-hover:text-green-600 transition-colors duration-300">
|
|
||||||
دسترسی همهجا
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-600 leading-relaxed">
|
<p class="text-gray-600 leading-relaxed">
|
||||||
از حسابیکس بر روی موبایل، تبلت و کامپیوتر لذت ببرید. تنها کافی است که یک اتصال اینترنت داشته باشید. حسابیکس در هر نقطهای از جهان در دسترس شما خواهد بود.
|
از حسابیکس بر روی موبایل، تبلت و کامپیوتر لذت ببرید. تنها کافی است که یک اتصال اینترنت داشته باشید. حسابیکس در هر نقطهای از جهان در دسترس شما خواهد بود.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -457,7 +438,7 @@
|
||||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
{{ Jdate.jdate('Y/n/d',post.dateSubmit) }}
|
{{ Jdate.jdate('Y/n/d',post.dateSubmit|date('U')) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,87 +4,75 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="bg-primary text-white py-5">
|
<div class="bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-700 text-white py-16">
|
||||||
<div class="container">
|
<div class="container mx-auto px-4">
|
||||||
<div class="row align-items-center">
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
<div class="col-lg-8 mx-auto text-center">
|
<div class="mb-8">
|
||||||
<div class="mb-4">
|
<div class="inline-flex items-center justify-center mb-6 bg-white bg-opacity-20 rounded-full w-32 h-32">
|
||||||
<div class="d-inline-flex align-items-center justify-content-center mb-3 bg-white bg-opacity-20 rounded-circle" style="width: 120px; height: 120px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-warning">
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h1 class="display-4 fw-bold mb-3">پشتیبانی تخصصی حسابیکس</h1>
|
|
||||||
<p class="mb-4">نرم افزار حسابداری ابری، متن باز و رایگان با پشتیبانی حرفهای برای کسب و کارها</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold mb-6">پشتیبانی تخصصی حسابیکس</h1>
|
||||||
|
<p class="text-xl text-blue-100 mb-8">نرم افزار حسابداری ابری، متن باز و رایگان با پشتیبانی حرفهای برای کسب و کارها</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container py-5">
|
<div class="container mx-auto px-4 py-16">
|
||||||
<!-- Open Source Section -->
|
<!-- Open Source Section -->
|
||||||
<div class="row mb-5">
|
<div class="mb-16">
|
||||||
<div class="col-lg-10 mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<div class="card border-0 shadow-lg">
|
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
|
||||||
<div class="card-body p-5">
|
<div class="p-8 md:p-12">
|
||||||
<div class="row align-items-center">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
|
||||||
<div class="col-md-6 text-center mb-4 mb-md-0">
|
<div class="text-center lg:text-right">
|
||||||
<div class="bg-success bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 120px; height: 120px;">
|
<div class="bg-green-100 rounded-full inline-flex items-center justify-center mb-6 w-32 h-32">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600">
|
||||||
<polyline points="16,18 22,12 16,6"></polyline>
|
<polyline points="16,18 22,12 16,6"></polyline>
|
||||||
<polyline points="8,6 2,12 8,18"></polyline>
|
<polyline points="8,6 2,12 8,18"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="h3 text-success fw-bold mb-3">حسابیکس - نرم افزار متن باز</h2>
|
<h2 class="text-3xl font-bold text-green-600 mb-4">حسابیکس - نرم افزار متن باز</h2>
|
||||||
<p class="text-muted">حسابیکس یک نرم افزار حسابداری ابری، متن باز و کاملاً رایگان است که شما میتوانید از آن برای مدیریت مالی کسب و کار خود استفاده کنید.</p>
|
<p class="text-gray-600 text-lg leading-relaxed">حسابیکس یک نرم افزار حسابداری ابری، متن باز و کاملاً رایگان است که شما میتوانید از آن برای مدیریت مالی کسب و کار خود استفاده کنید.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="row g-3">
|
<div class="text-center p-4 bg-gray-50 rounded-2xl hover:bg-gray-100 transition-colors duration-200">
|
||||||
<div class="col-6">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600 mb-2 mx-auto">
|
||||||
<div class="text-center p-3 bg-light rounded-3">
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary mb-2">
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||||
<circle cx="9" cy="7" r="4"></circle>
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
</svg>
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
<h6 class="font-bold mb-1 text-gray-800">جامعه فعال</h6>
|
||||||
</svg>
|
<small class="text-gray-500">توسعهدهندگان</small>
|
||||||
<h6 class="fw-bold mb-1">جامعه فعال</h6>
|
</div>
|
||||||
<small class="text-muted">توسعهدهندگان</small>
|
<div class="text-center p-4 bg-gray-50 rounded-2xl hover:bg-gray-100 transition-colors duration-200">
|
||||||
</div>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 mb-2 mx-auto">
|
||||||
</div>
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||||
<div class="col-6">
|
</svg>
|
||||||
<div class="text-center p-3 bg-light rounded-3">
|
<h6 class="font-bold mb-1 text-gray-800">امنیت بالا</h6>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success mb-2">
|
<small class="text-gray-500">کدهای باز</small>
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
</div>
|
||||||
</svg>
|
<div class="text-center p-4 bg-gray-50 rounded-2xl hover:bg-gray-100 transition-colors duration-200">
|
||||||
<h6 class="fw-bold mb-1">امنیت بالا</h6>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-500 mb-2 mx-auto">
|
||||||
<small class="text-muted">کدهای باز</small>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
</div>
|
<polyline points="7,10 12,15 17,10"></polyline>
|
||||||
</div>
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
<div class="col-6">
|
</svg>
|
||||||
<div class="text-center p-3 bg-light rounded-3">
|
<h6 class="font-bold mb-1 text-gray-800">کاملاً رایگان</h6>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-info mb-2">
|
<small class="text-gray-500">بدون هزینه</small>
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
</div>
|
||||||
<polyline points="7,10 12,15 17,10"></polyline>
|
<div class="text-center p-4 bg-gray-50 rounded-2xl hover:bg-gray-100 transition-colors duration-200">
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-500 mb-2 mx-auto">
|
||||||
</svg>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
<h6 class="fw-bold mb-1">کاملاً رایگان</h6>
|
<path d="M12 1v6m0 6v6"></path>
|
||||||
<small class="text-muted">بدون هزینه</small>
|
<path d="M21 12h-6m-6 0H3"></path>
|
||||||
</div>
|
</svg>
|
||||||
</div>
|
<h6 class="font-bold mb-1 text-gray-800">قابل سفارشی</h6>
|
||||||
<div class="col-6">
|
<small class="text-gray-500">انعطافپذیر</small>
|
||||||
<div class="text-center p-3 bg-light rounded-3">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-warning mb-2">
|
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
|
||||||
<path d="M12 1v6m0 6v6"></path>
|
|
||||||
<path d="M21 12h-6m-6 0H3"></path>
|
|
||||||
</svg>
|
|
||||||
<h6 class="fw-bold mb-1">قابل سفارشی</h6>
|
|
||||||
<small class="text-muted">انعطافپذیر</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -94,55 +82,55 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Professional Services Section -->
|
<!-- Professional Services Section -->
|
||||||
<div class="row mb-5">
|
<div class="mb-16">
|
||||||
<div class="col-lg-10 mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<div class="text-center mb-5">
|
<div class="text-center mb-12">
|
||||||
<h2 class="display-6 fw-bold text-primary mb-3">پشتیبانی تخصصی برای کسب و کارها</h2>
|
<h2 class="text-4xl md:text-5xl font-bold text-blue-600 mb-6">پشتیبانی تخصصی برای کسب و کارها</h2>
|
||||||
<p class="text-muted">اگر شما یک شرکت یا کسب و کار دارید و نیاز به پشتیبانی حرفهای، قابلیتهای سفارشی یا خدمات تخصصی برای نرم افزار حسابداری حسابیکس دارید، ما آماده ارائه خدمات زیر هستیم:</p>
|
<p class="text-gray-600 text-lg max-w-4xl mx-auto leading-relaxed">اگر شما یک شرکت یا کسب و کار دارید و نیاز به پشتیبانی حرفهای، قابلیتهای سفارشی یا خدمات تخصصی برای نرم افزار حسابداری حسابیکس دارید، ما آماده ارائه خدمات زیر هستیم:</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<!-- Custom Development -->
|
<!-- Custom Development -->
|
||||||
<div class="col-lg-6">
|
<div class="group">
|
||||||
<div class="card h-100 border-0 shadow-sm hover-card">
|
<div class="bg-white rounded-3xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 h-full">
|
||||||
<div class="card-body p-4">
|
<div class="p-8">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="flex items-center mb-6">
|
||||||
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
<div class="bg-blue-100 rounded-full flex items-center justify-center w-16 h-16 ml-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600">
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
<path d="M12 1v6m0 6v6"></path>
|
<path d="M12 1v6m0 6v6"></path>
|
||||||
<path d="M21 12h-6m-6 0H3"></path>
|
<path d="M21 12h-6m-6 0H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="fw-bold mb-1">توسعه قابلیتهای سفارشی</h4>
|
<h4 class="text-xl font-bold text-gray-800 mb-2">توسعه قابلیتهای سفارشی</h4>
|
||||||
<p class="text-muted mb-0">توسعه ماژولهای حسابداری خاص مطابق با نیازهای کسب و کار شما</p>
|
<p class="text-gray-600">توسعه ماژولهای حسابداری خاص مطابق با نیازهای کسب و کار شما</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="space-y-3">
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
تحلیل نیازمندیهای حسابداری
|
<span class="text-gray-700">تحلیل نیازمندیهای حسابداری</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
طراحی ماژولهای اختصاصی
|
<span class="text-gray-700">طراحی ماژولهای اختصاصی</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
یکپارچهسازی با سیستمهای موجود
|
<span class="text-gray-700">یکپارچهسازی با سیستمهای موجود</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
تست و تضمین کیفیت
|
<span class="text-gray-700">تست و تضمین کیفیت</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -150,44 +138,44 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Technical Support -->
|
<!-- Technical Support -->
|
||||||
<div class="col-lg-6">
|
<div class="group">
|
||||||
<div class="card h-100 border-0 shadow-sm hover-card">
|
<div class="bg-white rounded-3xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 h-full">
|
||||||
<div class="card-body p-4">
|
<div class="p-8">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="flex items-center mb-6">
|
||||||
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
<div class="bg-green-100 rounded-full flex items-center justify-center w-16 h-16 ml-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600">
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="fw-bold mb-1">پشتیبانی فنی ۲۴/۷</h4>
|
<h4 class="text-xl font-bold text-gray-800 mb-2">پشتیبانی فنی ۲۴/۷</h4>
|
||||||
<p class="text-muted mb-0">پشتیبانی فنی شبانهروزی برای حل مشکلات حسابداری و پاسخ به سوالات</p>
|
<p class="text-gray-600">پشتیبانی فنی شبانهروزی برای حل مشکلات حسابداری و پاسخ به سوالات</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="space-y-3">
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
پاسخ سریع به مشکلات فنی
|
<span class="text-gray-700">پاسخ سریع به مشکلات فنی</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
راهنمایی تخصصی حسابداری
|
<span class="text-gray-700">راهنمایی تخصصی حسابداری</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
حل مشکلات از راه دور
|
<span class="text-gray-700">حل مشکلات از راه دور</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
پشتیبانی تلفنی و آنلاین
|
<span class="text-gray-700">پشتیبانی تلفنی و آنلاین</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -195,45 +183,45 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Training -->
|
<!-- Training -->
|
||||||
<div class="col-lg-6">
|
<div class="group">
|
||||||
<div class="card h-100 border-0 shadow-sm hover-card">
|
<div class="bg-white rounded-3xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 h-full">
|
||||||
<div class="card-body p-4">
|
<div class="p-8">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="flex items-center mb-6">
|
||||||
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
<div class="bg-blue-100 rounded-full flex items-center justify-center w-16 h-16 ml-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-info">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600">
|
||||||
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
|
||||||
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="fw-bold mb-1">آموزش و راهنمایی</h4>
|
<h4 class="text-xl font-bold text-gray-800 mb-2">آموزش و راهنمایی</h4>
|
||||||
<p class="text-muted mb-0">آموزش کارکنان در استفاده بهینه از نرم افزار حسابداری حسابیکس</p>
|
<p class="text-gray-600">آموزش کارکنان در استفاده بهینه از نرم افزار حسابداری حسابیکس</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="space-y-3">
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
آموزش حضوری و آنلاین
|
<span class="text-gray-700">آموزش حضوری و آنلاین</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
مستندات آموزشی اختصاصی
|
<span class="text-gray-700">مستندات آموزشی اختصاصی</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
ویدیوهای آموزشی حسابداری
|
<span class="text-gray-700">ویدیوهای آموزشی حسابداری</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
پشتیبانی آموزشی مداوم
|
<span class="text-gray-700">پشتیبانی آموزشی مداوم</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,46 +229,46 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Installation -->
|
<!-- Installation -->
|
||||||
<div class="col-lg-6">
|
<div class="group">
|
||||||
<div class="card h-100 border-0 shadow-sm hover-card">
|
<div class="bg-white rounded-3xl shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2 h-full">
|
||||||
<div class="card-body p-4">
|
<div class="p-8">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="flex items-center mb-6">
|
||||||
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 60px; height: 60px;">
|
<div class="bg-yellow-100 rounded-full flex items-center justify-center w-16 h-16 ml-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-warning">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-600">
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="fw-bold mb-1">نصب و راهاندازی</h4>
|
<h4 class="text-xl font-bold text-gray-800 mb-2">نصب و راهاندازی</h4>
|
||||||
<p class="text-muted mb-0">نصب، پیکربندی و راهاندازی نرم افزار حسابداری در محیط شما</p>
|
<p class="text-gray-600">نصب، پیکربندی و راهاندازی نرم افزار حسابداری در محیط شما</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-unstyled">
|
<ul class="space-y-3">
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
نصب روی سرور اختصاصی
|
<span class="text-gray-700">نصب روی سرور اختصاصی</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
پیکربندی امنیتی
|
<span class="text-gray-700">پیکربندی امنیتی</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
تنظیمات پایگاه داده
|
<span class="text-gray-700">تنظیمات پایگاه داده</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-2">
|
<li class="flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600 ml-3">
|
||||||
<polyline points="20,6 9,17 4,12"></polyline>
|
<polyline points="20,6 9,17 4,12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
تضمین عملکرد بهینه
|
<span class="text-gray-700">تضمین عملکرد بهینه</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -291,51 +279,45 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact CTA Section -->
|
<!-- Contact CTA Section -->
|
||||||
<div class="row mb-5">
|
<div class="mb-16">
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="max-w-4xl mx-auto">
|
||||||
<div class="card border-0 shadow-lg bg-primary text-white">
|
<div class="bg-gradient-to-br from-blue-600 via-purple-600 to-indigo-700 text-white rounded-3xl shadow-2xl overflow-hidden">
|
||||||
<div class="card-body p-5 text-center">
|
<div class="p-8 md:p-12 text-center">
|
||||||
<div class="mb-4">
|
<div class="mb-8">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-warning mb-3">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-400 mb-6 mx-auto">
|
||||||
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"></polygon>
|
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
<h3 class="fw-bold mb-3">آماده شروع هستید؟</h3>
|
<h3 class="text-3xl font-bold mb-4">آماده شروع هستید؟</h3>
|
||||||
<p class="mb-4">برای دریافت خدمات تخصصی و مشاوره رایگان درباره نرم افزار حسابداری حسابیکس، همین حالا با ما تماس بگیرید</p>
|
<p class="text-xl text-blue-100 mb-8">برای دریافت خدمات تخصصی و مشاوره رایگان درباره نرم افزار حسابداری حسابیکس، همین حالا با ما تماس بگیرید</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mb-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
||||||
<div class="col-md-4">
|
<div class="text-center">
|
||||||
<div class="text-center">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-3 mx-auto text-yellow-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2">
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<polyline points="12,6 12,12 16,14"></polyline>
|
||||||
<polyline points="12,6 12,12 16,14"></polyline>
|
</svg>
|
||||||
</svg>
|
<h6 class="text-lg font-bold mb-2">پاسخ سریع</h6>
|
||||||
<h6 class="fw-bold">پاسخ سریع</h6>
|
<small class="text-blue-200">حداکثر ۲۴ ساعت</small>
|
||||||
<small>حداکثر ۲۴ ساعت</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center">
|
||||||
<div class="text-center">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-3 mx-auto text-yellow-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2">
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
</svg>
|
||||||
</svg>
|
<h6 class="text-lg font-bold mb-2">پشتیبانی ۲۴/۷</h6>
|
||||||
<h6 class="fw-bold">پشتیبانی ۲۴/۷</h6>
|
<small class="text-blue-200">همیشه در دسترس</small>
|
||||||
<small>همیشه در دسترس</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center">
|
||||||
<div class="text-center">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-3 mx-auto text-yellow-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2">
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
</svg>
|
||||||
</svg>
|
<h6 class="text-lg font-bold mb-2">تضمین کیفیت</h6>
|
||||||
<h6 class="fw-bold">تضمین کیفیت</h6>
|
<small class="text-blue-200">خدمات تضمین شده</small>
|
||||||
<small>خدمات تضمین شده</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="{{ path('app_page', {'url': 'contact'}) }}" class="btn btn-warning btn-lg px-5 py-3 fw-bold transition-all">
|
<a href="{{ path('app_page', {'url': 'contact'}) }}" class="inline-flex items-center px-8 py-4 bg-yellow-500 text-gray-900 rounded-2xl hover:bg-yellow-400 transform hover:-translate-y-1 transition-all duration-300 shadow-lg hover:shadow-xl font-bold text-lg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="ml-2">
|
||||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||||
<polygon points="22,2 15,2 11,6 2,15 2,22 9,22 13,18 22,9 22,2"></polygon>
|
<polygon points="22,2 15,2 11,6 2,15 2,22 9,22 13,18 22,9 22,2"></polygon>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -347,106 +329,79 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Benefits Section -->
|
<!-- Benefits Section -->
|
||||||
<div class="row">
|
<div>
|
||||||
<div class="col-lg-10 mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<div class="text-center mb-5">
|
<div class="text-center mb-12">
|
||||||
<h2 class="display-6 fw-bold text-primary mb-3">مزایای پشتیبانی تخصصی حسابیکس</h2>
|
<h2 class="text-4xl md:text-5xl font-bold text-blue-600 mb-6">مزایای پشتیبانی تخصصی حسابیکس</h2>
|
||||||
<p class="text-muted">چرا باید از خدمات پشتیبانی تخصصی ما برای نرم افزار حسابداری حسابیکس استفاده کنید؟</p>
|
<p class="text-gray-600 text-lg max-w-4xl mx-auto leading-relaxed">چرا باید از خدمات پشتیبانی تخصصی ما برای نرم افزار حسابداری حسابیکس استفاده کنید؟</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-blue-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary">
|
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"></polygon>
|
||||||
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"></polygon>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">پاسخ سریع</h5>
|
|
||||||
<p class="text-muted">دریافت پاسخ تخصصی در کمترین زمان ممکن</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">پاسخ سریع</h5>
|
||||||
|
<p class="text-gray-600">دریافت پاسخ تخصصی در کمترین زمان ممکن</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-green-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-success bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-success">
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
||||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">حل مشکلات</h5>
|
|
||||||
<p class="text-muted">حل مشکلات پیچیده حسابداری در کمترین زمان</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">حل مشکلات</h5>
|
||||||
|
<p class="text-gray-600">حل مشکلات پیچیده حسابداری در کمترین زمان</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-blue-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-info bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-info">
|
<polyline points="16,18 22,12 16,6"></polyline>
|
||||||
<polyline points="16,18 22,12 16,6"></polyline>
|
<polyline points="8,6 2,12 8,18"></polyline>
|
||||||
<polyline points="8,6 2,12 8,18"></polyline>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">توسعه سفارشی</h5>
|
|
||||||
<p class="text-muted">توسعه قابلیتهای اختصاصی برای کسب و کار شما</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">توسعه سفارشی</h5>
|
||||||
|
<p class="text-gray-600">توسعه قابلیتهای اختصاصی برای کسب و کار شما</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-yellow-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-warning bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-yellow-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-warning">
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
<circle cx="9" cy="7" r="4"></circle>
|
||||||
<circle cx="9" cy="7" r="4"></circle>
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">آموزش کارکنان</h5>
|
|
||||||
<p class="text-muted">آموزش کامل کارکنان برای استفاده بهینه از حسابیکس</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">آموزش کارکنان</h5>
|
||||||
|
<p class="text-gray-600">آموزش کامل کارکنان برای استفاده بهینه از حسابیکس</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-red-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-danger bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-red-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-danger">
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
<path d="M12 1v6m0 6v6"></path>
|
||||||
<path d="M12 1v6m0 6v6"></path>
|
<path d="M21 12h-6m-6 0H3"></path>
|
||||||
<path d="M21 12h-6m-6 0H3"></path>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">پشتیبانی از راه دور</h5>
|
|
||||||
<p class="text-muted">حل مشکلات بدون نیاز به حضور فیزیکی</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">پشتیبانی از راه دور</h5>
|
||||||
|
<p class="text-gray-600">حل مشکلات بدون نیاز به حضور فیزیکی</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="text-center p-6 group">
|
||||||
<div class="text-center p-4">
|
<div class="bg-gray-100 rounded-full inline-flex items-center justify-center mb-6 w-20 h-20 group-hover:scale-110 transition-transform duration-300">
|
||||||
<div class="bg-secondary bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 80px; height: 80px;">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-gray-600">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-secondary">
|
<polyline points="23,4 23,10 17,10"></polyline>
|
||||||
<polyline points="23,4 23,10 17,10"></polyline>
|
<polyline points="1,20 1,14 7,14"></polyline>
|
||||||
<polyline points="1,20 1,14 7,14"></polyline>
|
<path d="M20 21C20 19.6044 20 18.4067 20 17.4069C20 16.4072 20 15.6081 20 15.0096C20 14.4111 20 14.012 20 13.8125C20 13.6129 20 13.5132 20 13.5132C20 13.5132 20 13.6129 20 13.8125C20 14.012 20 14.4111 20 15.0096C20 15.6081 20 16.4072 20 17.4069C20 18.4067 20 19.6044 20 21"></path>
|
||||||
<path d="M20 21C20 19.6044 20 18.4067 20 17.4069C20 16.4072 20 15.6081 20 15.0096C20 14.4111 20 14.012 20 13.8125C20 13.6129 20 13.5132 20 13.5132C20 13.5132 20 13.6129 20 13.8125C20 14.012 20 14.4111 20 15.0096C20 15.6081 20 16.4072 20 17.4069C20 18.4067 20 19.6044 20 21"></path>
|
<path d="M4 3C4 4.3956 4 5.5933 4 6.5931C4 7.5928 4 8.3919 4 8.9904C4 9.5889 4 9.988 4 10.1875C4 10.3871 4 10.4868 4 10.4868C4 10.4868 4 10.3871 4 10.1875C4 9.988 4 9.5889 4 8.9904C4 8.3919 4 7.5928 4 6.5931C4 5.5933 4 4.3956 4 3"></path>
|
||||||
<path d="M4 3C4 4.3956 4 5.5933 4 6.5931C4 7.5928 4 8.3919 4 8.9904C4 9.5889 4 9.988 4 10.1875C4 10.3871 4 10.4868 4 10.4868C4 10.4868 4 10.3871 4 10.1875C4 9.988 4 9.5889 4 8.9904C4 8.3919 4 7.5928 4 6.5931C4 5.5933 4 4.3956 4 3"></path>
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h5 class="fw-bold mb-3">بهروزرسانی مداوم</h5>
|
|
||||||
<p class="text-muted">بهروزرسانی و نگهداری منظم نرم افزار حسابداری</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h5 class="text-xl font-bold mb-3 text-gray-800">بهروزرسانی مداوم</h5>
|
||||||
|
<p class="text-gray-600">بهروزرسانی و نگهداری منظم نرم افزار حسابداری</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.hover-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transition-all {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -2,155 +2,6 @@
|
||||||
|
|
||||||
{% block title %}نصب حسابیکس{% endblock %}
|
{% block title %}نصب حسابیکس{% endblock %}
|
||||||
|
|
||||||
{% block stylesheets %}
|
|
||||||
{{ parent() }}
|
|
||||||
<style>
|
|
||||||
.installation-container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-number {
|
|
||||||
background: rgba(255,255,255,0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
height: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
height: 100%;
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 10px 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-success {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-info {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
border: 1px solid #bee5eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 3px solid #f3f3f3;
|
|
||||||
border-top: 3px solid #667eea;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="installation-container" data-controller="installation">
|
<div class="installation-container" data-controller="installation">
|
||||||
<h1 class="text-center mb-4">نصب خودکار حسابیکس</h1>
|
<h1 class="text-center mb-4">نصب خودکار حسابیکس</h1>
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ Jdate.jdate('Y/n/d',post.dateSubmit) }}</span>
|
<span>{{ Jdate.jdate('Y/n/d',post.dateSubmit|date('U')) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 space-x-reverse">
|
<div class="flex items-center space-x-2 space-x-reverse">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{Jdate.jdate('Y/n/d',item.dateSubmit)}}</span>
|
<span>{{Jdate.jdate('Y/n/d',item.dateSubmit|date('U'))}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2 space-x-reverse">
|
<div class="flex items-center space-x-2 space-x-reverse">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -90,40 +90,154 @@
|
||||||
{{ item.body | raw }}
|
{{ item.body | raw }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- اشتراکگذاری -->
|
</article>
|
||||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
|
||||||
<div class="flex items-center justify-between">
|
<!-- اشتراکگذاری -->
|
||||||
<div>
|
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">این مطلب را به اشتراک بگذارید</h3>
|
<div class="text-center">
|
||||||
<div class="flex space-x-3 space-x-reverse">
|
<h3 class="text-xl font-bold text-gray-900 mb-6 flex items-center justify-center">
|
||||||
<a href="https://twitter.com/intent/tweet?text={{item.title|url_encode}}&url={{app.request.uri|url_encode}}"
|
<svg class="w-6 h-6 text-blue-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
target="_blank"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z"></path>
|
||||||
class="flex items-center space-x-2 space-x-reverse px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-200">
|
</svg>
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
این مطلب را به اشتراک بگذارید
|
||||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
</h3>
|
||||||
</svg>
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
<span>توییتر</span>
|
<!-- توییتر -->
|
||||||
</a>
|
<a href="https://twitter.com/intent/tweet?text={{item.title|url_encode}}&url={{app.request.uri|url_encode}}"
|
||||||
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{app.request.uri|url_encode}}"
|
target="_blank"
|
||||||
target="_blank"
|
class="group flex items-center space-x-2 space-x-reverse px-6 py-3 bg-gradient-to-r from-blue-400 to-blue-500 text-white rounded-xl hover:from-blue-500 hover:to-blue-600 transition-all duration-300 transform hover:scale-105 hover:shadow-lg">
|
||||||
class="flex items-center space-x-2 space-x-reverse px-4 py-2 bg-blue-700 text-white rounded-lg hover:bg-blue-800 transition-colors duration-200">
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
</svg>
|
||||||
</svg>
|
<span class="font-medium">توییتر</span>
|
||||||
<span>لینکدین</span>
|
</a>
|
||||||
</a>
|
|
||||||
<a href="https://t.me/share/url?url={{app.request.uri|url_encode}}&text={{item.title|url_encode}}"
|
<!-- لینکدین -->
|
||||||
target="_blank"
|
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{app.request.uri|url_encode}}"
|
||||||
class="flex items-center space-x-2 space-x-reverse px-4 py-2 bg-blue-400 text-white rounded-lg hover:bg-blue-500 transition-colors duration-200">
|
target="_blank"
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
class="group flex items-center space-x-2 space-x-reverse px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl hover:from-blue-700 hover:to-blue-800 transition-all duration-300 transform hover:scale-105 hover:shadow-lg">
|
||||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||||
<span>تلگرام</span>
|
</svg>
|
||||||
</a>
|
<span class="font-medium">لینکدین</span>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
|
||||||
|
<!-- تلگرام -->
|
||||||
|
<a href="https://t.me/share/url?url={{app.request.uri|url_encode}}&text={{item.title|url_encode}}"
|
||||||
|
target="_blank"
|
||||||
|
class="group flex items-center space-x-2 space-x-reverse px-6 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 transform hover:scale-105 hover:shadow-lg">
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium">تلگرام</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- واتساپ -->
|
||||||
|
<a href="https://wa.me/?text={{item.title|url_encode}}%20{{app.request.uri|url_encode}}"
|
||||||
|
target="_blank"
|
||||||
|
class="group flex items-center space-x-2 space-x-reverse px-6 py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl hover:from-green-600 hover:to-green-700 transition-all duration-300 transform hover:scale-105 hover:shadow-lg">
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893A11.821 11.821 0 0020.885 3.488"/>
|
||||||
|
</svg>
|
||||||
|
<span class="font-medium">واتساپ</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- بخش کامنتها -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<!-- نمایش کامنتهای موجود -->
|
||||||
|
{% if comments|length > 0 %}
|
||||||
|
<div class="mb-12">
|
||||||
|
<h3 class="text-2xl font-bold text-gray-900 mb-8 flex items-center">
|
||||||
|
<svg class="w-6 h-6 text-blue-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||||
|
</svg>
|
||||||
|
نظرات ({{ comments|length }})
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-6">
|
||||||
|
{% for comment in comments %}
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||||
|
<div class="flex items-start space-x-4 space-x-reverse">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-blue-600 font-semibold text-sm">
|
||||||
|
{{ comment.name|slice(0, 1)|upper }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center space-x-2 space-x-reverse mb-3">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-900">{{ comment.name }}</h4>
|
||||||
|
<span class="text-xs text-gray-500">{{ Jdate.jdate('Y/n/d H:i', comment.dateSubmit|date('U')) }}</span>
|
||||||
|
{% if comment.website %}
|
||||||
|
<a href="{{ comment.website }}" target="_blank" class="text-blue-500 hover:text-blue-700 text-xs">
|
||||||
|
وبسایت
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-700 leading-relaxed">{{ comment.body }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- فرم ارسال کامنت -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-8">
|
||||||
|
<h3 class="text-xl font-bold text-gray-900 mb-6">نظر خود را بنویسید</h3>
|
||||||
|
<form method="post" action="{{ path('app_blog_post_comment', {'url': item.url}) }}" class="space-y-6">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label for="comment_name" class="block text-sm font-medium text-gray-700 mb-2">نام *</label>
|
||||||
|
<input type="text"
|
||||||
|
id="comment_name"
|
||||||
|
name="comment[name]"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
|
||||||
|
placeholder="نام شما">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="comment_email" class="block text-sm font-medium text-gray-700 mb-2">ایمیل</label>
|
||||||
|
<input type="email"
|
||||||
|
id="comment_email"
|
||||||
|
name="comment[email]"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
|
||||||
|
placeholder="ایمیل شما (اختیاری)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="comment_website" class="block text-sm font-medium text-gray-700 mb-2">وبسایت</label>
|
||||||
|
<input type="url"
|
||||||
|
id="comment_website"
|
||||||
|
name="comment[website]"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200"
|
||||||
|
placeholder="آدرس وبسایت شما (اختیاری)">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="comment_body" class="block text-sm font-medium text-gray-700 mb-2">نظر *</label>
|
||||||
|
<textarea id="comment_body"
|
||||||
|
name="comment[body]"
|
||||||
|
required
|
||||||
|
rows="5"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 resize-none"
|
||||||
|
placeholder="نظر خود را اینجا بنویسید..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
نظر شما پس از تایید مدیر نمایش داده خواهد شد
|
||||||
|
</p>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium">
|
||||||
|
ارسال نظر
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -152,7 +266,7 @@
|
||||||
{{post.title}}
|
{{post.title}}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-xs text-gray-500 mt-1">
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
{{ Jdate.jdate('Y/n/d',post.dateSubmit) }}
|
{{ Jdate.jdate('Y/n/d',post.dateSubmit|date('U')) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,91 +3,137 @@
|
||||||
{% block title %}پاسخ به سوال: {{ question.title }} - پرسش و پاسخ{% endblock %}
|
{% block title %}پاسخ به سوال: {{ question.title }} - پرسش و پاسخ{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container my-4">
|
<main class="min-h-screen bg-gray-50">
|
||||||
<div class="row justify-content-center">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="col-lg-8">
|
<div class="max-w-4xl mx-auto">
|
||||||
<!-- نمایش سوال -->
|
<!-- نمایش سوال -->
|
||||||
<div class="card mb-4">
|
<div class="bg-white rounded-2xl shadow-soft mb-8 overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 px-8 py-6 border-b border-gray-200">
|
||||||
<h4 class="mb-0">سوال:</h4>
|
<h2 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||||
|
<svg class="w-6 h-6 text-blue-600 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
سوال:
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-8">
|
||||||
<h5 class="card-title">{{ question.title }}</h5>
|
<h3 class="text-2xl font-bold text-gray-900 mb-4">{{ question.title }}</h3>
|
||||||
<div class="question-content">
|
<div class="prose prose-lg max-w-none text-gray-800 leading-relaxed mb-6">
|
||||||
{{ question.content|nl2br }}
|
{{ question.content|markdown|raw }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="flex flex-wrap gap-2">
|
||||||
<div class="tags">
|
{% for tagRelation in question.tagRelations %}
|
||||||
{% for tagRelation in question.tagRelations %}
|
<a href="{{ path('qa_tag_questions', {'name': tagRelation.tag.name}) }}"
|
||||||
<span class="badge bg-light text-dark me-1">
|
class="inline-flex items-center px-3 py-1 bg-blue-100 text-blue-800 text-sm font-medium rounded-full hover:bg-blue-200 transition-colors duration-200">
|
||||||
{{ tagRelation.tag.name }}
|
{{ tagRelation.tag.name }}
|
||||||
</span>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- فرم پاسخ -->
|
<!-- فرم پاسخ -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="bg-gradient-to-r from-green-50 to-emerald-50 px-8 py-6 border-b border-gray-200">
|
||||||
<h4 class="mb-0">
|
<h2 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="me-2">
|
<svg class="w-6 h-6 text-green-600 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<polyline points="9,17 4,12 9,7"></polyline>
|
<polyline points="9,17 4,12 9,7"></polyline>
|
||||||
<path d="M20 18v-2a4 4 0 0 0-4-4H4"></path>
|
<path d="M20 18v-2a4 4 0 0 0-4-4H4"></path>
|
||||||
</svg>پاسخ شما
|
</svg>
|
||||||
</h4>
|
پاسخ شما
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-8">
|
||||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-6'}}) }}
|
||||||
|
|
||||||
<div class="mb-4">
|
<div>
|
||||||
{{ form_label(form.content) }}
|
{{ form_label(form.content, null, {'label_attr': {'class': 'block text-sm font-medium text-gray-700 mb-2'}}) }}
|
||||||
<div class="editor-toolbar mb-2">
|
<div class="qa-editor-toolbar border border-gray-300 border-b-0 rounded-t-xl p-3 bg-gray-50">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="formatText('bold')" title="پررنگ">
|
<div class="flex flex-wrap gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<button type="button" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" onclick="formatText('bold')" title="پررنگ">
|
||||||
<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
||||||
</button>
|
</svg>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="formatText('italic')" title="کج">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<button type="button" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" onclick="formatText('italic')" title="کج">
|
||||||
<line x1="19" y1="4" x2="10" y2="4"></line>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<line x1="14" y1="20" x2="5" y2="20"></line>
|
<line x1="19" y1="4" x2="10" y2="4"></line>
|
||||||
<line x1="15" y1="4" x2="9" y2="20"></line>
|
<line x1="14" y1="20" x2="5" y2="20"></line>
|
||||||
</svg>
|
<line x1="15" y1="4" x2="9" y2="20"></line>
|
||||||
</button>
|
</svg>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="formatText('code')" title="کد">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<button type="button" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" onclick="formatText('code')" title="کد">
|
||||||
<polyline points="16,18 22,12 16,6"></polyline>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<polyline points="8,6 2,12 8,18"></polyline>
|
<polyline points="16,18 22,12 16,6"></polyline>
|
||||||
</svg>
|
<polyline points="8,6 2,12 8,18"></polyline>
|
||||||
</button>
|
</svg>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="insertList()" title="لیست">
|
</button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<button type="button" class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" onclick="insertList()" title="لیست">
|
||||||
<line x1="8" y1="6" x2="21" y2="6"></line>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<line x1="8" y1="12" x2="21" y2="12"></line>
|
<line x1="8" y1="6" x2="21" y2="6"></line>
|
||||||
<line x1="8" y1="18" x2="21" y2="18"></line>
|
<line x1="8" y1="12" x2="21" y2="12"></line>
|
||||||
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
<line x1="8" y1="18" x2="21" y2="18"></line>
|
||||||
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
||||||
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
||||||
</svg>
|
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ form_widget(form.content, {'attr': {'class': 'form-control editor-textarea', 'rows': 8}}) }}
|
{{ form_widget(form.content, {'attr': {'class': 'w-full px-4 py-3 border border-gray-300 border-t-0 rounded-b-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200', 'rows': 8}}) }}
|
||||||
{{ form_errors(form.content) }}
|
{{ form_errors(form.content) }}
|
||||||
<div class="form-text">
|
<div class="mt-2 text-sm text-gray-600">
|
||||||
پاسخ خود را به صورت کامل و مفصل ارائه دهید. هرچه پاسخ شما دقیقتر باشد، برای دیگران مفیدتر خواهد بود.
|
پاسخ خود را به صورت کامل و مفصل ارائه دهید. هرچه پاسخ شما دقیقتر باشد، برای دیگران مفیدتر خواهد بود.
|
||||||
<br><small class="text-muted">میتوانید از دکمههای بالا برای فرمت کردن متن استفاده کنید.</small>
|
<br><span class="text-gray-500">میتوانید از دکمههای بالا برای فرمت کردن متن استفاده کنید.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<!-- پیوست فایل -->
|
||||||
<a href="{{ path('qa_question_show', {'id': question.id}) }}" class="btn btn-outline-secondary">
|
<div>
|
||||||
<i class="fas fa-arrow-right me-2"></i>انصراف
|
<label for="{{ form.attachments.vars.id }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
پیوست فایل
|
||||||
|
<span class="text-gray-500">(اختیاری)</span>
|
||||||
|
</label>
|
||||||
|
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 bg-gray-50 hover:bg-gray-100 transition-colors duration-200">
|
||||||
|
{{ form_widget(form.attachments, {'attr': {'class': 'w-full', 'id': 'file-input'}}) }}
|
||||||
|
{{ form_errors(form.attachments) }}
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<div class="flex items-center justify-center space-x-2 space-x-reverse text-gray-500">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">فایلهای خود را اینجا بکشید یا کلیک کنید</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 mt-2">
|
||||||
|
فرمتهای مجاز: JPG, PNG, GIF, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT
|
||||||
|
<br>حداکثر 3 فایل، هر کدام 4 مگابایت
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- نمایش فایلهای انتخاب شده -->
|
||||||
|
<div id="selected-files" class="mt-4 hidden">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">فایلهای انتخاب شده:</h4>
|
||||||
|
<div id="file-list" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pt-6">
|
||||||
|
<a href="{{ path('qa_question_show', {'id': question.id}) }}"
|
||||||
|
class="inline-flex items-center px-6 py-3 bg-gray-100 text-gray-700 rounded-xl hover:bg-gray-200 transition-all duration-200 font-medium">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
انصراف
|
||||||
</a>
|
</a>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit"
|
||||||
<i class="fas fa-paper-plane me-2"></i>ارسال پاسخ
|
class="inline-flex items-center px-8 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||||
|
</svg>
|
||||||
|
ارسال پاسخ
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -96,30 +142,83 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- راهنمای پاسخ دادن -->
|
<!-- راهنمای پاسخ دادن -->
|
||||||
<div class="card mt-4">
|
<div class="bg-white rounded-2xl shadow-soft mt-8 overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="bg-gradient-to-r from-yellow-50 to-orange-50 px-8 py-6 border-b border-gray-200">
|
||||||
<h5 class="mb-0">
|
<h3 class="text-xl font-bold text-gray-900 flex items-center">
|
||||||
<i class="fas fa-lightbulb me-2"></i>راهنمای پاسخ دادن
|
<svg class="w-6 h-6 text-yellow-600 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</h5>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
||||||
|
</svg>
|
||||||
|
راهنمای پاسخ دادن
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-8">
|
||||||
<div class="row">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div class="col-md-6">
|
<div>
|
||||||
<h6 class="text-success">✅ پاسخ خوب:</h6>
|
<h4 class="text-lg font-semibold text-green-700 mb-4 flex items-center">
|
||||||
<ul class="list-unstyled">
|
<svg class="w-5 h-5 text-green-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<li><i class="fas fa-check text-success me-2"></i>مستقیماً به سوال پاسخ دهید</li>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
<li><i class="fas fa-check text-success me-2"></i>از مثالهای عملی استفاده کنید</li>
|
</svg>
|
||||||
<li><i class="fas fa-check text-success me-2"></i>منابع و لینکهای مفید ارائه دهید</li>
|
پاسخ خوب:
|
||||||
<li><i class="fas fa-check text-success me-2"></i>کد و نمونههای کد ارائه دهید</li>
|
</h4>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-green-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">مستقیماً به سوال پاسخ دهید</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-green-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">از مثالهای عملی استفاده کنید</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-green-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">منابع و لینکهای مفید ارائه دهید</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-green-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">کد و نمونههای کد ارائه دهید</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div>
|
||||||
<h6 class="text-danger">❌ پاسخ ضعیف:</h6>
|
<h4 class="text-lg font-semibold text-red-700 mb-4 flex items-center">
|
||||||
<ul class="list-unstyled">
|
<svg class="w-5 h-5 text-red-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<li><i class="fas fa-times text-danger me-2"></i>پاسخ مبهم و کلی</li>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
<li><i class="fas fa-times text-danger me-2"></i>عدم ارائه راهحل عملی</li>
|
</svg>
|
||||||
<li><i class="fas fa-times text-danger me-2"></i>تکرار پاسخهای قبلی</li>
|
پاسخ ضعیف:
|
||||||
<li><i class="fas fa-times text-danger me-2"></i>پاسخ نامربوط</li>
|
</h4>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-red-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">پاسخ مبهم و کلی</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-red-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">عدم ارائه راهحل عملی</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-red-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">تکرار پاسخهای قبلی</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start">
|
||||||
|
<svg class="w-5 h-5 text-red-600 ml-3 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-gray-700">پاسخ نامربوط</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,60 +226,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-control:focus {
|
|
||||||
border-color: #0d6efd;
|
|
||||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-bottom: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-content {
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-text {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-unstyled li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags .badge {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 0.375rem 0.375rem 0 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-textarea {
|
|
||||||
border-radius: 0 0 0.375rem 0.375rem;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// توابع ادیتور متن
|
// توابع ادیتور متن
|
||||||
function formatText(command) {
|
function formatText(command) {
|
||||||
const textarea = document.querySelector('.editor-textarea');
|
const textarea = document.querySelector('textarea[name="answer[content]"]');
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
const selectedText = textarea.value.substring(start, end);
|
const selectedText = textarea.value.substring(start, end);
|
||||||
|
|
@ -205,7 +257,7 @@ function formatText(command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertList() {
|
function insertList() {
|
||||||
const textarea = document.querySelector('.editor-textarea');
|
const textarea = document.querySelector('textarea[name="answer[content]"]');
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
const selectedText = textarea.value.substring(start, end);
|
const selectedText = textarea.value.substring(start, end);
|
||||||
|
|
@ -214,5 +266,193 @@ function insertList() {
|
||||||
textarea.value = textarea.value.substring(0, start) + listText + textarea.value.substring(end);
|
textarea.value = textarea.value.substring(0, start) + listText + textarea.value.substring(end);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// مدیریت فایلها
|
||||||
|
class FileManager {
|
||||||
|
constructor() {
|
||||||
|
this.maxFiles = 3;
|
||||||
|
this.maxFileSize = 4 * 1024 * 1024; // 4MB
|
||||||
|
this.allowedTypes = [
|
||||||
|
'image/jpeg', 'image/png', 'image/gif',
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
];
|
||||||
|
this.selectedFiles = [];
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fileInput = document.getElementById('file-input');
|
||||||
|
this.selectedFilesDiv = document.getElementById('selected-files');
|
||||||
|
this.fileListDiv = document.getElementById('file-list');
|
||||||
|
|
||||||
|
if (!this.fileInput) return;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||||
|
|
||||||
|
// Drag and drop
|
||||||
|
const dropZone = this.fileInput.closest('.border-dashed');
|
||||||
|
if (dropZone) {
|
||||||
|
dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||||||
|
dropZone.addEventListener('drop', (e) => this.handleDrop(e));
|
||||||
|
dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect(event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
this.processFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.add('bg-blue-50', 'border-blue-400');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('bg-blue-50', 'border-blue-400');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('bg-blue-50', 'border-blue-400');
|
||||||
|
|
||||||
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
this.processFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
processFiles(files) {
|
||||||
|
files.forEach(file => {
|
||||||
|
if (this.validateFile(file)) {
|
||||||
|
this.addFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updateFileInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFile(file) {
|
||||||
|
// بررسی تعداد فایلها
|
||||||
|
if (this.selectedFiles.length >= this.maxFiles) {
|
||||||
|
alert(`حداکثر ${this.maxFiles} فایل میتوانید انتخاب کنید`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی نوع فایل
|
||||||
|
if (!this.allowedTypes.includes(file.type)) {
|
||||||
|
alert('فرمت فایل مجاز نیست');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی حجم فایل
|
||||||
|
if (file.size > this.maxFileSize) {
|
||||||
|
alert('حجم فایل نمیتواند بیش از 4 مگابایت باشد');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file) {
|
||||||
|
const fileId = Date.now() + Math.random();
|
||||||
|
this.selectedFiles.push({
|
||||||
|
id: fileId,
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(fileId) {
|
||||||
|
this.selectedFiles = this.selectedFiles.filter(f => f.id !== fileId);
|
||||||
|
this.renderFiles();
|
||||||
|
this.updateFileInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFiles() {
|
||||||
|
if (this.selectedFiles.length === 0) {
|
||||||
|
this.selectedFilesDiv.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedFilesDiv.classList.remove('hidden');
|
||||||
|
this.fileListDiv.innerHTML = '';
|
||||||
|
|
||||||
|
this.selectedFiles.forEach(fileData => {
|
||||||
|
const fileElement = document.createElement('div');
|
||||||
|
fileElement.className = 'flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200';
|
||||||
|
fileElement.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-3 space-x-reverse">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
${this.getFileIcon(fileData.type)}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate">${fileData.name}</p>
|
||||||
|
<p class="text-xs text-gray-500">${this.formatFileSize(fileData.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
class="flex-shrink-0 text-red-600 hover:text-red-800 transition-colors duration-200"
|
||||||
|
onclick="fileManager.removeFile(${fileData.id})">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.fileListDiv.appendChild(fileElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(type) {
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
return '<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21,15 16,10 5,21"></polyline></svg>';
|
||||||
|
} else if (type === 'application/pdf') {
|
||||||
|
return '<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
} else if (type.includes('word') || type.includes('document')) {
|
||||||
|
return '<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
} else {
|
||||||
|
return '<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileInput() {
|
||||||
|
// ایجاد DataTransfer object جدید
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
this.selectedFiles.forEach(fileData => {
|
||||||
|
dt.items.add(fileData.file);
|
||||||
|
});
|
||||||
|
this.fileInput.files = dt.files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// متغیرهای سراسری
|
||||||
|
let fileManager;
|
||||||
|
|
||||||
|
// مقداردهی اولیه
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
fileManager = new FileManager();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<!-- فرم ایجاد سوال -->
|
<!-- فرم ایجاد سوال -->
|
||||||
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
|
||||||
<div class="p-8">
|
<div class="p-8">
|
||||||
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-8'}}) }}
|
{{ form_start(form, {'attr': {'novalidate': 'novalidate', 'class': 'space-y-8', 'data-turbo': 'false'}}) }}
|
||||||
|
|
||||||
<!-- عنوان سوال -->
|
<!-- عنوان سوال -->
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -175,6 +175,50 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- اطلاعرسانی ایمیل -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center space-x-3 space-x-reverse">
|
||||||
|
{{ form_widget(form.notifyOnAnswer, {'attr': {'class': 'form-check-input'}}) }}
|
||||||
|
<label for="{{ form.notifyOnAnswer.vars.id }}" class="text-sm font-medium text-gray-700">
|
||||||
|
اطلاعرسانی ایمیل هنگام دریافت پاسخ
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
|
در صورت انتخاب این گزینه، هر زمان که برای سوال شما پاسخی ارسال شود، از طریق ایمیل مطلع خواهید شد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- پیوست فایل -->
|
||||||
|
<div>
|
||||||
|
<label for="{{ form.attachments.vars.id }}" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
پیوست فایل
|
||||||
|
<span class="text-gray-500">(اختیاری)</span>
|
||||||
|
</label>
|
||||||
|
<div class="border-2 border-dashed border-gray-300 rounded-xl p-6 bg-gray-50 hover:bg-gray-100 transition-colors duration-200">
|
||||||
|
{{ form_widget(form.attachments, {'attr': {'class': 'w-full', 'id': 'file-input'}}) }}
|
||||||
|
{{ form_errors(form.attachments) }}
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<div class="flex items-center justify-center space-x-2 space-x-reverse text-gray-500">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">فایلهای خود را اینجا بکشید یا کلیک کنید</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 mt-2">
|
||||||
|
فرمتهای مجاز: JPG, PNG, GIF, PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT
|
||||||
|
<br>حداکثر 3 فایل، هر کدام 4 مگابایت
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- نمایش فایلهای انتخاب شده -->
|
||||||
|
<div id="selected-files" class="mt-4 hidden">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">فایلهای انتخاب شده:</h4>
|
||||||
|
<div id="file-list" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- دکمههای فرم -->
|
<!-- دکمههای فرم -->
|
||||||
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
|
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
|
||||||
<a href="{{ path('qa_index') }}"
|
<a href="{{ path('qa_index') }}"
|
||||||
|
|
@ -293,7 +337,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// سیستم مدیریت تگها
|
// سیستم مدیریت تگها
|
||||||
class TagManager {
|
if (typeof window.QuestionFormClasses === 'undefined') {
|
||||||
|
window.QuestionFormClasses = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.QuestionFormClasses.TagManager === 'undefined') {
|
||||||
|
window.QuestionFormClasses.TagManager = class TagManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.selectedTags = [];
|
this.selectedTags = [];
|
||||||
this.maxTags = 5;
|
this.maxTags = 5;
|
||||||
|
|
@ -320,19 +369,17 @@ class TagManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
|
// حذف event listener های قبلی
|
||||||
|
this.removeEventListeners();
|
||||||
|
|
||||||
// افزودن تگ با دکمه
|
// افزودن تگ با دکمه
|
||||||
this.addTagBtn.addEventListener('click', () => this.addTagFromInput());
|
this.addTagBtn.addEventListener('click', this.handleAddTagClick.bind(this));
|
||||||
|
|
||||||
// افزودن تگ با Enter
|
// افزودن تگ با Enter
|
||||||
this.tagInput.addEventListener('keypress', (e) => {
|
this.tagInput.addEventListener('keypress', this.handleKeyPress.bind(this));
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.addTagFromInput();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// جستجوی تگها
|
// جستجوی تگها
|
||||||
this.tagInput.addEventListener('input', () => this.filterSuggestions());
|
this.tagInput.addEventListener('input', this.handleInputChange.bind(this));
|
||||||
|
|
||||||
// کلیک روی تگهای پیشنهادی
|
// کلیک روی تگهای پیشنهادی
|
||||||
this.tagSuggestions.forEach(suggestion => {
|
this.tagSuggestions.forEach(suggestion => {
|
||||||
|
|
@ -341,10 +388,42 @@ class TagManager {
|
||||||
|
|
||||||
// نمایش همه تگها
|
// نمایش همه تگها
|
||||||
if (this.showAllTagsBtn) {
|
if (this.showAllTagsBtn) {
|
||||||
this.showAllTagsBtn.addEventListener('click', () => this.showAllTags());
|
this.showAllTagsBtn.addEventListener('click', this.handleShowAllTags.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeEventListeners() {
|
||||||
|
if (this.addTagBtn) {
|
||||||
|
this.addTagBtn.removeEventListener('click', this.handleAddTagClick);
|
||||||
|
}
|
||||||
|
if (this.tagInput) {
|
||||||
|
this.tagInput.removeEventListener('keypress', this.handleKeyPress);
|
||||||
|
this.tagInput.removeEventListener('input', this.handleInputChange);
|
||||||
|
}
|
||||||
|
if (this.showAllTagsBtn) {
|
||||||
|
this.showAllTagsBtn.removeEventListener('click', this.handleShowAllTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddTagClick() {
|
||||||
|
this.addTagFromInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.addTagFromInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange() {
|
||||||
|
this.filterSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleShowAllTags() {
|
||||||
|
this.showAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
addTag(tagId, tagName) {
|
addTag(tagId, tagName) {
|
||||||
// بررسی محدودیت تعداد تگها
|
// بررسی محدودیت تعداد تگها
|
||||||
if (this.selectedTags.length >= this.maxTags) {
|
if (this.selectedTags.length >= this.maxTags) {
|
||||||
|
|
@ -363,13 +442,19 @@ class TagManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedTags.push({ id: tagId, name: tagName });
|
this.selectedTags.push({ id: tagId, name: tagName });
|
||||||
|
|
||||||
this.updateTagCount();
|
this.updateTagCount();
|
||||||
this.renderSelectedTags();
|
this.renderSelectedTags();
|
||||||
this.updateHiddenInput();
|
this.updateHiddenInput();
|
||||||
this.updateSuggestionState(tagId, true);
|
this.updateSuggestionState(tagId, true);
|
||||||
|
|
||||||
if (window.notification) {
|
// نمایش پیام موفقیت فقط یک بار
|
||||||
|
if (window.notification && !this._showingMessage) {
|
||||||
|
this._showingMessage = true;
|
||||||
window.notification.success(`تگ "${tagName}" اضافه شد`);
|
window.notification.success(`تگ "${tagName}" اضافه شد`);
|
||||||
|
setTimeout(() => {
|
||||||
|
this._showingMessage = false;
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -595,17 +680,54 @@ class TagManager {
|
||||||
|
|
||||||
validateTags() {
|
validateTags() {
|
||||||
if (this.selectedTags.length < this.minTags) {
|
if (this.selectedTags.length < this.minTags) {
|
||||||
if (window.notification) {
|
// نمایش پیام خطای زیبا
|
||||||
window.notification.error(`حداقل ${this.minTags} تگ باید انتخاب کنید`);
|
this.showValidationError(`حداقل ${this.minTags} تگ باید انتخاب کنید`);
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showValidationError(message) {
|
||||||
|
// حذف پیام خطای قبلی اگر وجود دارد
|
||||||
|
const existingError = document.querySelector('.tag-validation-error');
|
||||||
|
if (existingError) {
|
||||||
|
existingError.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ایجاد پیام خطای زیبا
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'tag-validation-error mt-4 p-4 bg-red-50 border border-red-200 rounded-lg';
|
||||||
|
errorDiv.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-5 h-5 text-red-500 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-red-700 font-medium">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// اضافه کردن پیام خطا به فرم
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.parentNode.insertBefore(errorDiv, submitButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// حذف خودکار پیام خطا بعد از 5 ثانیه
|
||||||
|
setTimeout(() => {
|
||||||
|
if (errorDiv.parentNode) {
|
||||||
|
errorDiv.remove();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ادیتور متن
|
// ادیتور متن
|
||||||
class TextEditor {
|
if (typeof window.QuestionFormClasses.TextEditor === 'undefined') {
|
||||||
|
window.QuestionFormClasses.TextEditor = class TextEditor {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.textarea = document.querySelector('textarea[name="question[content]"]');
|
this.textarea = document.querySelector('textarea[name="question[content]"]');
|
||||||
}
|
}
|
||||||
|
|
@ -648,21 +770,216 @@ class TextEditor {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// متغیرهای سراسری
|
// مدیریت فایلها
|
||||||
let tagManager;
|
if (typeof window.QuestionFormClasses.FileManager === 'undefined') {
|
||||||
let textEditor;
|
window.QuestionFormClasses.FileManager = class FileManager {
|
||||||
|
constructor() {
|
||||||
|
this.maxFiles = 3;
|
||||||
|
this.maxFileSize = 4 * 1024 * 1024; // 4MB
|
||||||
|
this.allowedTypes = [
|
||||||
|
'image/jpeg', 'image/png', 'image/gif',
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'text/plain'
|
||||||
|
];
|
||||||
|
this.selectedFiles = [];
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fileInput = document.getElementById('file-input');
|
||||||
|
this.selectedFilesDiv = document.getElementById('selected-files');
|
||||||
|
this.fileListDiv = document.getElementById('file-list');
|
||||||
|
|
||||||
|
if (!this.fileInput) return;
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||||
|
|
||||||
|
// Drag and drop
|
||||||
|
const dropZone = this.fileInput.closest('.border-dashed');
|
||||||
|
if (dropZone) {
|
||||||
|
dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||||||
|
dropZone.addEventListener('drop', (e) => this.handleDrop(e));
|
||||||
|
dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect(event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
this.processFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.add('bg-blue-50', 'border-blue-400');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('bg-blue-50', 'border-blue-400');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('bg-blue-50', 'border-blue-400');
|
||||||
|
|
||||||
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
this.processFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
processFiles(files) {
|
||||||
|
files.forEach(file => {
|
||||||
|
if (this.validateFile(file)) {
|
||||||
|
this.addFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.updateFileInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateFile(file) {
|
||||||
|
// بررسی تعداد فایلها
|
||||||
|
if (this.selectedFiles.length >= this.maxFiles) {
|
||||||
|
if (window.notification) {
|
||||||
|
window.notification.warning(`حداکثر ${this.maxFiles} فایل میتوانید انتخاب کنید`);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی نوع فایل
|
||||||
|
if (!this.allowedTypes.includes(file.type)) {
|
||||||
|
if (window.notification) {
|
||||||
|
window.notification.error('فرمت فایل مجاز نیست');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی حجم فایل
|
||||||
|
if (file.size > this.maxFileSize) {
|
||||||
|
if (window.notification) {
|
||||||
|
window.notification.error('حجم فایل نمیتواند بیش از 4 مگابایت باشد');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile(file) {
|
||||||
|
const fileId = Date.now() + Math.random();
|
||||||
|
this.selectedFiles.push({
|
||||||
|
id: fileId,
|
||||||
|
file: file,
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(fileId) {
|
||||||
|
this.selectedFiles = this.selectedFiles.filter(f => f.id !== fileId);
|
||||||
|
this.renderFiles();
|
||||||
|
this.updateFileInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFiles() {
|
||||||
|
if (this.selectedFiles.length === 0) {
|
||||||
|
this.selectedFilesDiv.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedFilesDiv.classList.remove('hidden');
|
||||||
|
this.fileListDiv.innerHTML = '';
|
||||||
|
|
||||||
|
this.selectedFiles.forEach(fileData => {
|
||||||
|
const fileElement = document.createElement('div');
|
||||||
|
fileElement.className = 'flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200';
|
||||||
|
fileElement.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-3 space-x-reverse">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
${this.getFileIcon(fileData.type)}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate">${fileData.name}</p>
|
||||||
|
<p class="text-xs text-gray-500">${this.formatFileSize(fileData.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
class="flex-shrink-0 text-red-600 hover:text-red-800 transition-colors duration-200"
|
||||||
|
onclick="fileManager.removeFile(${fileData.id})">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
this.fileListDiv.appendChild(fileElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(type) {
|
||||||
|
if (type.startsWith('image/')) {
|
||||||
|
return '<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21,15 16,10 5,21"></polyline></svg>';
|
||||||
|
} else if (type === 'application/pdf') {
|
||||||
|
return '<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
} else if (type.includes('word') || type.includes('document')) {
|
||||||
|
return '<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
} else {
|
||||||
|
return '<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path></svg>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileInput() {
|
||||||
|
// ایجاد DataTransfer object جدید
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
this.selectedFiles.forEach(fileData => {
|
||||||
|
dt.items.add(fileData.file);
|
||||||
|
});
|
||||||
|
this.fileInput.files = dt.files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// متغیرهای سراسری - استفاده از window object
|
||||||
|
if (!window.questionFormManager) {
|
||||||
|
window.questionFormManager = {
|
||||||
|
tagManager: null,
|
||||||
|
textEditor: null,
|
||||||
|
fileManager: null,
|
||||||
|
initialized: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// توابع سراسری برای HTML
|
// توابع سراسری برای HTML
|
||||||
window.formatText = function(command) {
|
window.formatText = function(command) {
|
||||||
if (textEditor) {
|
if (window.questionFormManager.textEditor) {
|
||||||
textEditor.formatText(command);
|
window.questionFormManager.textEditor.formatText(command);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.insertList = function() {
|
window.insertList = function() {
|
||||||
if (textEditor) {
|
if (window.questionFormManager.textEditor) {
|
||||||
textEditor.insertList();
|
window.questionFormManager.textEditor.insertList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -673,23 +990,67 @@ function initializeQuestionForm() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tagManager = new TagManager();
|
// جلوگیری از مقداردهی مجدد
|
||||||
textEditor = new TextEditor();
|
if (window.questionFormManager.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ایجاد instance های جدید
|
||||||
|
if (typeof window.QuestionFormClasses.TagManager !== 'undefined') {
|
||||||
|
window.questionFormManager.tagManager = new window.QuestionFormClasses.TagManager();
|
||||||
|
}
|
||||||
|
if (typeof window.QuestionFormClasses.TextEditor !== 'undefined') {
|
||||||
|
window.questionFormManager.textEditor = new window.QuestionFormClasses.TextEditor();
|
||||||
|
}
|
||||||
|
if (typeof window.QuestionFormClasses.FileManager !== 'undefined') {
|
||||||
|
window.questionFormManager.fileManager = new window.QuestionFormClasses.FileManager();
|
||||||
|
}
|
||||||
|
|
||||||
// اعتبارسنجی فرم
|
// اعتبارسنجی فرم
|
||||||
const form = document.querySelector('form');
|
const form = document.querySelector('form');
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener('submit', function(e) {
|
// حذف event listener قبلی اگر وجود دارد
|
||||||
if (!tagManager.validateTags()) {
|
form.removeEventListener('submit', handleFormSubmit);
|
||||||
e.preventDefault();
|
form.addEventListener('submit', handleFormSubmit, false);
|
||||||
return false;
|
|
||||||
}
|
// اطمینان از اینکه form دارای data-turbo="false" است
|
||||||
});
|
form.setAttribute('data-turbo', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.questionFormManager.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit(e) {
|
||||||
|
// اطمینان از اینکه تگها به فرم اضافه شدهاند
|
||||||
|
if (window.questionFormManager.tagManager) {
|
||||||
|
window.questionFormManager.tagManager.updateHiddenInput();
|
||||||
|
|
||||||
|
// اعتبارسنجی تگها
|
||||||
|
if (!window.questionFormManager.tagManager.validateTags()) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// غیرفعال کردن Turbo برای این فرم
|
||||||
|
const form = e.target;
|
||||||
|
if (form) {
|
||||||
|
form.setAttribute('data-turbo', 'false');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// اجرا در بارگذاری صفحه
|
// اجرا در بارگذاری صفحه
|
||||||
document.addEventListener('DOMContentLoaded', initializeQuestionForm);
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.addEventListener('turbo:load', initializeQuestionForm);
|
// Reset initialization state
|
||||||
|
window.questionFormManager.initialized = false;
|
||||||
|
initializeQuestionForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('turbo:load', function() {
|
||||||
|
// Reset initialization state for Turbo navigation
|
||||||
|
window.questionFormManager.initialized = false;
|
||||||
|
initializeQuestionForm();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ question.createdAt|date('Y/m/d H:i') }}</span>
|
<span>{{ question.createdAt|date('U')|jdate('Y/m/d H:i') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1 space-x-reverse">
|
<div class="flex items-center space-x-1 space-x-reverse">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,32 @@
|
||||||
<div class="prose prose-lg max-w-none text-gray-800 leading-relaxed mb-6">
|
<div class="prose prose-lg max-w-none text-gray-800 leading-relaxed mb-6">
|
||||||
{{ question.content|markdown|raw }}
|
{{ question.content|markdown|raw }}
|
||||||
</div>
|
</div>
|
||||||
|
{% if question.attachments is defined and question.attachments|length > 0 %}
|
||||||
|
<div class="mt-4 border-t border-gray-200 pt-4">
|
||||||
|
<h3 class="text-sm font-medium text-gray-700 mb-3">پیوستها</h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{% for attachment in question.attachments %}
|
||||||
|
<a href="/uploads/attachments/{{ attachment.filename }}" target="_blank" class="block group">
|
||||||
|
<div class="flex items-center space-x-3 space-x-reverse p-3 bg-gray-50 rounded-xl border border-gray-200 group-hover:bg-gray-100 transition-colors duration-200">
|
||||||
|
{% if attachment.isImage() %}
|
||||||
|
<img src="/uploads/attachments/{{ attachment.filename }}" alt="{{ attachment.originalFilename }}" class="w-12 h-12 rounded-lg object-cover">
|
||||||
|
{% else %}
|
||||||
|
<div class="w-12 h-12 flex items-center justify-center rounded-lg bg-white border border-gray-200">
|
||||||
|
<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate">{{ attachment.originalFilename }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ attachment.getFormattedSize() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-4 pt-6 border-t border-gray-200">
|
<div class="flex flex-wrap items-center justify-between gap-4 pt-6 border-t border-gray-200">
|
||||||
<!-- تگها -->
|
<!-- تگها -->
|
||||||
|
|
@ -76,7 +102,7 @@
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ question.createdAt|date('Y/m/d H:i') }}</span>
|
<span>{{ question.createdAt|date('U')|jdate('Y/m/d H:i') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-1 space-x-reverse">
|
<div class="flex items-center space-x-1 space-x-reverse">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -182,6 +208,32 @@
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if answer.attachments is defined and answer.attachments|length > 0 %}
|
||||||
|
<div class="mt-4 border-t border-gray-200 pt-4">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-3">پیوستهای پاسخ</h4>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{% for attachment in answer.attachments %}
|
||||||
|
<a href="/uploads/attachments/{{ attachment.filename }}" target="_blank" class="block group">
|
||||||
|
<div class="flex items-center space-x-3 space-x-reverse p-3 bg-gray-50 rounded-xl border border-gray-200 group-hover:bg-gray-100 transition-colors duration-200">
|
||||||
|
{% if attachment.isImage() %}
|
||||||
|
<img src="/uploads/attachments/{{ attachment.filename }}" alt="{{ attachment.originalFilename }}" class="w-12 h-12 rounded-lg object-cover">
|
||||||
|
{% else %}
|
||||||
|
<div class="w-12 h-12 flex items-center justify-center rounded-lg bg-white border border-gray-200">
|
||||||
|
<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-900 truncate">{{ attachment.originalFilename }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ attachment.getFormattedSize() }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
||||||
<div class="flex items-center space-x-4 space-x-reverse text-sm text-gray-500">
|
<div class="flex items-center space-x-4 space-x-reverse text-sm text-gray-500">
|
||||||
|
|
@ -195,7 +247,7 @@
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ answer.createdAt|date('Y/m/d H:i') }}</span>
|
<span>{{ answer.createdAt|date('U')|jdate('Y/m/d H:i') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,182 +3,248 @@
|
||||||
{% block title %}سوالات تگ {{ tag.name }} - پرسش و پاسخ{% endblock %}
|
{% block title %}سوالات تگ {{ tag.name }} - پرسش و پاسخ{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container my-4">
|
<main class="min-h-screen bg-gray-50">
|
||||||
<div class="row">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="col-12">
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<!-- هدر -->
|
||||||
<div>
|
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-2xl p-8 mb-8 text-white">
|
||||||
<h1 class="text-primary fw-bold mb-2">
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
<i class="fas fa-tag me-2"></i>سوالات تگ "{{ tag.name }}"
|
<div>
|
||||||
</h1>
|
<h1 class="text-3xl font-bold mb-2 flex items-center">
|
||||||
{% if tag.description %}
|
<svg class="w-8 h-8 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<p class="text-muted mb-0">{{ tag.description }}</p>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
{% endif %}
|
</svg>
|
||||||
|
سوالات تگ "{{ tag.name }}"
|
||||||
|
</h1>
|
||||||
|
{% if tag.description %}
|
||||||
|
<p class="text-blue-100 text-lg">{{ tag.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<a href="{{ path('qa_index') }}"
|
||||||
|
class="mt-4 md:mt-0 inline-flex items-center px-6 py-3 bg-white bg-opacity-20 text-white rounded-xl hover:bg-opacity-30 transition-all duration-200 font-medium backdrop-blur-sm">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
بازگشت به همه سوالات
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ path('qa_index') }}" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-arrow-right me-2"></i>بازگشت به همه سوالات
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
<div class="col-lg-8">
|
<div class="lg:col-span-2">
|
||||||
{% if questions is empty %}
|
{% if questions is empty %}
|
||||||
<div class="card">
|
<div class="bg-white rounded-2xl shadow-soft p-12 text-center">
|
||||||
<div class="card-body text-center py-5">
|
<div class="max-w-md mx-auto">
|
||||||
<i class="fas fa-question-circle fa-3x text-muted mb-3"></i>
|
<svg class="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<h4 class="text-muted">سوالی یافت نشد</h4>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
<p class="text-muted">هنوز سوالی با تگ "{{ tag.name }}" وجود ندارد.</p>
|
</svg>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">سوالی یافت نشد</h3>
|
||||||
|
<p class="text-gray-600 mb-6">هنوز سوالی با تگ "{{ tag.name }}" وجود ندارد.</p>
|
||||||
{% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
|
{% if app.user and 'ROLE_CUSTOMER' in app.user.roles %}
|
||||||
<a href="{{ path('qa_ask') }}" class="btn btn-primary">اولین سوال را بپرسید</a>
|
<a href="{{ path('qa_ask') }}"
|
||||||
|
class="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all duration-200 font-medium shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||||
|
</svg>
|
||||||
|
اولین سوال را بپرسید
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for question in questions %}
|
<div class="space-y-6">
|
||||||
<div class="card mb-3 question-card">
|
{% for question in questions %}
|
||||||
<div class="card-body">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden qa-card-hover">
|
||||||
<div class="row">
|
<div class="p-6">
|
||||||
<div class="col-2 col-md-1 text-center">
|
<div class="flex gap-6">
|
||||||
<div class="d-flex flex-column align-items-center">
|
<!-- آمار رای و پاسخ -->
|
||||||
<div class="vote-count {{ question.votes > 0 ? 'text-success' : (question.votes < 0 ? 'text-danger' : 'text-muted') }}">
|
<div class="flex-shrink-0">
|
||||||
{{ question.votes }}
|
<div class="flex flex-col items-center space-y-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold {{ question.votes > 0 ? 'text-green-600' : (question.votes < 0 ? 'text-red-600' : 'text-gray-500') }}">
|
||||||
|
{{ question.votes }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">رای</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-2xl font-bold {{ question.answers|length > 0 ? 'text-green-600' : 'text-gray-500' }}">
|
||||||
|
{{ question.answers|length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">پاسخ</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">رای</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-2 col-md-1 text-center">
|
|
||||||
<div class="d-flex flex-column align-items-center">
|
|
||||||
<div class="answer-count {{ question.answers|length > 0 ? 'text-success' : 'text-muted' }}">
|
|
||||||
{{ question.answers|length }}
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">پاسخ</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-8 col-md-10">
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<a href="{{ path('qa_question_show', {'id': question.id}) }}"
|
|
||||||
class="text-decoration-none text-dark">
|
|
||||||
{{ question.title }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
{% if question.isSolved %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check me-1"></i>حل شده
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="card-text text-muted mb-2">
|
<!-- محتوای سوال -->
|
||||||
{% set content = question.content|markdown|raw %}
|
<div class="flex-1 min-w-0">
|
||||||
{% set plainText = content|striptags %}
|
<div class="flex items-start justify-between mb-3">
|
||||||
{% if plainText|length > 200 %}
|
<h3 class="text-lg font-semibold text-gray-900 leading-tight">
|
||||||
{{ plainText|slice(0, 200) }}...
|
<a href="{{ path('qa_question_show', {'id': question.id}) }}"
|
||||||
{% else %}
|
class="hover:text-blue-600 transition-colors duration-200">
|
||||||
{{ plainText }}
|
{{ question.title }}
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div class="tags">
|
|
||||||
{% for tagRelation in question.tagRelations %}
|
|
||||||
<a href="{{ path('qa_tag_questions', {'name': tagRelation.tag.name}) }}"
|
|
||||||
class="badge bg-light text-dark text-decoration-none me-1 {{ tagRelation.tag.name == tag.name ? 'bg-primary text-white' : '' }}">
|
|
||||||
{{ tagRelation.tag.name }}
|
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
</h3>
|
||||||
|
{% if question.isSolved %}
|
||||||
|
<span class="inline-flex items-center px-3 py-1 bg-green-100 text-green-800 text-sm font-medium rounded-full">
|
||||||
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
حل شده
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small">
|
|
||||||
<i class="fas fa-user me-1"></i>{{ question.author.name }}
|
<div class="prose prose-sm max-w-none text-gray-600 mb-4">
|
||||||
<i class="fas fa-clock ms-3 me-1"></i>{{ question.createdAt|date('Y/m/d H:i') }}
|
{% set content = question.content|markdown|raw %}
|
||||||
<i class="fas fa-eye ms-3 me-1"></i>{{ question.views }}
|
{% set plainText = content|striptags %}
|
||||||
|
{% if plainText|length > 200 %}
|
||||||
|
{{ plainText|slice(0, 200) }}...
|
||||||
|
{% else %}
|
||||||
|
{{ plainText }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{% for tagRelation in question.tagRelations %}
|
||||||
|
<a href="{{ path('qa_tag_questions', {'name': tagRelation.tag.name}) }}"
|
||||||
|
class="inline-flex items-center px-3 py-1 text-sm font-medium rounded-full transition-all duration-200 {{ tagRelation.tag.name == tag.name ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||||
|
{{ tagRelation.tag.name }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4 space-x-reverse text-sm text-gray-500">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ question.author.name }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ question.createdAt|date('U')|jdate('Y/m/d H:i') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ question.views }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
|
|
||||||
<!-- صفحهبندی -->
|
<!-- صفحهبندی -->
|
||||||
{% if totalPages > 1 %}
|
{% if totalPages > 1 %}
|
||||||
<nav aria-label="صفحهبندی سوالات">
|
<div class="mt-8">
|
||||||
<ul class="pagination justify-content-center">
|
<nav class="flex justify-center" aria-label="صفحهبندی سوالات">
|
||||||
{% if currentPage > 1 %}
|
<div class="flex items-center space-x-2 space-x-reverse">
|
||||||
<li class="page-item">
|
{% if currentPage > 1 %}
|
||||||
<a class="page-link" href="?page={{ currentPage - 1 }}">قبلی</a>
|
<a href="?page={{ currentPage - 1 }}"
|
||||||
</li>
|
class="flex items-center space-x-2 space-x-reverse px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-all duration-200">
|
||||||
{% endif %}
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||||
{% for page in 1..totalPages %}
|
</svg>
|
||||||
{% if page == currentPage %}
|
<span>قبلی</span>
|
||||||
<li class="page-item active">
|
</a>
|
||||||
<span class="page-link">{{ page }}</span>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page }}">{{ page }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if currentPage < totalPages %}
|
<div class="flex items-center space-x-1 space-x-reverse">
|
||||||
<li class="page-item">
|
{% for page in 1..totalPages %}
|
||||||
<a class="page-link" href="?page={{ currentPage + 1 }}">بعدی</a>
|
{% if page == currentPage %}
|
||||||
</li>
|
<span class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg">
|
||||||
{% endif %}
|
{{ page }}
|
||||||
</ul>
|
</span>
|
||||||
</nav>
|
{% elseif page <= currentPage + 2 and page >= currentPage - 2 %}
|
||||||
|
<a href="?page={{ page }}"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-all duration-200">
|
||||||
|
{{ page }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if currentPage < totalPages %}
|
||||||
|
<a href="?page={{ currentPage + 1 }}"
|
||||||
|
class="flex items-center space-x-2 space-x-reverse px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-gray-700 transition-all duration-200">
|
||||||
|
<span>بعدی</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- سایدبار -->
|
<!-- سایدبار -->
|
||||||
<div class="col-lg-4">
|
<div class="space-y-6">
|
||||||
<!-- اطلاعات تگ -->
|
<!-- اطلاعات تگ -->
|
||||||
<div class="card mb-4">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
|
||||||
<h6 class="mb-0">
|
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||||
<i class="fas fa-info-circle me-2"></i>اطلاعات تگ
|
<svg class="w-5 h-5 text-gray-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</h6>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
اطلاعات تگ
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-6">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="flex items-center mb-4">
|
||||||
<span class="badge bg-primary fs-6 me-2">{{ tag.name }}</span>
|
<span class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-lg font-medium rounded-full mr-3">
|
||||||
|
{{ tag.name }}
|
||||||
|
</span>
|
||||||
{% if tag.color %}
|
{% if tag.color %}
|
||||||
<div class="tag-color" style="background-color: {{ tag.color }}; width: 20px; height: 20px; border-radius: 50%; border: 2px solid #dee2e6;"></div>
|
<div class="w-6 h-6 rounded-full border-2 border-gray-300" style="background-color: {{ tag.color }};"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if tag.description %}
|
{% if tag.description %}
|
||||||
<p class="text-muted mb-3">{{ tag.description }}</p>
|
<p class="text-gray-600 mb-6">{{ tag.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row text-center">
|
<div class="grid grid-cols-2 gap-4 text-center">
|
||||||
<div class="col-6">
|
<div class="bg-blue-50 rounded-xl p-4">
|
||||||
<div class="h4 text-primary">{{ questions|length }}</div>
|
<div class="text-2xl font-bold text-blue-600">{{ questions|length }}</div>
|
||||||
<small class="text-muted">سوال</small>
|
<div class="text-sm text-gray-600">سوال</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="bg-green-50 rounded-xl p-4">
|
||||||
<div class="h4 text-success">{{ tag.usageCount }}</div>
|
<div class="text-2xl font-bold text-green-600">{{ tag.usageCount }}</div>
|
||||||
<small class="text-muted">استفاده</small>
|
<div class="text-sm text-gray-600">استفاده</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- تگهای مرتبط -->
|
<!-- تگهای مرتبط -->
|
||||||
<div class="card">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden">
|
||||||
<div class="card-header">
|
<div class="bg-gradient-to-r from-gray-50 to-gray-100 px-6 py-4 border-b border-gray-200">
|
||||||
<h6 class="mb-0">
|
<h3 class="text-lg font-semibold text-gray-900 flex items-center">
|
||||||
<i class="fas fa-tags me-2"></i>تگهای مرتبط
|
<svg class="w-5 h-5 text-gray-600 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</h6>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
تگهای مرتبط
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="p-6">
|
||||||
<p class="text-muted small mb-3">تگهای مشابه که ممکن است برای شما مفید باشند:</p>
|
<p class="text-gray-600 text-sm mb-4">تگهای مشابه که ممکن است برای شما مفید باشند:</p>
|
||||||
<!-- در اینجا میتوانید تگهای مرتبط را نمایش دهید -->
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="{{ path('qa_tags') }}" class="btn btn-outline-primary btn-sm">
|
<a href="{{ path('qa_tags') }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-all duration-200 font-medium">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
مشاهده همه تگها
|
مشاهده همه تگها
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -188,50 +254,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
.question-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
border-left-color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-count, .answer-count {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags .badge {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags .badge:hover {
|
|
||||||
background-color: #0d6efd !important;
|
|
||||||
color: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title a:hover {
|
|
||||||
color: #0d6efd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-color {
|
|
||||||
border: 2px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-preview {
|
|
||||||
line-height: 1.6;
|
|
||||||
max-height: 4.8em; /* حدود 3 خط */
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,76 @@
|
||||||
{% block title %}تگها - پرسش و پاسخ{% endblock %}
|
{% block title %}تگها - پرسش و پاسخ{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container my-4">
|
<main class="min-h-screen bg-gray-50">
|
||||||
<div class="row">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<div class="col-12">
|
<div class="max-w-7xl mx-auto">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<!-- هدر -->
|
||||||
<h1 class="text-primary fw-bold">
|
<div class="bg-gradient-to-r from-purple-600 to-indigo-700 rounded-2xl p-8 mb-8 text-white">
|
||||||
<i class="fas fa-tags me-2"></i>تگها
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
</h1>
|
<div>
|
||||||
<a href="{{ path('qa_index') }}" class="btn btn-outline-primary">
|
<h1 class="text-3xl font-bold mb-2 flex items-center">
|
||||||
<i class="fas fa-arrow-right me-2"></i>بازگشت به سوالات
|
<svg class="w-8 h-8 ml-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</a>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
|
</svg>
|
||||||
|
تگها
|
||||||
|
</h1>
|
||||||
|
<p class="text-purple-100 text-lg">تمام تگهای موجود در سیستم پرسش و پاسخ</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ path('qa_index') }}"
|
||||||
|
class="mt-4 md:mt-0 inline-flex items-center px-6 py-3 bg-white bg-opacity-20 text-white rounded-xl hover:bg-opacity-30 transition-all duration-200 font-medium backdrop-blur-sm">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||||
|
</svg>
|
||||||
|
بازگشت به سوالات
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if tags is empty %}
|
{% if tags is empty %}
|
||||||
<div class="card">
|
<div class="bg-white rounded-2xl shadow-soft p-12 text-center">
|
||||||
<div class="card-body text-center py-5">
|
<div class="max-w-md mx-auto">
|
||||||
<i class="fas fa-tags fa-3x text-muted mb-3"></i>
|
<svg class="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<h4 class="text-muted">تگی یافت نشد</h4>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||||
<p class="text-muted">هنوز تگی در سیستم ثبت نشده است.</p>
|
</svg>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 mb-2">تگی یافت نشد</h3>
|
||||||
|
<p class="text-gray-600">هنوز تگی در سیستم ثبت نشده است.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<div class="col-lg-4 col-md-6 mb-4">
|
<div class="bg-white rounded-2xl shadow-soft overflow-hidden qa-card-hover">
|
||||||
<div class="card tag-card h-100">
|
<div class="p-6">
|
||||||
<div class="card-body">
|
<div class="flex items-start justify-between mb-4">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
<h5 class="card-title mb-0">
|
|
||||||
<a href="{{ path('qa_tag_questions', {'name': tag.name}) }}"
|
|
||||||
class="text-decoration-none text-dark">
|
|
||||||
{{ tag.name }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<span class="badge bg-primary">{{ tag.usageCount }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if tag.description %}
|
|
||||||
<p class="card-text text-muted mb-3">
|
|
||||||
{{ tag.description }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<a href="{{ path('qa_tag_questions', {'name': tag.name}) }}"
|
<a href="{{ path('qa_tag_questions', {'name': tag.name}) }}"
|
||||||
class="btn btn-outline-primary btn-sm">
|
class="hover:text-blue-600 transition-colors duration-200">
|
||||||
<i class="fas fa-eye me-1"></i>مشاهده سوالات
|
{{ tag.name }}
|
||||||
</a>
|
</a>
|
||||||
{% if tag.color %}
|
</h3>
|
||||||
<div class="tag-color" style="background-color: {{ tag.color }}; width: 20px; height: 20px; border-radius: 50%;"></div>
|
<span class="inline-flex items-center px-3 py-1 bg-blue-100 text-blue-800 text-sm font-medium rounded-full">
|
||||||
{% endif %}
|
{{ tag.usageCount }}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tag.description %}
|
||||||
|
<p class="text-gray-600 mb-4 text-sm leading-relaxed">
|
||||||
|
{{ tag.description }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="{{ path('qa_tag_questions', {'name': tag.name}) }}"
|
||||||
|
class="inline-flex items-center px-4 py-2 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-all duration-200 font-medium text-sm">
|
||||||
|
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||||
|
</svg>
|
||||||
|
مشاهده سوالات
|
||||||
|
</a>
|
||||||
|
{% if tag.color %}
|
||||||
|
<div class="w-6 h-6 rounded-full border-2 border-gray-300" style="background-color: {{ tag.color }};"></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -62,26 +81,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
.tag-card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border-left: 4px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
border-left-color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-card .card-title a:hover {
|
|
||||||
color: #0d6efd !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-color {
|
|
||||||
border: 2px solid #dee2e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue