add blog post CRUD

This commit is contained in:
Simon Vieille 2021-03-17 15:57:07 +01:00
parent e0d58349de
commit 4273607594
26 changed files with 798 additions and 92 deletions

BIN
assets/img/blank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

BIN
assets/img/no-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -25,6 +25,7 @@
"symfony/dotenv": "5.2.*",
"symfony/event-dispatcher": "5.2.*",
"symfony/expression-language": "5.2.*",
"symfony/finder": "5.2.*",
"symfony/flex": "^1.3.1",
"symfony/form": "5.2.*",
"symfony/framework-bundle": "5.2.*",

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -68,7 +68,7 @@ class AccountAdminController extends AdminController
} else {
$this->addFlash('success', 'Double authentification activée.');
$entityManager->update($account)->flush()->clear();
$entityManager->update($account);
return $this->redirectToRoute('admin_account');
}
@ -78,7 +78,7 @@ class AccountAdminController extends AdminController
if (!$enable && $account->isTotpAuthenticationEnabled()) {
$account->setTotpSecret(null);
$entityManager->update($account)->flush()->clear();
$entityManager->update($account);
$this->addFlash('success', 'Double authentification désactivée.');
@ -100,7 +100,8 @@ class AccountAdminController extends AdminController
Request $request,
UserRepository $repository,
TokenGeneratorInterface $tokenGenerator,
UserPasswordEncoderInterface $encoder
UserPasswordEncoderInterface $encoder,
EntityManager $entityManager
): Response {
$account = $this->getUser();
$csrfToken = $request->request->get('_csrf_token');
@ -129,10 +130,7 @@ class AccountAdminController extends AdminController
->setConfirmationToken($tokenGenerator->generateToken())
;
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($account);
$entityManager->flush();
$entityManager->clear();
$entityManager->update($account);
$this->addFlash('success', 'Mot de passe modifié !');

View file

@ -71,7 +71,7 @@ class AuthController extends AbstractController
$account->setConfirmationToken($tokenGenerator->generateToken());
$account->setPasswordRequestedAt(new \DateTime('now'));
$entityManager->update($account)->flush()->clear();
$entityManager->update($account);
$eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT);
$emailSent = true;
@ -135,7 +135,7 @@ class AuthController extends AbstractController
->setPasswordRequestedAt(new \DateTime('now'))
;
$entityManager->update($account)->flush()->clear();
$entityManager->update($account);
$passwordUpdated = true;
}

View file

@ -42,7 +42,7 @@ class CategoryAdminController extends AdminController
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity)->flush()->clear();
$entityManager->create($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_blog_category_edit', [
@ -54,6 +54,7 @@ class CategoryAdminController extends AdminController
return $this->render('blog/category_admin/new.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
@ -68,7 +69,7 @@ class CategoryAdminController extends AdminController
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->update($entity)->flush()->clear();
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_blog_category_edit', [
@ -107,7 +108,7 @@ class CategoryAdminController extends AdminController
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity)->flush()->clear();
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimées.');
}

View file

