- toolbar and fixes
This commit is contained in:
Jérémy Guillot
2024-06-29 14:51:10 +02:00
parent b04055ec22
commit 858a5bed06
20 changed files with 404 additions and 68 deletions

View File

@@ -150,3 +150,9 @@ npm-run: ## Run the dev server
npm-watch: ## Watch for changes
@$(DOCKER_COMP) exec node npm run watch
npm-add: ## Add a package as a dependency make npm-add p=package-name
@$(DOCKER_COMP) exec node npm install $(p)
npm-add-dev: ## Add a package as a dev dependency make npm-add-dev p=package-name
@$(DOCKER_COMP) exec node npm install $(p) --save-dev

View File

@@ -0,0 +1,45 @@
// assets/controllers/dropdown_controller.js
import {Controller} from "@hotwired/stimulus"
import {useClickOutside} from "stimulus-use"
export default class extends Controller {
static targets = ["button", "menu"]
connect() {
useClickOutside(this)
}
toggle(event) {
this.menuTarget.classList.toggle('hidden')
if (!this.menuTarget.classList.contains('hidden')) {
this.positionMenu()
}
}
clickOutside(event) {
this.menuTarget.classList.add('hidden')
}
positionMenu() {
const buttonRect = this.buttonTarget.getBoundingClientRect()
const menuRect = this.menuTarget.getBoundingClientRect()
const spaceRight = window.innerWidth - buttonRect.right
const spaceBottom = window.innerHeight - buttonRect.bottom
if (spaceRight < menuRect.width && buttonRect.left > menuRect.width) {
this.menuTarget.style.left = 'auto'
this.menuTarget.style.right = '0'
} else {
this.menuTarget.style.left = '0'
this.menuTarget.style.right = 'auto'
}
if (spaceBottom < menuRect.height && buttonRect.top > menuRect.height) {
this.menuTarget.style.top = 'auto'
this.menuTarget.style.bottom = '100%'
} else {
this.menuTarget.style.top = '100%'
this.menuTarget.style.bottom = 'auto'
}
}
}

View File

@@ -0,0 +1,59 @@
// assets/controllers/toolbar_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["dropdown"]
static values = {
currentSort: String,
currentOrder: String
}
refresh() {
console.log("Refreshing...")
}
syncRss() {
console.log("Syncing RSS...")
}
search() {
console.log("Searching...")
}
import() {
console.log("Importing...")
}
editMangas() {
console.log("Editing mangas...")
}
showOptions() {
console.log("Showing options...")
}
changeView() {
console.log("Changing view...")
}
sort(event) {
event.preventDefault()
const sortOption = event.currentTarget.dataset.sortOption
let order = 'asc'
if (sortOption === this.currentSortValue && this.currentOrderValue === 'asc') {
order = 'desc'
}
const url = new URL(window.location)
url.searchParams.set('sort', sortOption)
url.searchParams.set('order', order)
window.location = url.toString()
}
filter() {
console.log("Filtering...")
}
}

21
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"regenerator-runtime": "^0.13.9",
"sass": "^1.59.3",
"sass-loader": "^13.2.0",
"stimulus-use": "^0.52.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-notifier": "^1.15.0"
@@ -5706,6 +5707,16 @@
"node": ">= 0.4"
}
},
"node_modules/hotkeys-js": {
"version": "3.13.7",
"resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.13.7.tgz",
"integrity": "sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ==",
"dev": true,
"peer": true,
"funding": {
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -9207,6 +9218,16 @@
"node": ">= 0.8"
}
},
"node_modules/stimulus-use": {
"version": "0.52.2",
"resolved": "https://registry.npmjs.org/stimulus-use/-/stimulus-use-0.52.2.tgz",
"integrity": "sha512-413+tIw9n6Jnb0OFiQE1i3aP01i0hhGgAnPp1P6cNuBbhhqG2IOp8t1O/4s5Tw2lTvSYrFeLNdaY8sYlDaULeg==",
"dev": true,
"peerDependencies": {
"@hotwired/stimulus": ">= 3",
"hotkeys-js": ">= 3"
}
},
"node_modules/streamx": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz",

