Added:
- activity on menu - starting activity page
This commit is contained in:
52
assets/controllers/activity_controller.js
Normal file
52
assets/controllers/activity_controller.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import {Controller} from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* The following line makes this controller "lazy": it won't be downloaded until needed
|
||||
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
|
||||
*/
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
static targets = ['activity']
|
||||
|
||||
// ...
|
||||
async connect() {
|
||||
try {
|
||||
const response = await fetch(`/activity/status`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
// Handle the response data as needed
|
||||
this.activityTarget.innerHTML = data.length;
|
||||
if (data.length > 0) {
|
||||
this.activityTarget.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
|
||||
|
||||
const mercureHubUrl = 'https://localhost/.well-known/mercure';
|
||||
const eventSource = new EventSource(`${mercureHubUrl}?topic=activity`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.processing !== undefined && data.pending !== undefined) {
|
||||
let totalActivities = data.processing.length + data.pending.length;
|
||||
this.activityTarget.innerHTML = totalActivities;
|
||||
if (totalActivities > 0) {
|
||||
this.activityTarget.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
eventSource
|
||||
.onerror = (event) => {
|
||||
console.error('EventSource failed:', event);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ services:
|
||||
tags:
|
||||
- { name: kernel.event_listener, event: 'manga.scraped', method: 'onMangaScraped' }
|
||||
|
||||
App\EventSubscriber\QueueStatusSubscriber:
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
||||
|
||||
App\Controller\MenuController:
|
||||
tags: [ 'controller.service_arguments' ]
|
||||
|
||||
|
||||
113
src/Controller/ActivityController.php
Normal file
113
src/Controller/ActivityController.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Message\DownloadChapter;
|
||||
use App\Repository\ChapterRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class ActivityController extends AbstractController
|
||||
{
|
||||
public function __construct(private Connection $connection, private readonly ChapterRepository $chapterRepository)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#[Route('/activity', name: 'app_activity')]
|
||||
public function index(): Response
|
||||
{
|
||||
$queueStatus = $this->getQueueStatus();
|
||||
$decodedPending = $this->decodeMessages($queueStatus['pending']);
|
||||
$decodedProcessing = $this->decodeMessages($queueStatus['processing']);
|
||||
|
||||
$status = array_merge(
|
||||
$this->buildStatusActivity($decodedPending),
|
||||
$this->buildStatusActivity($decodedProcessing)
|
||||
);
|
||||
|
||||
return $this->render('activity/index.html.twig', [
|
||||
'controller_name' => 'ActivityController',
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/activity/status', name: 'app_activity_status', methods: ['GET'])]
|
||||
public function getStatus(): JsonResponse
|
||||
{
|
||||
$queueStatus = $this->getQueueStatus();
|
||||
$decodedPending = $this->decodeMessages($queueStatus['pending']);
|
||||
$decodedProcessing = $this->decodeMessages($queueStatus['processing']);
|
||||
$status = array_merge(
|
||||
$this->buildStatusActivity($decodedPending),
|
||||
$this->buildStatusActivity($decodedProcessing)
|
||||
);
|
||||
|
||||
return new JsonResponse($status);
|
||||
}
|
||||
|
||||
// TODO refactorer ce code avec celui du QueueStatusSubscriber
|
||||
private function getQueueStatus(): array
|
||||
{
|
||||
// Requête pour récupérer les messages en attente
|
||||
$sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NULL';
|
||||
$pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']);
|
||||
|
||||
// Requête pour récupérer les messages en cours de traitement
|
||||
$sqlProcessing = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NOT NULL';
|
||||
$processing = $this->connection->fetchAllAssociative($sqlProcessing, ['queue' => 'default']);
|
||||
|
||||
return [
|
||||
'pending' => $pending,
|
||||
'processing' => $processing
|
||||
];
|
||||
}
|
||||
|
||||
private function buildStatusActivity(array $activity): array
|
||||
{
|
||||
$status = [];
|
||||
foreach ($activity as $envelope) {
|
||||
$envelope = $envelope['body'];
|
||||
if ($envelope instanceof Envelope) {
|
||||
if (!$envelope->getMessage() instanceof DownloadChapter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chapter = $this->chapterRepository->find($envelope->getMessage()->getChapterId());
|
||||
$manga = $chapter->getManga();
|
||||
$status[] = [
|
||||
'manga' => $manga->getTitle(),
|
||||
'volume' => $chapter->getVolume(),
|
||||
'chapter' => $chapter->getNumber(),
|
||||
'title' => $chapter->getTitle(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function decodeMessages(array $messages): array
|
||||
{
|
||||
$decodedMessages = [];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$decodedMessages[] = [
|
||||
'id' => $message['id'],
|
||||
'body' => $this->decodeMessageBody($message['body']),
|
||||
'headers' => json_decode($message['headers'], true),
|
||||
];
|
||||
}
|
||||
|
||||
return $decodedMessages;
|
||||
}
|
||||
|
||||
private function decodeMessageBody(string $body)
|
||||
{
|
||||
return unserialize(stripcslashes($body));
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,86 @@ namespace App\Controller;
|
||||
use App\Entity\Chapter;
|
||||
use App\Entity\ContentSource;
|
||||
use App\Entity\Manga;
|
||||
use App\Message\DownloadChapter;
|
||||
use App\Repository\ChapterRepository;
|
||||
use App\Repository\MangaRepository;
|
||||
use App\Service\MangadexProvider;
|
||||
use App\Service\MangaScraperService;
|
||||
use App\Service\MangaUpdatesMetadataProvider;
|
||||
use App\Service\SushiScanProviderService;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
class TestController extends AbstractController
|
||||
{
|
||||
public function __construct(private MangadexProvider $mangadexProvider, private MangaRepository $mangaRepository)
|
||||
public function __construct(
|
||||
private MangadexProvider $mangadexProvider,
|
||||
private MangaRepository $mangaRepository,
|
||||
private MessageBusInterface $bus,
|
||||
private Connection $connection,
|
||||
private SerializerInterface $serializer,
|
||||
private readonly ChapterRepository $chapterRepository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/test', name: 'test')]
|
||||
public function test(): Response
|
||||
{
|
||||
$manga = $this->mangaRepository->find(8);
|
||||
$sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue';
|
||||
$pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']);
|
||||
|
||||
dd($this->mangadexProvider->getFeed($manga));
|
||||
// // Requête pour récupérer les messages en cours de traitement
|
||||
// $sqlProcessing = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NOT NULL';
|
||||
// $processing = $this->connection->fetchAllAssociative($sqlProcessing, ['queue' => 'default']);
|
||||
|
||||
// dd($pending);
|
||||
$decoded = $this->decodeMessages($pending);
|
||||
|
||||
$status = [];
|
||||
foreach($decoded as $message) {
|
||||
$message = $message['body'];
|
||||
if($message instanceof Envelope) {
|
||||
$chapter = $this->chapterRepository->find($message->getMessage()->getChapterId());
|
||||
$manga = $chapter->getManga();
|
||||
$status[] = [
|
||||
'manga' => $manga->getTitle(),
|
||||
'volume' => $chapter->getVolume(),
|
||||
'chapter' => $chapter->getNumber(),
|
||||
'title' => $chapter->getTitle(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// $this->bus->dispatch(new DownloadChapter(1));
|
||||
|
||||
dd($status);
|
||||
}
|
||||
|
||||
private function decodeMessages(array $messages): array
|
||||
{
|
||||
$decodedMessages = [];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$decodedMessages[] = [
|
||||
'id' => $message['id'],
|
||||
'body' => $this->decodeMessageBody($message['body']),
|
||||
'headers' => json_decode($message['headers'], true),
|
||||
];
|
||||
}
|
||||
|
||||
return $decodedMessages;
|
||||
}
|
||||
|
||||
private function decodeMessageBody(string $body)
|
||||
{
|
||||
return unserialize(stripcslashes($body));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
36
src/Event/PageScrappingProgressEvent.php
Normal file
36
src/Event/PageScrappingProgressEvent.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class PageScrappingProgressEvent extends Event
|
||||
{
|
||||
public const NAME = 'page.scrapping.progress';
|
||||
|
||||
private int $chapterId;
|
||||
private int $pageIndex;
|
||||
private int $totalPages;
|
||||
|
||||
public function __construct(int $chapterId, int $pageIndex, int $totalPages)
|
||||
{
|
||||
$this->chapterId = $chapterId;
|
||||
$this->pageIndex = $pageIndex;
|
||||
$this->totalPages = $totalPages;
|
||||
}
|
||||
|
||||
public function getChapterId(): int
|
||||
{
|
||||
return $this->chapterId;
|
||||
}
|
||||
|
||||
public function getPageIndex(): int
|
||||
{
|
||||
return $this->pageIndex;
|
||||
}
|
||||
|
||||
public function getTotalPages(): int
|
||||
{
|
||||
return $this->totalPages;
|
||||
}
|
||||
}
|
||||
146
src/EventSubscriber/QueueStatusSubscriber.php
Normal file
146
src/EventSubscriber/QueueStatusSubscriber.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Event\PageScrappingProgressEvent;
|
||||
use App\Message\DownloadChapter;
|
||||
use App\Repository\ChapterRepository;
|
||||
use App\Service\ActivityService;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Messenger\Envelope;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
|
||||
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
|
||||
|
||||
class QueueStatusSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ActivityService $activityService,
|
||||
private Connection $connection,
|
||||
private ChapterRepository $chapterRepository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
WorkerMessageReceivedEvent::class => 'onMessageReceived',
|
||||
WorkerMessageHandledEvent::class => 'onMessageHandled',
|
||||
WorkerMessageFailedEvent::class => 'onMessageFailed',
|
||||
PageScrappingProgressEvent::NAME => 'onPageScrapingProgress',
|
||||
];
|
||||
}
|
||||
|
||||
public function onMessageReceived(WorkerMessageReceivedEvent $event): void
|
||||
{
|
||||
$envelope = $event->getEnvelope();
|
||||
$message = $envelope->getMessage();
|
||||
|
||||
if ($message instanceof DownloadChapter) {
|
||||
$this->activityService->sendUpdate($this->getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
public function onMessageHandled(WorkerMessageHandledEvent $event): void
|
||||
{
|
||||
$envelope = $event->getEnvelope();
|
||||
$message = $envelope->getMessage();
|
||||
|
||||
if ($message instanceof DownloadChapter) {
|
||||
$this->activityService->sendUpdate($this->getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
public function onMessageFailed(WorkerMessageFailedEvent $event): void
|
||||
{
|
||||
$envelope = $event->getEnvelope();
|
||||
$message = $envelope->getMessage();
|
||||
|
||||
if ($message instanceof DownloadChapter) {
|
||||
$this->activityService->sendUpdate($this->getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageScrapingProgress(PageScrappingProgressEvent $event): void
|
||||
{
|
||||
$data = [
|
||||
'status' => 'Page scraping progress',
|
||||
'chapterId' => $event->getChapterId(),
|
||||
'pageIndex' => $event->getPageIndex(),
|
||||
'totalPages' => $event->getTotalPages(),
|
||||
];
|
||||
$this->activityService->sendUpdate($data);
|
||||
}
|
||||
|
||||
private function getActivity(): array
|
||||
{
|
||||
$queueStatus = $this->getQueueStatus();
|
||||
return [
|
||||
'processing' => $this->buildStatusActivity($this->decodeMessages($queueStatus['processing'])),
|
||||
'pending' => $this->buildStatusActivity($this->decodeMessages($queueStatus['pending']))
|
||||
];
|
||||
}
|
||||
|
||||
//TODO refactorer ce code avec celui du ActivityController
|
||||
private function buildStatusActivity(array $activity): array
|
||||
{
|
||||
$status = [];
|
||||
foreach ($activity as $envelope) {
|
||||
$envelope = $envelope['body'];
|
||||
if ($envelope instanceof Envelope) {
|
||||
if (!$envelope->getMessage() instanceof DownloadChapter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$chapter = $this->chapterRepository->find($envelope->getMessage()->getChapterId());
|
||||
$manga = $chapter->getManga();
|
||||
$status[] = [
|
||||
'manga' => $manga->getTitle(),
|
||||
'volume' => $chapter->getVolume(),
|
||||
'chapter' => $chapter->getNumber(),
|
||||
'title' => $chapter->getTitle(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
private function getQueueStatus(): array
|
||||
{
|
||||
// Requête pour récupérer les messages en attente
|
||||
$sqlPending = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NULL';
|
||||
$pending = $this->connection->fetchAllAssociative($sqlPending, ['queue' => 'default']);
|
||||
|
||||
// Requête pour récupérer les messages en cours de traitement
|
||||
$sqlProcessing = 'SELECT * FROM messenger_messages WHERE queue_name = :queue AND available_at IS NOT NULL';
|
||||
$processing = $this->connection->fetchAllAssociative($sqlProcessing, ['queue' => 'default']);
|
||||
|
||||
return [
|
||||
'pending' => $pending,
|
||||
'processing' => $processing
|
||||
];
|
||||
}
|
||||
|
||||
private function decodeMessages(array $messages): array
|
||||
{
|
||||
$decodedMessages = [];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$decodedMessages[] = [
|
||||
'id' => $message['id'],
|
||||
'body' => $this->decodeMessageBody($message['body']),
|
||||
'headers' => json_decode($message['headers'], true),
|
||||
];
|
||||
}
|
||||
|
||||
return $decodedMessages;
|
||||
}
|
||||
|
||||
private function decodeMessageBody(string $body)
|
||||
{
|
||||
return unserialize(stripcslashes($body));
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,10 @@ readonly class DownloadChapterHandler
|
||||
{
|
||||
$chapter = $this->chapterRepository->find($message->getChapterId());
|
||||
if (!$chapter) {
|
||||
$this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'Chapter not found.']);
|
||||
$this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter not found.']);
|
||||
throw new BadRequestHttpException('Chapter not found');
|
||||
} elseif ($chapter->getLocalPath() !== null) {
|
||||
$this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'Chapter already scraped.']);
|
||||
$this->notificationService->sendUpdate(['status' => 'error', 'message' => 'Chapter already scraped.']);
|
||||
throw new BadRequestHttpException('Chapter already downloaded');
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ readonly class DownloadChapterHandler
|
||||
$scrapedSuccessfully = true;
|
||||
break;
|
||||
} catch (Exception $e) {
|
||||
$this->notificationService->sendUpdate('notification', [
|
||||
$this->notificationService->sendUpdate([
|
||||
'status' => 'warning',
|
||||
'message' => 'An error occurred while scraping with source: ' . $source->getBaseUrl() . '. Trying next source...'
|
||||
]);
|
||||
@@ -74,13 +74,13 @@ readonly class DownloadChapterHandler
|
||||
}
|
||||
|
||||
if (!$scrapedSuccessfully) {
|
||||
$this->notificationService->sendUpdate('notification', [
|
||||
$this->notificationService->sendUpdate([
|
||||
'status' => 'error',
|
||||
'message' => 'All sources failed to scrape the chapter ' . $chapter->getManga()->getTitle() . ' ' . $chapter->getNumber() . '.'
|
||||
]);
|
||||
throw new Exception('All sources failed to scrape the chapter ' . $chapter->getManga()->getTitle() . ' ' . $chapter->getNumber() . '.');
|
||||
}
|
||||
|
||||
$this->notificationService->sendUpdate('notification', ['status' => 'success', 'message' => 'Chapter scraped successfully.']);
|
||||
$this->notificationService->sendUpdate(['status' => 'success', 'message' => 'Chapter scraped successfully.']);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/Service/ActivityService.php
Normal file
20
src/Service/ActivityService.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
use Symfony\Component\Mercure\Update;
|
||||
|
||||
class ActivityService
|
||||
{
|
||||
public function __construct(private HubInterface $hub)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function sendUpdate(mixed $data): void
|
||||
{
|
||||
$update = new Update('activity', json_encode($data));
|
||||
$this->hub->publish($update);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Service;
|
||||
use App\Entity\Chapter;
|
||||
use App\Entity\Manga;
|
||||
use App\Entity\ContentSource;
|
||||
use App\Event\PageScrappingProgressEvent;
|
||||
use App\EventSubscriber\MangaScrapedEvent;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
@@ -143,6 +144,9 @@ class MangaScraperService
|
||||
|
||||
$this->downloadAndSaveImage($pageUrl, $imagePath);
|
||||
|
||||
$event = new PageScrappingProgressEvent($chapter->getId(), count($pageData) + 1, count($results['chapter']['dataSaver']));
|
||||
$this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME);
|
||||
|
||||
$pageData[] = [
|
||||
'image_url' => $pageUrl,
|
||||
'local_image_url' => sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName),
|
||||
@@ -215,6 +219,9 @@ class MangaScraperService
|
||||
|
||||
$this->downloadAndSaveImage($page['image_url'], $imagePath);
|
||||
|
||||
$event = new PageScrappingProgressEvent($chapter->getId(), count($pageData) + 1, 0);
|
||||
$this->eventDispatcher->dispatch($event, PageScrappingProgressEvent::NAME);
|
||||
|
||||
$pageData[] = [
|
||||
'image_url' => $page['image_url'],
|
||||
'local_image_url' => sprintf('/manga-images/%s/%s/%s', $mangaTitle, $chapterNumber, $imageName),
|
||||
|
||||
@@ -12,7 +12,7 @@ use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
readonly class MangadexProvider implements MetadataProviderInterface
|
||||
{
|
||||
public function __construct(private ClientInterface $client, private SluggerInterface $slugger)
|
||||
public function __construct(private ClientInterface $client, private SluggerInterface $slugger, private NotificationService $notificationService)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -22,12 +22,17 @@ readonly class MangadexProvider implements MetadataProviderInterface
|
||||
return new ArrayCollection();
|
||||
}
|
||||
|
||||
try{
|
||||
$results = $this->client->get('/manga', [
|
||||
'title' => $title,
|
||||
'contentRating' => ['safe', 'suggestive'],
|
||||
'includes' => ['cover_art', 'author'],
|
||||
'limit' => 25
|
||||
]);
|
||||
}catch(\Exception $e){
|
||||
$this->notificationService->sendUpdate('notification', ['status' => 'error', 'message' => 'An error occurred while fetching data from Mangadex.']);
|
||||
return new ArrayCollection();
|
||||
}
|
||||
|
||||
$mangas = [];
|
||||
foreach ($results['data'] as $result) {
|
||||
|
||||
@@ -12,9 +12,9 @@ class NotificationService
|
||||
|
||||
}
|
||||
|
||||
public function sendUpdate(string $topic, mixed $data): void
|
||||
public function sendUpdate(mixed $data): void
|
||||
{
|
||||
$update = new Update($topic, json_encode($data));
|
||||
$update = new Update('notification', json_encode($data));
|
||||
$this->hub->publish($update);
|
||||
}
|
||||
}
|
||||
|
||||
39
templates/activity/index.html.twig
Normal file
39
templates/activity/index.html.twig
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% block body %}
|
||||
{# TODO styliser cette page #}
|
||||
<table class="min-w-full bg-white">
|
||||
<thead class="bg-gray-800 text-white">
|
||||
<tr>
|
||||
<th class="w-1/12 py-2 px-4">
|
||||
<input type="checkbox">
|
||||
</th>
|
||||
<th class="w-2/12 py-2 px-4 text-left">Manga</th>
|
||||
<th class="w-1/12 py-2 px-4 text-left">Volume</th>
|
||||
<th class="w-4/12 py-2 px-4 text-left">Chapter</th>
|
||||
<th class="w-1/12 py-2 px-4 text-left">Title</th>
|
||||
<th class="w-1/12 py-2 px-4 text-left">Actions</th>
|
||||
|
||||
<th class="w-1/12 py-2 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-gray-700">
|
||||
{% for manga in status %}
|
||||
<tr class="border-b">
|
||||
<td class="py-2 px-4 text-center">
|
||||
<input type="checkbox">
|
||||
</td>
|
||||
<td class="py-2 px-4">{{ manga.manga }}</td>
|
||||
<td class="py-2 px-4">{{ manga.volume }}</td>
|
||||
<td class="py-2 px-4">{{ manga.chapter }}</td>
|
||||
<td class="py-2 px-4">{{ manga.title }}</td>
|
||||
<td class="py-2 px-4 text-center">
|
||||
<button class="text-red-500 hover:text-red-700">×</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4">
|
||||
<span>Total records: {{ status|length }}</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -21,9 +21,18 @@
|
||||
<i class="fas fa-calendar-alt mr-2"></i>
|
||||
<span>Calendrier</span>
|
||||
</li>
|
||||
<li class="mb-4 pl-8 flex items-center hover:text-green-600">
|
||||
<li class="mb-4 pl-8 hover:text-green-600">
|
||||
<a href="{{ path('app_activity') }}">
|
||||
<div data-controller="activity" class="flex flew-row justify-between">
|
||||
<div class="flex flex-row">
|
||||
<i class="fas fa-clock mr-2"></i>
|
||||
<span>Activité</span>
|
||||
</div>
|
||||
{# TODO le texte doit être blanc au survol #}
|
||||
<span data-activity-target="activity"
|
||||
class="bg-green-500 rounded px-2 mr-4 hover:text-white hidden"></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li class="mb-4 pl-8 flex items-center hover:text-green-600">
|
||||
<i class="fas fa-cog mr-2"></i>
|
||||
|
||||
Reference in New Issue
Block a user