@ -3,6 +3,14 @@
namespace App\Controller\Blog;
use App\Controller\Admin\AdminController;
use App\Entity\Blog\Post as Entity;
use App\Factory\Blog\PostFactory as EntityFactory;
use App\Form\Blog\PostType as EntityType;
use App\Manager\EntityManager;
use App\Repository\Blog\PostRepositoryQuery;
use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery;
use App\Form\FileUploadHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -12,14 +20,104 @@ use Symfony\Component\Routing\Annotation\Route;
class PostAdminController extends AdminController
{
/**
* @Route("/", name="admin_blog_post_index")
* @Route("/{page}", name="admin_blog_post_index", requirements={"page": "\d+"})
*/
public function index(): Response
public function index(int $page = 1, RepositoryQuery $query, Request $request): Response
{
$pager = $query->paginate($page);
return $this->render('blog/post_admin/index.html.twig', [
'pager' => $pager,
]);
}
/**
* @Route("/new", name="admin_blog_post_new")
*/
public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response
{
$entity = $factory->create($this->getUser());
$form = $this->createForm(EntityType::class, $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$entityManager->create($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_blog_post_edit', [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->render('blog/post_admin/new.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/edit/{entity}", name="admin_blog_post_edit")
*/
public function edit(Entity $entity, EntityManager $entityManager, FileUploadHandler $fileUpload, Request $request): Response
{
$form = $this->createForm(EntityType::class, $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$fileUpload->handleForm(
$form->get('image')->getData(),
'uploads/post/'.date('Y'),
function ($filename) use ($entity) {
$entity->setImage('post/'.date('Y').'/'.$filename);
}
);
$entityManager->update($entity);
$this->addFlash('success', 'Donnée enregistrée.');
return $this->redirectToRoute('admin_blog_post_edit', [
'entity' => $entity->getId(),
]);
}
$this->addFlash('warning', 'Le formulaire est invalide.');
}
return $this->render('blog/post_admin/edit.html.twig', [
'form' => $form->createView(),
'entity' => $entity,
]);
}
/**
* @Route("/show/{entity}", name="admin_blog_post_show")
*/
public function show(Entity $entity, PostRepositoryQuery $postQuery): Response
{
return $this->render('blog/post_admin/show.html.twig', [
'entity' => $entity,
]);
}
/**
* @Route("/delete/{entity}", name="admin_blog_post_delete", methods={"DELETE"})
*/
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) {
$entityManager->delete($entity);
$this->addFlash('success', 'Données supprimées.');
}
return $this->redirectToRoute('admin_blog_post_index');
}
public function getSection(): string
{
return 'blog_post';

View file

@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=CategoryRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Category implements EntityInterface
{

View file

@ -12,6 +12,7 @@ use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=PostRepository::class)
* @ORM\HasLifecycleCallbacks
*/
class Post implements EntityInterface
{
@ -153,8 +154,12 @@ class Post implements EntityInterface
return $this->image;
}
public function setImage(?string $image): self
public function setImage(?string $image, bool $force = false): self
{
if (false === $force && null === $image) {
return $this;
}
$this->image = $image;
return $this;

View file

@ -9,11 +9,11 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="`user`")
*/
class User implements UserInterface, TwoFactorInterface, EntityInterface
@ -191,16 +191,6 @@ class User implements UserInterface, TwoFactorInterface, EntityInterface
return null !== $this->getTotpSecret();
}
public function getTotpAuthenticationUsername(): string
{
return $this->getEmail();
}
public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
public function isGoogleAuthenticatorEnabled(): bool
{
return $this->isTotpAuthenticationEnabled();

View file

@ -0,0 +1,78 @@
<?php
namespace App\EventSuscriber;
use App\Event\EntityManager\EntityManagerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use App\Entity\EntityInterface;
use App\Entity\Blog\Post;
use Symfony\Component\Filesystem\Filesystem;
use App\Repository\Blog\PostRepositoryQuery;
use Symfony\Component\Finder\Finder;
/**
* class BlogPostEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class BlogPostEventSubscriber implements EventSubscriberInterface
{
protected Filesystem $filesystem;
protected PostRepositoryQuery $query;
public function __construct(Filesystem $filesystem, PostRepositoryQuery $query)
{
$this->filesystem = $filesystem;
$this->query = $query;
}
public static function getSubscribedEvents()
{
return [
EntityManagerEvent::UPDATE_EVENT => 'onUpdate',
EntityManagerEvent::DELETE_EVENT => 'onDelete',
];
}
public function support(EntityInterface $entity)
{
return $entity instanceof Post;
}
public function onUpdate(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$this->removeOrphanUploads();
}
public function onDelete(EntityManagerEvent $event)
{
if (!$this->support($event->getEntity())) {
return;
}
$this->removeOrphanUploads();
}
protected function removeOrphanUploads()
{
$finder = new Finder();
$finder->files()->in('uploads/post');
foreach ($finder as $file) {
$image = str_replace('uploads/', '', $file->getPathname());
$post = $this->query->create()
->where('.image = :image')
->setParameter(':image', $image)
->findOne();
if (null === $post) {
$this->filesystem->remove($file->getRealPath());
}
}
}
}

View file

@ -6,7 +6,7 @@ use App\Event\EntityManager\EntityManagerEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* class EventListener.
* class EntityManagerEventSubscriber.
*
* @author Simon Vieille <simon@deblan.fr>
*/

View file

@ -0,0 +1,25 @@
<?php
namespace App\Factory\Blog;
use App\Entity\Blog\Post;
use App\Entity\User;
/**
* class PostFactory.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class PostFactory
{
public function create(?User $author = null): Post
{
$entity = new Post();
$entity
->setAuthor($author)
->setStatus(0);
return $entity;
}
}

208
src/Form/Blog/PostType.php Normal file
View file

@ -0,0 +1,208 @@
<?php
namespace App\Form\Blog;
use App\Entity\Blog\Post;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Blog\Category;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Validator\Constraints\Date;
use App\Entity\User;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\Image;
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'title',
TextType::class,
[
'label' => 'Titre',
'required' => true,
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'subTitle',
TextareaType::class,
[
'label' => 'Sous-titre',
'required' => false,
'attr' => [
'rows' => 5,
],
'constraints' => [
],
]
);
$builder->add(
'metaDescription',
TextType::class,
[
'label' => 'Meta description',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'content',
TextareaType::class,
[
'label' => 'Contenu',
'required' => false,
'attr' => [
'data-tinymce' => '',
'rows' => 20,
],
'constraints' => [
],
]
);
$builder->add(
'slug',
TextType::class,
[
'label' => 'Slug',
'required' => false,
'help' => 'Laisser vide pour une génération automatique',
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'categories',
EntityType::class,
[
'label' => 'Catégories',
'class' => Category::class,
'choice_label' => 'title',
'required' => false,
'multiple' => true,
'attr' => [
'data-jschoice' => '',
],
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('a')
->orderBy('a.title', 'ASC')
;
},
'constraints' => [
],
]
);
$builder->add(
'status',
ChoiceType::class,
[
'label' => 'Statut',
'required' => true,
'choices' => [
'Brouillon' => 0,
'Publié' => 1,
],
'attr' => [
],
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'imageCaption',
TextType::class,
[
'label' => 'Titre de l\'image',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
$builder->add(
'publishedAt',
DateType::class,
[
'label' => 'Date de publication',
'required' => false,
'html5' => true,
'widget' => 'single_text',
'attr' => [
],
'constraints' => [
new Date(),
],
]
);
$builder->add(
'author',
EntityType::class,
[
'label' => 'Auteur',
'class' => User::class,
'choice_label' => 'displayName',
'required' => true,
'attr' => [
],
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('u')
->orderBy('u.displayName', 'ASC')
;
},
'constraints' => [
new NotBlank(),
],
]
);
$builder->add(
'image',
FileType::class,
[
'label' => 'Image',
'required' => false,
'data_class' => null,
'attr' => [
],
'constraints' => [
new Image(),
],
]
);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Form;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* class FileUploadHandler.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class FileUploadHandler
{
public function handleForm(?UploadedFile $uploadedFile, string $path, callable $afterUploadCallback): void
{
if (null === $uploadedFile) {
return;
}
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
$filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension();
$uploadedFile->move($path, $filename);
$afterUploadCallback($filename);
}
}

View file

@ -44,6 +44,8 @@ class EntityManager
public function delete(EntityInterface $entity): self
{
$this->entityManager->remove($entity);
$this->flush();
$this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT);
return $this;
@ -66,5 +68,6 @@ class EntityManager
protected function persist(EntityInterface $entity)
{
$this->entityManager->persist($entity);
$this->flush();
}
}

View file

@ -3,8 +3,8 @@
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto">
<h1 class="display-8">{{ entity.title }}</h1>
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.title }}</h1>
</div>
<div class="ml-auto">
@ -38,7 +38,7 @@
</div>
</div>
<form action="{{ app.request.uri }}" method="post" id="form-main">
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">

View file

@ -1,10 +1,10 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.totalItemCount == 0 %}pb-5{% endif %}">
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="d-flex">
<div class="mr-auto">
<h1 class="display-8">Catégories</h1>
<div class="mr-auto w-50">
<h1 class="display-5">Catégories</h1>
</div>
<div class="ml-auto">
@ -35,7 +35,7 @@
<tr data-dblclick="{{ edit }}">
<td class="col-6">
<a href="{{ show }}" class="font-weight-bold">
<a href="{{ show }}" class="font-weight-bold text-body d-block">
{{ item.title }}
</a>
</td>

View file

@ -3,8 +3,8 @@
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto">
<h1 class="display-8">Nouvelle catégorie</h1>
<div class="mr-auto w-50">
<h1 class="display-5">Nouvelle catégorie</h1>
</div>
<div class="ml-auto">
@ -25,7 +25,7 @@
</div>
</div>
<form action="{{ app.request.uri }}" method="post" id="form-main">
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">

View file

@ -3,8 +3,8 @@
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto">
<h1 class="display-8">{{ entity.title }}</h1>
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.title }}</h1>
</div>
<div class="ml-auto">
@ -28,17 +28,17 @@
<div class="col-4 p-3">
<ul class="list-group">
<li class="list-group-item">
<span class="font-weight-bold pb-2">Titre</span><br>
<span class="font-weight-bold pb-2 d-block">Titre</span>
{{ entity.title }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2">Sous-titre</span><br>
<span class="font-weight-bold pb-2 d-block">Sous-titre</span>
{{ entity.subTitle }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2">URL</span><br>
<span class="font-weight-bold pb-2 d-block">URL</span>
{{ absolute_url('/' ~ entity.slug) }}
</li>
@ -61,7 +61,9 @@
{% for item in posts %}
<tr>
<td>
{{ item.post }}
<a href="{{ path('admin_blog_post_show', {entity: item.id}) }}" class="text-body">
{{ item.title }}
</a>
</td>
</tr>
{% else %}

View file

@ -0,0 +1,36 @@
<div class="row">
<div class="col-3 p-3">
<div class="row">
{% for item in ['title', 'subTitle', 'categories', 'metaDescription', 'slug'] %}
<div class="col-12">
{{ form_row(form[item]) }}
</div>
{% endfor %}
</div>
</div>
<div class="col-7 p-3">
<div class="row">
{% for item in ['content'] %}
<div class="col-12">
{{ form_row(form[item]) }}
</div>
{% endfor %}
</div>
</div>
<div class="col-2 p-3">
<div class="row">
{% for item in ['image', 'imageCaption', 'status', 'publishedAt', 'author'] %}
<div class="col-12">
{{ form_row(form[item]) }}
{% if item == 'image' %}
{% if entity.image %}
<img src="{{ asset('uploads/' ~ entity.image) }}" class="img-fluid">
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>

View file

@ -0,0 +1,57 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.title }}</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<a href="{{ path('admin_blog_post_show', {entity: entity.id}) }}" class="btn btn-secondary">
<span class="fa fa-eye pr-1"></span>
Voir
</a>
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
<button type="button" class="btn btn-white dropdown-toggle dropdown-toggle-hide-after" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="font-weight-bold">
⋅⋅⋅
</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<button type="submit" form="form-delete" class="dropdown-item">
Supprimer
</button>
</div>
</div>
</div>
</div>
</div>
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">
{{ include('blog/post_admin/_form.html.twig') }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
<form method="post" action="{{ path('admin_blog_post_delete', {entity: entity.id}) }}" id="form-delete" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ entity.id) }}">
</form>
{% endblock %}

View file

@ -1,32 +1,23 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5">
<div class="bg-light pl-5 pr-4 pt-5 {% if pager.getPaginationData.pageCount < 2 %}pb-5{% endif %}">
<div class="d-flex">
<div class="mr-auto">
<h1 class="display-8">Articles</h1>
<div class="mr-auto w-50">
<h1 class="display-5">Articles</h1>
</div>
<div class="ml-auto">
<button type="button" class="btn btn-primary">Nouveau</button>
<div class="btn-group">
<a href="{{ path('admin_blog_post_new') }}" class="btn btn-primary">
<span class="fa fa-plus pr-1"></span>
Nouveau
</a>
</div>
</div>
</div>
<div class="row">
<ul class="pagination ml-auto">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true">Previous</a>
</li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item active" aria-current="page">
<a class="page-link" href="#">2</a>
</li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</div>
{{ knp_pagination_render(pager) }}
</div>
<table class="table" data-table-fixed>
@ -37,37 +28,76 @@
<th class="col-1">Statut</th>
<th class="col-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for item in range(1, 20) %}
<tr>
<td class="col-6">
<img src="https://fakeimg.deblan.org/50x50/c473c4/fff.png&text=img" class="rounded float-left mr-2">
</thead>
<tbody>
{% for item in pager %}
{% set edit = path('admin_blog_post_edit', {entity: item.id}) %}
{% set show = path('admin_blog_post_show', {entity: item.id}) %}
<span class="font-weight-bold">Titre de l'article {{ item }}</span> <br>
Dans <a href="">Nom de la catégorie</a> par <a href="">Mark</a>
</td>
<td class="col-2">
<span class="btn btn-sm btn-light">
<span class="fa fa-calendar-alt text-black-50 mr-1"></span>
27/03/2021 09:10
</span>
</td>
<td class="col-1">
<button class="btn btn-sm btn-success">
En ligne
</button>
</td>
<td class="col-3 text-right">
<button type="button" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</button>
<button type="button" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
</button>
</td>
</tr>
{% endfor %}
<tr>
<td class="col-6">
{% if item.image %}
{% set image = asset('uploads/' ~ item.image) %}
{% else %}
{% set image = asset('build/images/no-image.png') %}
{% endif %}
<img src="{{ asset('build/images/blank.png') }}" style="background: url({{ image }}); background-size: cover" class="rounded float-left mr-2">
<a href="{{ show }}" class="font-weight-bold text-body d-block">
{{ item.title }}
</a>
{% set categories = [] %}
{% for category in item.categories %}
{% set url = path('admin_blog_category_show', {entity: category.id}) %}
{% set categories = categories|merge(['<a href="' ~ url ~ '">' ~ category.title ~ '</a>']) %}
{% endfor %}
Dans {{ categories|join(', ')|raw }} par <a href="">{{ item.author.displayName }}</a>
</td>
<td class="col-2">
<span class="btn btn-sm btn-light">
<span class="fa fa-calendar-alt text-black-50 mr-1"></span>
{{ item.updatedAt|date('d/m/Y H:i') }}
</span>
</td>
<td class="col-1">
{% set map = {
0: ['warning', 'Brouillon'],
1: ['success', 'Publié'],
} %}
<button class="btn btn-sm btn-{{ map[item.status].0 }}">
{{ map[item.status].1 }}
</button>
</td>
<td class="col-3 text-right">
<a href="{{ edit }}" class="btn btn-sm btn-primary mr-1">
<span class="fa fa-edit"></span>
</a>
<button type="submit" form="form-delete-{{ item.id }}" class="btn btn-sm btn-danger">
<span class="fa fa-trash"></span>
</button>
<form method="post" action="{{ path('admin_blog_post_delete', {entity: item.id}) }}" id="form-delete-{{ item.id }}" data-form-confirm>
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ item.id) }}">
</form>
</td>
</tr>
{% else %}
<tr>
<td class="col-12 text-center p-4 text-black-50">
<div class="display-1">
<span class="fa fa-search"></span>
</div>
<div class="display-5 mt-3">
Aucun résultat
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">Nouvel article</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-light">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<button type="submit" form="form-main" class="btn btn-primary">
<span class="fa fa-save pr-1"></span>
Enregistrer
</button>
</div>
</div>
</div>
</div>
<form action="{{ app.request.uri }}" method="post" id="form-main" enctype="multipart/form-data">
<div class="tab-content">
<div class="tab-pane active">
<div class="tab-form">
{{ include('blog/post_admin/_form.html.twig') }}
</div>
</div>
</div>
{{ form_rest(form) }}
</form>
{% endblock %}

View file

@ -0,0 +1,106 @@
{% extends 'admin/layout.html.twig' %}
{% block body %}
<div class="bg-light pl-5 pr-4 pt-5 pb-5">
<div class="d-flex">
<div class="mr-auto w-50">
<h1 class="display-5">{{ entity.title }}</h1>
</div>
<div class="ml-auto">
<div class="btn-group">
<a href="{{ path('admin_blog_post_index') }}" class="btn btn-secondary">
<span class="fa fa-list pr-1"></span>
Retour à la liste
</a>
<a href="{{ path('admin_blog_post_edit', {entity: entity.id}) }}" class="btn btn-primary">
<span class="fa fa-edit pr-1"></span>
Éditer
</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-3 p-3">
<ul class="list-group">
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Titre</span>
{{ entity.title }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Sous-titre</span>
{{ entity.subTitle }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Catégories</span>
{% for category in entity.categories %}
<a class="d-block" href="{{ path('admin_blog_post_show', {entity: category.id}) }}">{{ category.title }}</a>
{% endfor %}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">URL</span>
{{ absolute_url('/' ~ entity.slug) }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Meta description</span>
{{ entity.metaDescription }}
</li>
</ul>
</div>
<div class="col-6 p-3">
<div class="font-weight-bold pb-2">Contenu</div>
{{ entity.content|raw|nl2br }}
</div>
<div class="col-3 p-3">
<ul class="list-group">
<li class="list-group-item">
<span class="font-weight-bold pb-3 d-block">Image</span>
{% if entity.image %}
<figure>
<img src="{{ asset('uploads/' ~ entity.image) }}" alt="{{ entity.imageCaption }}" title="{{ entity.imageCaption }}" class="img-fluid">
<figcaption>
{{ entity.imageCaption }}
</figcaption>
</figure>
{% else %}
-
{% endif %}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Statut</span>
{% if entity.status == 0 %}
Brouillon
{% else %}
Publié
{% endif %}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Date de publication</span>
{{ entity.publishedAt ? entity.publishedAt|date('d/m/Y H:i') : '-' }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Auteur</span>
<a href="">
{{ entity.author.displayName }}
</a>
</li>
</ul>
</div>
</div>
{% endblock %}