View File

@@ -11,6 +11,7 @@
"regenerator-runtime": "^0.13.9",
"sass": "^1.59.3",
"sass-loader": "^13.2.0",
"stimulus-use": "^0.52.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-notifier": "^1.15.0"

View File

@@ -2,6 +2,7 @@
namespace App\Controller;
use App\Manager\ToolbarManager;
use App\Message\DownloadChapter;
use App\Repository\ChapterRepository;
use Doctrine\DBAL\Connection;
@@ -13,7 +14,11 @@ use Symfony\Component\Routing\Annotation\Route;
class ActivityController extends AbstractController
{
public function __construct(private Connection $connection, private readonly ChapterRepository $chapterRepository)
public function __construct(
private readonly Connection $connection,
private readonly ChapterRepository $chapterRepository,
private readonly ToolbarManager $toolbarManager
)
{
}
@@ -33,6 +38,7 @@ class ActivityController extends AbstractController
return $this->render('activity/index.html.twig', [
'controller_name' => 'ActivityController',
'status' => $status,
'toolbarItems' => $this->toolbarManager->getToolbarItems(),
]);
}

View File

@@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\Chapter;
use App\Entity\Manga;
use App\Manager\ToolbarManager;
use App\Message\DownloadChapter;
use App\Repository\ChapterRepository;
use App\Repository\MangaRepository;
@@ -34,19 +35,23 @@ class MangaController extends AbstractController
private readonly ChapterRepository $chapterRepository,
private readonly MangaUpdatesMetadataProvider $mangaUpdatesDbProvider,
private readonly MessageBusInterface $bus,
private readonly CbzService $cbzService
private readonly CbzService $cbzService,
private readonly ToolbarManager $toolbarManager
)
{
}
#[Route('/manga', name: 'app_manga')]
public function index(): Response
public function index(Request $request): Response
{
// phpinfo();
$mangas = $this->mangaRepository->findAll();
$sort = $request->query->get('sort', 'title');
$order = $request->query->get('order', 'asc');
$mangas = $this->mangaRepository->findAllSorted($sort, $order);
return $this->render('manga/index.html.twig', [
'controller_name' => 'MangaController',
'mangas' => $mangas,
'toolbarItems' => $this->toolbarManager->getToolbarItems(),
]);
}
@@ -89,6 +94,7 @@ class MangaController extends AbstractController
return $this->render('manga/show_chapters.html.twig', [
'chapters_by_volume' => $chaptersByVolume,
'manga' => $manga,
'toolbarItems' => $this->toolbarManager->getToolbarItems(),
]);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Manager;
class ToolbarManager
{
public function getToolbarItems(): array
{
return [
'sortItems' => $this->getSortItems(),
'filterItems' => $this->getFilterItems(),
'viewOptions' => $this->getViewOptions()
];
}
private function getSortItems(): array
{
return [
['text' => 'Par titre', 'action' => 'sort', 'data' => ['sort-option' => 'title']],
['text' => 'Par année de publication', 'action' => 'sort', 'data' => ['sort-option' => 'publicationYear']],
['text' => 'Par date d\'ajout', 'action' => 'sort', 'data' => ['sort-option' => 'createdAt']]
];
}
private function getFilterItems(): array
{
return [
['text' => 'Tous les genres', 'action' => 'filter', 'data' => ['filter-option' => 'allGenres']],
['text' => 'Mangas terminés', 'action' => 'filter', 'data' => ['filter-option' => 'completed']],
['text' => 'Mangas en cours', 'action' => 'filter', 'data' => ['filter-option' => 'ongoing']]
];
}
private function getViewOptions(): array
{
return [
['text' => 'Vue grille', 'action' => 'changeView', 'data' => ['view-option' => 'grid']],
['text' => 'Vue liste', 'action' => 'changeView', 'data' => ['view-option' => 'list']]
];
}
}

View File

@@ -87,11 +87,6 @@ class MangaRepository extends ServiceEntityRepository
return $sortedEntities;
}
private function normalizeSlug(string $slug): string
{
return strtolower(preg_replace('/[^a-z0-9]+/i', '', $slug));
}
/**
* @throws NonUniqueResultException
*/
@@ -108,6 +103,28 @@ class MangaRepository extends ServiceEntityRepository
return $query->getQuery()->getOneOrNullResult();
}
public function findAllSorted(string $sort = 'title', string $order = 'asc'): array
{
$qb = $this->createQueryBuilder('m');
switch ($sort) {
case 'title':
$qb->orderBy('m.title', $order);
break;
case 'publicationYear':
$qb->orderBy('m.publicationYear', $order);
break;
case 'createdAt':
$qb->orderBy('m.createdAt', $order);
break;
// Ajoutez d'autres cas pour les différentes options de tri
default:
$qb->orderBy('m.title', 'asc');
}
return $qb->getQuery()->getResult();
}
// /**
// * @return Manga[] Returns an array of Manga objects
// */

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
class DropdownMenu
{
use DefaultActionTrait;
#[LiveProp (writable: true)]
public ?array $items = null;
}

View File

@@ -8,6 +8,7 @@ use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
@@ -27,6 +28,11 @@ class NewMangaForm
#[LiveProp(writable: true)]
public ?int $index = 0;
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}
public function mount(Manga $manga): void
{
$this->manga = $manga;
@@ -84,6 +90,8 @@ class NewMangaForm
$manga->addChapter($chapter);
}
$mangaChapterUrl = $this->urlGenerator->generate('app_manga_show', ['mangaSlug' => $manga->getSlug()]);
try {
foreach ($manga->getChapters() as $chapter) {
$entityManager->persist($chapter);
@@ -93,11 +101,11 @@ class NewMangaForm
$entityManager->flush();
} catch (\Exception $e) {
if ($e instanceof UniqueConstraintViolationException) {
return new RedirectResponse('/manga/' . $manga->getSlug());
return new RedirectResponse($mangaChapterUrl);
}
throw $e;
}
return new RedirectResponse('/manga/' . $manga->getSlug());
return new RedirectResponse($mangaChapterUrl);
}
}

View File

@@ -1,19 +1,41 @@
{% extends 'base.html.twig' %}
{% block toolbar %}
<div class="bg-gray-800 p-3 min-h-14">
<div class="flex flex-row items-center justify-between">
<div class="flex mr-2 items-center">
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser"/>
<twig:ToolBarButton icon="search" text="Rechercher le manga"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="sitemap" text="Aperçu renommage"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="wrench" text="Éditer"/>
<twig:ToolBarButton icon="trash-can" text="Supprimer"/>
</div>
</div>
</div>
{% set left_group %}
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser" action="refresh"/>
<twig:ToolBarButton icon="rss" text="Synchro RSS" action="syncRss"/>
<twig:Divider/>
<twig:ToolBarButton icon="search" text="Rechercher tout" action="search"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle" action="import"/>
<twig:Divider/>
<twig:ToolBarButton icon="wrench" text="Modifier Mangas" action="editMangas"/>
{% endset %}
{% set right_group %}
<twig:ToolBarButton icon="th-large" text="Options" action="showOptions"/>
<twig:DropdownMenu
icon="eye"
text="Vue"
items="{{ toolbarItems.viewOptions }}"
/>
<twig:Divider/>
<twig:DropdownMenu
icon="sort"
text="Trier"
items="{{ toolbarItems.sortItems }}"
/>
<twig:DropdownMenu
icon="filter"
text="Filtre"
items="{{ toolbarItems.filterItems }}"
/>
{% endset %}
<twig:Toolbar
left_group="{{ left_group }}"
right_group="{{ right_group }}"
data-action="click@window->toolbar#clickOutside"
/>
{% endblock %}
{% block body %}
<div class="container mx-auto mt-2">

View File

@@ -0,0 +1,2 @@
{# templates/components/Divider.html.twig #}
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>

View File

@@ -0,0 +1,29 @@
{# templates/components/DropdownMenu.html.twig #}
<div {{ attributes }} >
<div data-controller="dropdown" class="relative inline-block">
<twig:ToolBarButton
icon="{{ icon }}"
text="{{ text }}"
action="toggle"
data-dropdown-target="button"
/>
<div class="absolute left-0 mt-2 w-max z-10 bg-gray-800 rounded-sm shadow-lg hidden"
data-dropdown-target="menu" data-controller="toolbar">
<div class="py-1">
{% for item in items %}
<a href="#"
class="block px-4 py-2 text-sm text-white hover:text-green-500"
data-action="toolbar#{{ item.action }}"
{% if item.data is defined %}
{% for key, value in item.data %}
data-{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{{ item.text }}
</a>
{% endfor %}
</div>
</div>
</div>
</div>

View File

@@ -35,7 +35,7 @@
<span>{{ manga.title }}</span>
<span class="text-2xl text-gray-500 ml-2">({{ manga.publicationYear }})</span>
</div>
<a href="{{ path('manga_show', { 'mangaSlug': manga.slug }) }}"
<a href="{{ path('app_manga_show', { 'mangaSlug': manga.slug }) }}"
class="text-gray-400 hover:text-gray-500">
<i class="fas fa-external-link-alt"></i>
</a>

View File

@@ -16,7 +16,7 @@
<li class="px-4 py-2 text-gray-400">Mangas existants</li>
{% for manga in this.mangas %}
<li class="px-4 py-2 hover:bg-gray-600 cursor-pointer">
<a class="flex items-center" href="{{ path('manga_show', { 'mangaSlug': manga.slug }) }}">
<a class="flex items-center" href="{{ path('app_manga_show', { 'mangaSlug': manga.slug }) }}">
<img src="{{ manga.imageUrl ?? 'https://placehold.co/40x60' }}" alt="{{ manga.title }}"
class="w-10 h-15 object-cover mr-4">
<span>{{ manga.title }} ({{ manga.publicationYear }})</span>
@@ -26,7 +26,7 @@
{% else %}
<li class="px-4 py-2 text-gray-400">Aucun manga trouvé.</li>
<li class="px-4 py-2 hover:bg-gray-600 cursor-pointer">
<a class="flex items-center" href="{{ path('add_new_manga', {query: query}) }}">
<a class="flex items-center" href="{{ path('app_manga_new', {query: query}) }}">
<span>Ajouter {{ query }}</span>
</a>
</li>

View File

@@ -1,5 +1,11 @@
{# templates/components/ToolbarButton.html.twig #}
<div {{ attributes }}>
<button class="flex flex-col justify-around min-h-14 w-min ml-4 items-center text-white group">
<button
class="flex flex-col justify-around min-h-14 w-min ml-4 items-center text-white group"
{% if action %}
{{ stimulus_action('dropdown', action) }}
{% endif %}
>
<i class="fas fa-{{ icon }} text-xl group-hover:text-green-500"></i>
<span class="text-xs">{{ text }}</span>
</button>

View File

@@ -0,0 +1,11 @@
{# templates/components/Toolbar.html.twig #}
<div class="bg-gray-800 p-3 min-h-14" {{ stimulus_controller('toolbar') }}>
<div class="flex flex-row items-center justify-between">
<div class="flex mr-2 items-center">
{{ left_group|raw }}
</div>
<div class="flex mr-2 items-center">
{{ right_group|raw }}
</div>
</div>
</div>

View File

@@ -1,31 +1,49 @@
{% extends 'base.html.twig' %}
{% block toolbar %}
<div class="bg-gray-800 p-3 min-h-14">
<div class="flex flex-row items-center justify-between">
<div class="flex mr-2 items-center">
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser"/>
<twig:ToolBarButton icon="rss" text="Synchro RSS"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="search" text="Rechercher tout"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="wrench" text="Modifier Mangas"/>
</div>
<div class="flex mr-2 items-center">
<twig:ToolBarButton icon="th-large" text="Options"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="eye" text="Vue"/>
<twig:ToolBarButton icon="sort" text="Trier"/>
<twig:ToolBarButton icon="filter" text="Filtre"/>
</div>
</div>
</div>
{% set left_group %}
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser" action="refresh"/>
<twig:ToolBarButton icon="rss" text="Synchro RSS" action="syncRss"/>
<twig:Divider/>
<twig:ToolBarButton icon="search" text="Rechercher tout" action="search"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle" action="import"/>
<twig:Divider/>
<twig:ToolBarButton icon="wrench" text="Modifier Mangas" action="editMangas"/>
{% endset %}
{% set right_group %}
<twig:ToolBarButton icon="th-large" text="Options" action="showOptions"/>
<twig:DropdownMenu
icon="eye"
text="Vue"
items="{{ toolbarItems.viewOptions }}"
/>
<twig:Divider/>
<twig:DropdownMenu
icon="sort"
text="Trier"
items="{{ toolbarItems.sortItems }}"
/>
<twig:DropdownMenu
icon="filter"
text="Filtre"
items="{{ toolbarItems.filterItems }}"
/>
{% endset %}
<twig:Toolbar
left_group="{{ left_group }}"
right_group="{{ right_group }}"
data-action="click@window->toolbar#clickOutside"
/>
{% endblock %}
{% block body %}
<div class="w-full p-4 grid sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-8 2xl:grid-cols-12 gap-4">
{% for manga in mangas %}
<div class="bg-white overflow-hidden border border-gray-200 hover:shadow-2xl hover:border-gray-400 transition-all duration-300 flex flex-col">
<a href="{{ path('app_manga_show', { 'mangaSlug': manga.slug }) }}" class="block relative w-full pb-[150%] overflow-hidden">
<div
class="bg-white overflow-hidden border border-gray-200 hover:shadow-2xl hover:border-gray-400 transition-all duration-300 flex flex-col">
<a href="{{ path('app_manga_show', { 'mangaSlug': manga.slug }) }}"
class="block relative w-full pb-[150%] overflow-hidden">
<img src="{{ manga.imageUrl ?? 'https://placehold.co/150x220' }}" alt="{{ manga.title }}"
class="absolute top-0 left-0 w-full h-full object-cover">
</a>

View File

@@ -1,19 +1,41 @@
{% extends 'base.html.twig' %}
{% block toolbar %}
<div class="bg-gray-800 p-3 min-h-14">
<div class="flex flex-row items-center justify-between">
<div class="flex mr-2 items-center">
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser"/>
<twig:ToolBarButton icon="search" text="Rechercher le manga"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="sitemap" text="Aperçu renommage"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle"/>
<div class="min-h-14 mx-4 border-r opacity-50 border-green-500"></div>
<twig:ToolBarButton icon="wrench" text="Éditer"/>
<twig:ToolBarButton icon="trash-can" text="Supprimer"/>
</div>
</div>
</div>
{% set left_group %}
<twig:ToolBarButton icon="sync-alt" text="Tout actualiser" action="refresh"/>
<twig:ToolBarButton icon="rss" text="Synchro RSS" action="syncRss"/>
<twig:Divider/>
<twig:ToolBarButton icon="search" text="Rechercher tout" action="search"/>
<twig:ToolBarButton icon="user-plus" text="Importation manuelle" action="import"/>
<twig:Divider/>
<twig:ToolBarButton icon="wrench" text="Modifier Mangas" action="editMangas"/>
{% endset %}
{% set right_group %}
<twig:ToolBarButton icon="th-large" text="Options" action="showOptions"/>
<twig:DropdownMenu
icon="eye"
text="Vue"
items="{{ toolbarItems.viewOptions }}"
/>
<twig:Divider/>
<twig:DropdownMenu
icon="sort"
text="Trier"
items="{{ toolbarItems.sortItems }}"
/>
<twig:DropdownMenu
icon="filter"
text="Filtre"
items="{{ toolbarItems.filterItems }}"
/>
{% endset %}
<twig:Toolbar
left_group="{{ left_group }}"
right_group="{{ right_group }}"
data-action="click@window->toolbar#clickOutside"
/>
{% endblock %}
{% block body %}
<div class="relative">