}
- */
- async getPage(chapterId, pageNumber) {
- throw new Error('Not implemented');
- }
-
- /**
- * Récupère toutes les pages d'un chapitre
- * @param {string} chapterId - L'identifiant du chapitre
- * @returns {Promise<{pages: Page[], totalItems: number, currentPage: number, itemsPerPage: number, totalPages: number}>}
- */
- async getPages(chapterId) {
- throw new Error('Not implemented');
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/domain/reader.js b/assets/react/app/domain/reader.js
deleted file mode 100644
index f857c60..0000000
--- a/assets/react/app/domain/reader.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export class ReaderContext {
- constructor(id, title, number, manga, navigation) {
- this.id = id;
- this.title = title;
- this.number = number;
- this.manga = manga;
- this.navigation = navigation;
- }
-}
-
-export class Page {
- constructor(id, pageNumber, base64Content, mimeType, dimensions) {
- this.id = id;
- this.pageNumber = pageNumber;
- this.base64Content = base64Content;
- this.mimeType = mimeType;
- this.dimensions = dimensions;
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/index.jsx b/assets/react/app/index.jsx
deleted file mode 100644
index cec8494..0000000
--- a/assets/react/app/index.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react'
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.jsx'
-import '../../styles/app.scss'
-
-createRoot(document.getElementById('react-app')).render(
-
-
- ,
-)
diff --git a/assets/react/app/infrastructure/api/apiMangaRepository.js b/assets/react/app/infrastructure/api/apiMangaRepository.js
deleted file mode 100644
index 1c27aca..0000000
--- a/assets/react/app/infrastructure/api/apiMangaRepository.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import axios from 'axios';
-import { Manga, MangaCollection, MangaDetail } from '../../domain/manga';
-import { Chapter } from '../../domain/chapter';
-
-export class ApiMangaRepository {
- constructor() {
- this.api = axios.create({
- baseURL: '/api'
- });
- }
-
- async getMangaCollection(page = 1) {
- try {
- const response = await this.api.get(`/mangas?page=${page}`);
- const data = response.data;
-
- const mangas = data.items.map(item => new Manga(
- item.id,
- item.title,
- item.slug,
- item.imageUrl,
- item.author,
- item.publicationYear,
- item.genres,
- item.status,
- item.rating,
- item.description,
- item.createdAt
- ));
-
- return new MangaCollection(
- mangas,
- data.total,
- data.page,
- data.limit,
- data.hasNextPage,
- data.hasPreviousPage
- );
- } catch (error) {
- console.error('Error fetching manga collection:', error);
- throw error;
- }
- }
-
- async searchMangas(query) {
- try {
- const response = await this.api.get(`/mangas-search?title=${encodeURIComponent(query)}`);
- const data = response.data;
-
- return data.items.map(item => new Manga(
- item.externalId,
- item.title,
- item.slug,
- item.imageUrl,
- item.author,
- item.publicationYear,
- item.genres,
- item.status,
- item.rating,
- item.description,
- item.createdAt
- ));
- } catch (error) {
- console.error('Error searching mangas:', error);
- throw error;
- }
- }
-
- async getMangaBySlug(slug) {
- try {
- const mangaResponse = await this.api.get(`/mangas/by-slug/${slug}`);
- const mangaData = mangaResponse.data;
-
- const chaptersResponse = await this.api.get(`/mangas/${mangaData.id}/chapters?page=1&limit=1000&sortOrder=desc`);
- const chaptersData = chaptersResponse.data;
-
- const chapters = chaptersData.items.map(item => new Chapter(
- item.id,
- parseFloat(item.number),
- item.title,
- item.volume,
- item.isVisible,
- item.createdAt
- ));
-
- return new MangaDetail({
- id: mangaData.id,
- title: mangaData.title,
- slug: mangaData.slug,
- imageUrl: mangaData.imageUrl,
- author: mangaData.author,
- publicationYear: mangaData.publicationYear,
- genres: mangaData.genres,
- status: mangaData.status,
- rating: mangaData.rating,
- description: mangaData.description,
- createdAt: mangaData.createdAt
- }, chapters);
- } catch (error) {
- console.error('Error fetching manga details:', error);
- throw error;
- }
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/infrastructure/api/apiReaderRepository.js b/assets/react/app/infrastructure/api/apiReaderRepository.js
deleted file mode 100644
index 2fb9177..0000000
--- a/assets/react/app/infrastructure/api/apiReaderRepository.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import axios from 'axios';
-import { ReaderContext, Page } from '../../domain/reader';
-import { ReaderRepository } from '../../domain/ports/readerRepository';
-
-export class ApiReaderRepository extends ReaderRepository {
- constructor() {
- super();
- this.api = axios.create({
- baseURL: '/api'
- });
- }
-
- async getChapterContext(chapterId) {
- try {
- const response = await this.api.get(`/reader/chapter/${chapterId}`);
- const data = response.data;
-
- return new ReaderContext(
- data.id,
- data.title,
- data.number,
- data.manga,
- data.navigation
- );
- } catch (error) {
- console.error('Error fetching chapter context:', error);
- throw error;
- }
- }
-
- async getPage(chapterId, pageNumber) {
- try {
- const response = await this.api.get(`/reader/chapter/${chapterId}/page/${pageNumber}`);
- const data = response.data;
-
- return new Page(
- data.id,
- data.pageNumber,
- data.base64Content,
- data.mimeType,
- data.dimensions
- );
- } catch (error) {
- console.error('Error fetching page:', error);
- throw error;
- }
- }
-
- async getPages(chapterId) {
- try {
- const response = await this.api.get(`/reader/chapter/${chapterId}/pages`);
- const data = response.data;
-
- // Charger chaque page individuellement pour obtenir le contenu base64
- const pagesPromises = data.pages.map(async (page) => {
- const pageResponse = await this.getPage(chapterId, page.pageNumber);
- return pageResponse;
- });
-
- const loadedPages = await Promise.all(pagesPromises);
-
- return {
- pages: loadedPages,
- totalItems: data.totalItems,
- currentPage: data.currentPage,
- itemsPerPage: data.itemsPerPage,
- totalPages: data.totalPages
- };
- } catch (error) {
- console.error('Error fetching pages:', error);
- throw error;
- }
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/infrastructure/api/mockMangaRepository.js b/assets/react/app/infrastructure/api/mockMangaRepository.js
deleted file mode 100644
index 6794d33..0000000
--- a/assets/react/app/infrastructure/api/mockMangaRepository.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Manga, MangaCollection, MangaDetail } from '../../domain/manga.js';
-import { Chapter } from '../../domain/chapter.js';
-
-export class MockMangaRepository {
- constructor() {
- this.mangas = {
- 'one-piece': {
- id: '1',
- title: 'One Piece',
- slug: 'one-piece',
- imageUrl: 'https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?auto=format&fit=crop&w=800&q=80',
- author: 'Eiichiro Oda',
- publicationYear: 1997,
- genres: ['Action', 'Adventure', 'Comedy'],
- status: 'ongoing',
- rating: 4.9,
- description: 'Monkey D. Luffy refuses to let anyone or anything stand in the way of his quest to become king of all pirates.'
- },
- 'naruto': {
- id: '2',
- title: 'Naruto',
- slug: 'naruto',
- imageUrl: 'https://images.unsplash.com/photo-1618519764620-7403abdbdfe9?auto=format&fit=crop&w=800&q=80',
- author: 'Masashi Kishimoto',
- publicationYear: 1999,
- genres: ['Action', 'Adventure', 'Fantasy'],
- status: 'completed',
- rating: 4.8,
- description: 'Twelve years ago the Village Hidden in the Leaves was attacked by a fearsome threat.'
- },
- 'berserk': {
- id: '3',
- title: 'Berserk',
- slug: 'berserk',
- imageUrl: 'https://images.unsplash.com/photo-1607604276583-eef5d076aa5f?auto=format&fit=crop&w=800&q=80',
- author: 'Kentaro Miura',
- publicationYear: 1989,
- genres: ['Action', 'Dark Fantasy', 'Horror', 'Psychological'],
- status: 'ongoing',
- rating: 4.9,
- description: 'Guts, known as the Black Swordsman, seeks sanctuary from the demonic forces.'
- }
- };
- }
-
- async searchMangas(query) {
- if (!query) return [];
-
- const normalizedQuery = query.toLowerCase();
- return Object.values(this.mangas)
- .filter(manga =>
- manga.title.toLowerCase().includes(normalizedQuery) ||
- manga.author.toLowerCase().includes(normalizedQuery)
- )
- .map(manga => new Manga(
- manga.id,
- manga.title,
- manga.slug,
- manga.imageUrl,
- manga.author,
- manga.publicationYear,
- manga.genres,
- manga.status,
- manga.rating
- ));
- }
-
- async getMangaCollection(page = 1) {
- const mangas = Object.values(this.mangas).map(manga => new Manga(
- manga.id,
- manga.title,
- manga.slug,
- manga.imageUrl,
- manga.author,
- manga.publicationYear,
- manga.genres,
- manga.status,
- manga.rating
- ));
-
- return new MangaCollection(
- mangas,
- mangas.length,
- page,
- 10,
- false,
- false
- );
- }
-
- async getMangaBySlug(slug) {
- const manga = this.mangas[slug];
-
- if (!manga) {
- throw new Error(`Manga with slug "${slug}" not found`);
- }
-
- const chapters = [
- new Chapter('1', 378, 'Un assassin invité', 42, true, '2024-02-15'),
- new Chapter('2', 377, 'Snake In One\'s Bosom', 42, true, '2024-02-01'),
- new Chapter('3', 376, 'La mer tremble, la guerre se profile', 42, true, '2024-01-15'),
- new Chapter('4', 375, 'L\'aube suivant la nuit brumeuse', 41, true, '2024-01-01'),
- new Chapter('5', 374, 'Le monstre noir va-t-il se laisser faire ?', 41, true, '2023-12-15'),
- new Chapter('6', 373, 'Confrontation', 41, true, '2023-12-01'),
- ];
-
- return new MangaDetail(manga, chapters);
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/infrastructure/api/mockReaderRepository.js b/assets/react/app/infrastructure/api/mockReaderRepository.js
deleted file mode 100644
index d6e0ec6..0000000
--- a/assets/react/app/infrastructure/api/mockReaderRepository.js
+++ /dev/null
@@ -1,61 +0,0 @@
-export class MockReaderRepository {
- async getChapterContext(chapterId) {
- // Simuler un délai réseau
- await new Promise(resolve => setTimeout(resolve, 500));
-
- return {
- id: chapterId,
- title: "Un assassin invité",
- number: "378",
- manga: {
- id: "1",
- title: "One Piece"
- },
- navigation: {
- previous: {
- id: "prev-chapter",
- number: "377"
- },
- next: {
- id: "next-chapter",
- number: "379"
- }
- }
- };
- }
-
- async getPage(chapterId, pageNumber) {
- // Simuler un délai réseau
- await new Promise(resolve => setTimeout(resolve, 500));
-
- return {
- id: `page-${pageNumber}`,
- pageNumber: pageNumber,
- base64Content: "data:image/jpeg;base64,/9j/4AAQSkZJRg...", // Simulé
- mimeType: "image/jpeg",
- dimensions: {
- width: 800,
- height: 1200
- }
- };
- }
-
- async getPages(chapterId) {
- // Simuler un délai réseau
- await new Promise(resolve => setTimeout(resolve, 500));
-
- return {
- pages: Array.from({ length: 20 }, (_, i) => ({
- number: i + 1,
- dimensions: {
- width: 800,
- height: 1200
- }
- })),
- totalItems: 20,
- currentPage: 1,
- itemsPerPage: 20,
- totalPages: 1
- };
- }
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/Header.jsx b/assets/react/app/presentation/components/Header.jsx
deleted file mode 100644
index 1e0b4c7..0000000
--- a/assets/react/app/presentation/components/Header.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faBars } from '@fortawesome/free-solid-svg-icons';
-import { SearchBar } from './SearchBar/SearchBar.jsx';
-
-export function Header({ onMenuClick }) {
- return (
-
-
-
-
- Mangarr
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/Layout/Layout.jsx b/assets/react/app/presentation/components/Layout/Layout.jsx
deleted file mode 100644
index c599ea5..0000000
--- a/assets/react/app/presentation/components/Layout/Layout.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React, { useState } from 'react';
-import { Header } from '../Header';
-import { Sidebar } from '../Sidebar';
-
-export function Layout({ children, onMangaClick, onAddMangaClick }) {
- const [isSidebarOpen, setIsSidebarOpen] = useState(false);
-
- return (
-
- setIsSidebarOpen(!isSidebarOpen)}
- onMangaClick={onMangaClick}
- onAddMangaClick={onAddMangaClick}
- />
- setIsSidebarOpen(false)}
- onAddMangaClick={onAddMangaClick}
- />
-
-
- {children}
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/MangaCard.jsx b/assets/react/app/presentation/components/MangaCard.jsx
deleted file mode 100644
index f993e5c..0000000
--- a/assets/react/app/presentation/components/MangaCard.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-
-export function MangaCard({ manga }) {
- const navigate = useNavigate();
-
- const handleClick = () => {
- navigate(`/manga/${manga.slug}`);
- };
-
- const formatDate = (dateString) => {
- const date = new Date(dateString);
- return date.toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- year: 'numeric'
- });
- };
-
- return (
-
-
-

-
-
-
{manga.title}
-
- {manga.publicationYear}
-
-
- Added: {formatDate(manga.createdAt)}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/MangaGrid.jsx b/assets/react/app/presentation/components/MangaGrid.jsx
deleted file mode 100644
index b025504..0000000
--- a/assets/react/app/presentation/components/MangaGrid.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import { MangaCard } from './MangaCard.jsx';
-
-export function MangaGrid({ mangas }) {
- return (
-
- {mangas.map((manga) => (
-
- ))}
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/SearchBar/SearchBar.jsx b/assets/react/app/presentation/components/SearchBar/SearchBar.jsx
deleted file mode 100644
index 36ff23a..0000000
--- a/assets/react/app/presentation/components/SearchBar/SearchBar.jsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faSearch, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { ApiMangaRepository } from '../../../infrastructure/api/apiMangaRepository.js';
-import { SearchMangas } from '../../../application/useCases/searchMangas.js';
-
-const mangaRepository = new ApiMangaRepository();
-const searchMangas = new SearchMangas(mangaRepository);
-
-export function SearchBar() {
- const navigate = useNavigate();
- const [query, setQuery] = useState('');
- const [results, setResults] = useState([]);
- const [isOpen, setIsOpen] = useState(false);
- const [loading, setLoading] = useState(false);
- const [hasSearched, setHasSearched] = useState(false);
- const searchRef = useRef(null);
-
- useEffect(() => {
- const handleClickOutside = (event) => {
- if (searchRef.current && !searchRef.current.contains(event.target)) {
- setIsOpen(false);
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- useEffect(() => {
- const searchManga = async () => {
- if (!query.trim()) {
- setResults([]);
- setHasSearched(false);
- return;
- }
-
- setLoading(true);
- try {
- const searchResults = await searchMangas.execute(query);
- setResults(searchResults);
- setHasSearched(true);
- } catch (error) {
- console.error('Search error:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const timeoutId = setTimeout(searchManga, 300);
- return () => clearTimeout(timeoutId);
- }, [query]);
-
- const handleMangaClick = (slug) => {
- navigate(`/manga/${slug}`);
- setIsOpen(false);
- setQuery('');
- setHasSearched(false);
- };
-
- const handleAddMangaClick = () => {
- navigate(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
- setIsOpen(false);
- setQuery('');
- setHasSearched(false);
- };
-
- return (
-
-
-
- {
- setQuery(e.target.value);
- setIsOpen(true);
- }}
- onFocus={() => setIsOpen(true)}
- placeholder="Rechercher"
- className="appearance-none outline-none ml-2 pl-0 bg-transparent border-b border-white w-full placeholder:text-white text-white py-1 px-2 leading-tight transition-all duration-500 ease-in-out focus:placeholder:text-opacity-0 focus:border-opacity-0"
- />
-
-
- {isOpen && query.trim() && (
-
- {loading ? (
-
Chargement...
- ) : results.length > 0 ? (
-
-
- Mangas existants
-
- {results.map((manga) => (
-
- ))}
-
- ) : hasSearched && (
-
-
-
- )}
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/Sidebar.jsx b/assets/react/app/presentation/components/Sidebar.jsx
deleted file mode 100644
index 8e2311f..0000000
--- a/assets/react/app/presentation/components/Sidebar.jsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import React, { useState } from 'react';
-import { Link } from 'react-router-dom';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faBook,
- faPlus,
- faFileImport,
- faCompass,
- faExchangeAlt,
- faCalendar,
- faClockRotateLeft,
- faCog,
- faDesktop,
- faChevronDown,
- faChevronUp
-} from '@fortawesome/free-solid-svg-icons';
-
-export function Sidebar({ isOpen, onClose, onAddMangaClick }) {
- const [expandedMenus, setExpandedMenus] = useState({
- mangas: true, // Par défaut, le menu Mangas est ouvert
- settings: false,
- system: false
- });
-
- const menuItems = [
- {
- icon: faBook,
- text: 'Mangas',
- id: 'mangas',
- subItems: [
- { icon: faPlus, text: 'Ajouter un nouveau', onClick: () => onAddMangaClick() },
- { icon: faFileImport, text: 'Import bibliothèque', to: '/import' },
- { icon: faCompass, text: 'Découvrir', to: '/discover' },
- ]
- },
- {
- icon: faExchangeAlt,
- text: 'Convertir CBR en CBZ',
- to: '/convert'
- },
- {
- icon: faCalendar,
- text: 'Calendrier',
- to: '/calendar'
- },
- {
- icon: faClockRotateLeft,
- text: 'Activité',
- to: '/activity',
- badge: '3'
- },
- {
- icon: faCog,
- text: 'Paramètres',
- id: 'settings',
- subItems: [
- { text: 'Général', to: '/settings/general' },
- { text: 'Dossiers', to: '/settings/folders' },
- { text: 'Scrappers', to: '/settings/scrappers' },
- { text: 'UI', to: '/settings/ui' }
- ]
- },
- {
- icon: faDesktop,
- text: 'Système',
- id: 'system',
- subItems: [
- { text: 'Status', to: '/system/status' },
- { text: 'Backup', to: '/system/backup' },
- { text: 'Logs', to: '/system/logs' },
- { text: 'Updates', to: '/system/updates' }
- ]
- },
- ];
-
- const toggleMenu = (menuId) => {
- setExpandedMenus(prev => ({
- ...prev,
- [menuId]: !prev[menuId]
- }));
- };
-
- const MenuItem = ({ item }) => {
- const hasSubItems = item.subItems && item.subItems.length > 0;
- const isExpanded = item.id ? expandedMenus[item.id] : false;
-
- const handleClick = (e) => {
- if (hasSubItems) {
- e.preventDefault();
- toggleMenu(item.id);
- }
- };
-
- const renderLink = (linkItem, className) => {
- if (linkItem.onClick) {
- return (
-
- );
- }
-
- return (
-
- {linkItem.icon && }
- {linkItem.text}
-
- );
- };
-
- return (
-
- {item.to || item.onClick ? (
- renderLink(item, "flex items-center px-4 py-2 text-gray-300 hover:text-green-600 transition-colors duration-150")
- ) : (
-
- )}
-
- {hasSubItems && isExpanded && (
-
- {item.subItems.map((subItem, index) => {
- const link = renderLink(
- subItem,
- "block py-2 text-gray-300 hover:text-green-600 transition-colors duration-150"
- );
-
- return React.cloneElement(link, { key: `${subItem.text}-${index}` });
- })}
-
- )}
-
- );
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/Toolbar/Toolbar.jsx b/assets/react/app/presentation/components/Toolbar/Toolbar.jsx
deleted file mode 100644
index 7bc33ce..0000000
--- a/assets/react/app/presentation/components/Toolbar/Toolbar.jsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { ToolbarButton } from './ToolbarButton';
-
-export function Toolbar({
- leftSection = [],
- centerSection = [],
- rightSection = [],
- className = ''
-}) {
- const renderSection = (items) => (
-
- {items.map((item, index) => (
-
- ))}
-
- );
-
- return (
-
-
-
- {renderSection(leftSection)}
- {renderSection(centerSection)}
- {renderSection(rightSection)}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/components/Toolbar/ToolbarButton.jsx b/assets/react/app/presentation/components/Toolbar/ToolbarButton.jsx
deleted file mode 100644
index 6f1c91a..0000000
--- a/assets/react/app/presentation/components/Toolbar/ToolbarButton.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react';
-import { useNavigate } from 'react-router-dom';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-export function ToolbarButton({ icon, label, onClick, navigateTo, navigateBack = false, active = false }) {
- const navigate = useNavigate();
-
- const handleClick = () => {
- if (navigateBack) {
- navigate(-1, { replace: true });
- } else if (navigateTo) {
- navigate(navigateTo, { replace: true });
- } else if (onClick) {
- onClick();
- }
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/context/MangaContext.jsx b/assets/react/app/presentation/context/MangaContext.jsx
deleted file mode 100644
index c3252d0..0000000
--- a/assets/react/app/presentation/context/MangaContext.jsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react';
-import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
-import { GetMangaCollection } from '../../application/useCases/getMangaCollection';
-import { GetMangaDetail } from '../../application/useCases/getMangaDetail';
-
-const mangaRepository = new ApiMangaRepository();
-const getMangaCollection = new GetMangaCollection(mangaRepository);
-const getMangaDetail = new GetMangaDetail(mangaRepository);
-
-const MangaContext = createContext(null);
-
-const initialState = {
- collection: null,
- detailedMangas: {},
- loading: false,
- error: null,
- lastCollectionUpdate: null,
- isBackgroundLoading: false
-};
-
-function mangaReducer(state, action) {
- switch (action.type) {
- case 'SET_LOADING':
- return { ...state, loading: action.payload };
- case 'SET_BACKGROUND_LOADING':
- return { ...state, isBackgroundLoading: action.payload };
- case 'SET_ERROR':
- return { ...state, error: action.payload, loading: false };
- case 'SET_COLLECTION':
- return {
- ...state,
- collection: action.payload,
- loading: false,
- error: null,
- lastCollectionUpdate: Date.now()
- };
- case 'UPDATE_COLLECTION':
- return {
- ...state,
- collection: action.payload,
- isBackgroundLoading: false,
- lastCollectionUpdate: Date.now()
- };
- case 'SET_MANGA_DETAIL':
- // Mettre à jour également le manga dans la collection si présent
- const updatedCollection = state.collection ? {
- ...state.collection,
- items: state.collection.items.map(manga =>
- manga.slug === action.payload.slug
- ? {
- ...manga,
- ...action.payload,
- createdAt: manga.createdAt || action.payload.createdAt
- }
- : manga
- )
- } : state.collection;
-
- return {
- ...state,
- collection: updatedCollection,
- detailedMangas: {
- ...state.detailedMangas,
- [action.payload.slug]: {
- ...action.payload,
- createdAt: state.collection?.items.find(m => m.slug === action.payload.slug)?.createdAt || action.payload.createdAt
- }
- },
- loading: false,
- error: null
- };
- default:
- return state;
- }
-}
-
-export function MangaProvider({ children }) {
- const [state, dispatch] = useReducer(mangaReducer, initialState);
-
- // Fonction pour charger la collection en arrière-plan
- const refreshCollectionInBackground = useCallback(async () => {
- if (state.isBackgroundLoading) return;
-
- dispatch({ type: 'SET_BACKGROUND_LOADING', payload: true });
- try {
- const collection = await getMangaCollection.execute(1);
- dispatch({ type: 'UPDATE_COLLECTION', payload: collection });
- } catch (error) {
- console.error('Background collection refresh failed:', error);
- dispatch({ type: 'SET_BACKGROUND_LOADING', payload: false });
- }
- }, [state.isBackgroundLoading]);
-
- // Rafraîchir la collection toutes les 5 minutes si elle est chargée
- useEffect(() => {
- if (!state.collection) return;
-
- const interval = setInterval(() => {
- refreshCollectionInBackground();
- }, 5 * 60 * 1000);
-
- return () => clearInterval(interval);
- }, [state.collection, refreshCollectionInBackground]);
-
- const loadCollection = useCallback(async () => {
- // Si nous avons déjà des données, les afficher immédiatement
- if (state.collection) {
- // Rafraîchir en arrière-plan si les données sont vieilles de plus de 30 secondes
- const isStale = state.lastCollectionUpdate &&
- (Date.now() - state.lastCollectionUpdate) > 30 * 1000;
-
- if (isStale && !state.isBackgroundLoading) {
- refreshCollectionInBackground();
- }
- return;
- }
-
- dispatch({ type: 'SET_LOADING', payload: true });
- try {
- const collection = await getMangaCollection.execute(1);
- dispatch({ type: 'SET_COLLECTION', payload: collection });
- } catch (error) {
- dispatch({ type: 'SET_ERROR', payload: 'Failed to load manga collection' });
- console.error(error);
- }
- }, [state.collection, state.lastCollectionUpdate, state.isBackgroundLoading, refreshCollectionInBackground]);
-
- const loadMangaDetail = useCallback(async (slug) => {
- // Retourner les données en cache si disponibles
- if (state.detailedMangas[slug]) {
- // Rafraîchir en arrière-plan si les données sont vieilles de plus de 5 minutes
- const cachedManga = state.detailedMangas[slug];
- const isStale = cachedManga.lastUpdate &&
- (Date.now() - cachedManga.lastUpdate) > 5 * 60 * 1000;
-
- if (isStale) {
- // Charger les nouvelles données en arrière-plan
- getMangaDetail.execute(slug).then(manga => {
- dispatch({ type: 'SET_MANGA_DETAIL', payload: { ...manga, lastUpdate: Date.now() } });
- }).catch(console.error);
- }
-
- return state.detailedMangas[slug];
- }
-
- // Si le manga est dans la collection, l'utiliser comme données temporaires
- const collectionManga = getMangaFromCollection(slug);
- if (collectionManga) {
- dispatch({
- type: 'SET_MANGA_DETAIL',
- payload: { ...collectionManga, isPartial: true, lastUpdate: Date.now() }
- });
- }
-
- // Charger les détails complets
- dispatch({ type: 'SET_LOADING', payload: true });
- try {
- const manga = await getMangaDetail.execute(slug);
- dispatch({ type: 'SET_MANGA_DETAIL', payload: { ...manga, lastUpdate: Date.now() } });
- return manga;
- } catch (error) {
- dispatch({ type: 'SET_ERROR', payload: 'Failed to load manga details' });
- console.error(error);
- return null;
- }
- }, [state.detailedMangas]);
-
- const getMangaFromCollection = useCallback((slug) => {
- if (!state.collection) return null;
- return state.collection.items.find(manga => manga.slug === slug);
- }, [state.collection]);
-
- const value = {
- ...state,
- loadCollection,
- loadMangaDetail,
- getMangaFromCollection,
- refreshCollectionInBackground
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useManga() {
- const context = useContext(MangaContext);
- if (!context) {
- throw new Error('useManga must be used within a MangaProvider');
- }
- return context;
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/context/ReaderContext.jsx b/assets/react/app/presentation/context/ReaderContext.jsx
deleted file mode 100644
index 155e043..0000000
--- a/assets/react/app/presentation/context/ReaderContext.jsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { createContext, useContext, useReducer, useCallback } from 'react';
-import { ApiReaderRepository } from '../../infrastructure/api/apiReaderRepository';
-import { GetChapterContext } from '../../application/useCases/getChapterContext';
-import { GetPage } from '../../application/useCases/getPage';
-import { GetPages } from '../../application/useCases/getPages';
-
-const readerRepository = new ApiReaderRepository();
-const getChapterContext = new GetChapterContext(readerRepository);
-const getPage = new GetPage(readerRepository);
-const getPages = new GetPages(readerRepository);
-
-const ReaderContext = createContext(null);
-
-const initialState = {
- context: null,
- currentPage: 1,
- pages: [],
- loading: false,
- error: null,
- mode: 'classic', // 'classic' ou 'scrolling'
-};
-
-function readerReducer(state, action) {
- switch (action.type) {
- case 'SET_LOADING':
- return { ...state, loading: action.payload };
- case 'SET_ERROR':
- return { ...state, error: action.payload, loading: false };
- case 'SET_CONTEXT':
- return { ...state, context: action.payload, loading: false };
- case 'SET_PAGES':
- return { ...state, pages: action.payload, loading: false };
- case 'SET_CURRENT_PAGE':
- return { ...state, currentPage: action.payload };
- case 'SET_MODE':
- return { ...state, mode: action.payload };
- case 'RESET_STATE':
- return initialState;
- default:
- return state;
- }
-}
-
-export function ReaderProvider({ children }) {
- const [state, dispatch] = useReducer(readerReducer, initialState);
-
- const loadChapterContext = useCallback(async (chapterId) => {
- dispatch({ type: 'RESET_STATE' });
- dispatch({ type: 'SET_LOADING', payload: true });
- try {
- const context = await getChapterContext.execute(chapterId);
- dispatch({ type: 'SET_CONTEXT', payload: context });
- return true;
- } catch (error) {
- dispatch({ type: 'SET_ERROR', payload: error.response?.status === 404 ? 'Chapitre introuvable' : 'Erreur lors du chargement du chapitre' });
- console.error(error);
- return false;
- }
- }, []);
-
- const loadPage = useCallback(async (chapterId, pageNumber) => {
- dispatch({ type: 'SET_LOADING', payload: true });
- try {
- const page = await getPage.execute(chapterId, pageNumber);
- dispatch({ type: 'SET_LOADING', payload: false });
- return page;
- } catch (error) {
- const errorMessage = error.response?.status === 404
- ? 'Page introuvable'
- : 'Erreur lors du chargement de la page';
- dispatch({ type: 'SET_ERROR', payload: errorMessage });
- dispatch({ type: 'SET_LOADING', payload: false });
- return null;
- }
- }, []);
-
- const loadPages = useCallback(async (chapterId) => {
- dispatch({ type: 'SET_LOADING', payload: true });
- try {
- const { pages } = await getPages.execute(chapterId);
- if (!pages || pages.length === 0) {
- dispatch({ type: 'SET_ERROR', payload: 'Aucune page trouvée dans ce chapitre' });
- return false;
- }
- dispatch({ type: 'SET_PAGES', payload: pages });
- dispatch({ type: 'SET_CURRENT_PAGE', payload: 1 });
- dispatch({ type: 'SET_LOADING', payload: false });
- return true;
- } catch (error) {
- const errorMessage = error.response?.status === 404
- ? 'Chapitre introuvable'
- : 'Erreur lors du chargement des pages';
- dispatch({ type: 'SET_ERROR', payload: errorMessage });
- dispatch({ type: 'SET_LOADING', payload: false });
- return false;
- }
- }, []);
-
- const setCurrentPage = useCallback((pageNumber) => {
- dispatch({ type: 'SET_CURRENT_PAGE', payload: pageNumber });
- }, []);
-
- const setMode = useCallback((mode) => {
- dispatch({ type: 'SET_MODE', payload: mode });
- }, []);
-
- const value = {
- ...state,
- loadChapterContext,
- loadPage,
- loadPages,
- setCurrentPage,
- setMode,
- };
-
- return (
-
- {children}
-
- );
-}
-
-export function useReader() {
- const context = useContext(ReaderContext);
- if (!context) {
- throw new Error('useReader must be used within a ReaderProvider');
- }
- return context;
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/pages/AddMangaPage.jsx b/assets/react/app/presentation/pages/AddMangaPage.jsx
deleted file mode 100644
index 8cd827c..0000000
--- a/assets/react/app/presentation/pages/AddMangaPage.jsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useNavigate, useSearchParams } from 'react-router-dom';
-import { Layout } from '../components/Layout/Layout';
-import { Toolbar } from '../components/Toolbar/Toolbar';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faArrowLeft, faSearch, faStar } from '@fortawesome/free-solid-svg-icons';
-import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
-import { SearchMangas } from '../../application/useCases/searchMangas';
-
-const mangaRepository = new ApiMangaRepository();
-const searchMangas = new SearchMangas(mangaRepository);
-
-export function AddMangaPage() {
- const navigate = useNavigate();
- const [searchParams] = useSearchParams();
- const initialQuery = searchParams.get('q') || '';
-
- const [query, setQuery] = useState(initialQuery);
- const [results, setResults] = useState([]);
- const [loading, setLoading] = useState(false);
- const [selectedManga, setSelectedManga] = useState(null);
-
- useEffect(() => {
- if (initialQuery) {
- handleSearch(initialQuery);
- }
- }, [initialQuery]);
-
- const handleSearch = async (searchQuery) => {
- if (!searchQuery.trim()) {
- setResults([]);
- return;
- }
-
- setLoading(true);
- try {
- const searchResults = await searchMangas.execute(searchQuery);
- setResults(searchResults);
- } catch (error) {
- console.error('Search error:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const handleMangaClick = (slug) => {
- navigate(`/manga/${slug}`);
- };
-
- const handleAddMangaClick = (query = '') => {
- navigate(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
- };
-
- const toolbarConfig = {
- leftSection: [
- { icon: faArrowLeft, onClick: () => navigate(-1) }
- ]
- };
-
- return (
-
-
-
-
-
Ajouter un manga
-
-
-
-
- {
- setQuery(e.target.value);
- handleSearch(e.target.value);
- }}
- placeholder="Rechercher un manga..."
- className="w-full px-4 py-2 pl-10 border-b border-gray-300 focus:border-green-600 outline-none transition-colors"
- />
-
-
-
- {loading ? (
-
- ) : results.length > 0 ? (
-
- {results.map((manga) => (
-
setSelectedManga(manga)}
- className="flex gap-4 p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer"
- >
-

-
-
-
-
{manga.title}
-
{manga.author}
-
-
-
- {manga.rating}
-
-
-
-
- {manga.publicationYear} • {manga.status}
-
-
-
- {manga.genres.map((genre, index) => (
-
- {genre}
-
- ))}
-
-
-
- ))}
-
- ) : query && !loading && (
-
- Aucun résultat trouvé pour "{query}"
-
- )}
-
-
-
-
- {/* Modal de confirmation */}
- {selectedManga && (
-
-
-
-
Ajouter à la bibliothèque
-
-

-
-
{selectedManga.title}
-
{selectedManga.author}
-
- {selectedManga.genres.join(', ')}
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/pages/HomePage.jsx b/assets/react/app/presentation/pages/HomePage.jsx
deleted file mode 100644
index 8ea6841..0000000
--- a/assets/react/app/presentation/pages/HomePage.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
-import { MangaGrid } from '../components/MangaGrid.jsx';
-import { Layout } from '../components/Layout/Layout.jsx';
-import { Toolbar } from '../components/Toolbar/Toolbar.jsx';
-import { useManga } from '../context/MangaContext.jsx';
-import {
- faRefresh,
- faSearch,
- faGear,
- faEye,
- faSort,
- faFilter
-} from '@fortawesome/free-solid-svg-icons';
-
-export function HomePage() {
- const navigate = useNavigate();
- const {
- collection,
- loading,
- error,
- isBackgroundLoading,
- loadCollection,
- refreshCollectionInBackground
- } = useManga();
-
- useEffect(() => {
- loadCollection();
- }, [loadCollection]);
-
- const handleAddMangaClick = (query = '') => {
- navigate(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
- };
-
- const toolbarConfig = {
- leftSection: [
- {
- icon: faRefresh,
- label: 'Refresh',
- onClick: refreshCollectionInBackground,
- active: isBackgroundLoading
- },
- { icon: faSearch, label: 'Search', onClick: () => {} }
- ],
- rightSection: [
- { icon: faGear, onClick: () => {} },
- { icon: faEye, onClick: () => {} },
- { icon: faSort, onClick: () => {} },
- { icon: faFilter, onClick: () => {} }
- ]
- };
-
- if (loading && !collection) {
- return Loading...
;
- }
-
- if (error) {
- return {error}
;
- }
-
- return (
-
-
-
-
- {isBackgroundLoading && (
-
- Mise à jour en cours...
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/pages/MangaDetailPage.jsx b/assets/react/app/presentation/pages/MangaDetailPage.jsx
deleted file mode 100644
index 1ca3b98..0000000
--- a/assets/react/app/presentation/pages/MangaDetailPage.jsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
-import { Layout } from '../components/Layout/Layout.jsx';
-import { Toolbar } from '../components/Toolbar/Toolbar.jsx';
-import { useManga } from '../context/MangaContext.jsx';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faStar,
- faDownload,
- faEye,
- faArrowLeft,
- faRefresh,
- faBookmark,
- faShare
-} from '@fortawesome/free-solid-svg-icons';
-
-export function MangaDetailPage() {
- const { slug } = useParams();
- const navigate = useNavigate();
- const {
- detailedMangas,
- loading,
- error,
- loadMangaDetail,
- getMangaFromCollection
- } = useManga();
-
- const [isLoadingDetails, setIsLoadingDetails] = useState(false);
-
- // Obtenir les données du manga depuis le cache ou la collection
- const manga = detailedMangas[slug];
- const collectionManga = getMangaFromCollection(slug);
- const displayManga = manga || collectionManga;
-
- useEffect(() => {
- const loadDetails = async () => {
- if (!manga && displayManga) {
- setIsLoadingDetails(true);
- await loadMangaDetail(slug);
- setIsLoadingDetails(false);
- } else if (!manga && !displayManga) {
- await loadMangaDetail(slug);
- }
- };
-
- loadDetails();
- }, [slug, manga, displayManga, loadMangaDetail]);
-
- const handleAddMangaClick = (query = '') => {
- navigate(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
- };
-
- const toolbarConfig = {
- leftSection: [
- { icon: faArrowLeft, navigateBack: true },
- { icon: faRefresh, onClick: () => {} }
- ],
- rightSection: [
- { icon: faBookmark, onClick: () => {} },
- { icon: faShare, onClick: () => {} },
- { icon: faDownload, onClick: () => {} }
- ]
- };
-
- if (loading && !displayManga) {
- return Loading...
;
- }
-
- if (error) {
- return {error}
;
- }
-
- if (!displayManga) {
- return Manga not found
;
- }
-
- return (
-
-
-
- {/* Hero section with manga info */}
-
-
-

-
-
-
-
-
-
-
{displayManga.title}
-
- {displayManga.status}
-
- {isLoadingDetails && (
-
- Chargement des détails...
-
- )}
-
-
-
-
-
- {displayManga.rating}
-
-
{displayManga.publicationYear}
-
{displayManga.author}
-
-
-
- {displayManga.genres.map((genre, index) => (
-
- {genre}
-
- ))}
-
-
-
- {displayManga.description}
-
-
-
-
-
- {/* Chapters section - only shown when full details are loaded */}
- {manga && manga.chapters && (
-
- {Array.from(manga.chapters.entries()).map(([volume, chapters]) => (
-
-
- Volume {volume || 'Unknown'}
-
- ({chapters.length} chapters)
-
-
-
-
-
-
-
- |
- #
- |
-
- Title
- |
-
- Added
- |
-
- Actions
- |
-
-
-
- {chapters.map((chapter) => (
-
- |
- {chapter.number}
- |
-
- {chapter.title}
- |
-
- {new Date(chapter.createdAt).toLocaleDateString()}
- |
-
-
-
- |
-
- ))}
-
-
-
-
- ))}
-
- )}
-
- {!manga && (
-
-
- Chargement des chapitres...
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/assets/react/app/presentation/pages/ReaderPage.jsx b/assets/react/app/presentation/pages/ReaderPage.jsx
deleted file mode 100644
index 84114d3..0000000
--- a/assets/react/app/presentation/pages/ReaderPage.jsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import React, { useEffect, useState, useCallback } from 'react';
-import { useParams, useNavigate } from 'react-router-dom';
-import { useReader } from '../context/ReaderContext';
-import { Toolbar } from '../components/Toolbar/Toolbar.jsx';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faArrowLeft,
- faList,
- faExpand,
- faCompress,
- faBookOpen,
- faScroll
-} from '@fortawesome/free-solid-svg-icons';
-
-export function ReaderPage() {
- const { chapterId } = useParams();
- const navigate = useNavigate();
- const {
- context,
- currentPage,
- pages,
- loading,
- error,
- mode,
- loadChapterContext,
- loadPage,
- loadPages,
- setCurrentPage,
- setMode
- } = useReader();
-
- const [currentPageData, setCurrentPageData] = useState(null);
- const [isFullscreen, setIsFullscreen] = useState(false);
-
- useEffect(() => {
- const initializeChapter = async () => {
- const contextLoaded = await loadChapterContext(chapterId);
- if (contextLoaded) {
- await loadPages(chapterId);
- }
- };
- initializeChapter();
- }, [chapterId, loadChapterContext, loadPages]);
-
- useEffect(() => {
- if (mode === 'classic' && currentPage > 0 && pages.length > 0) {
- loadPage(chapterId, currentPage).then(setCurrentPageData);
- }
- }, [chapterId, currentPage, loadPage, mode, pages.length]);
-
- const handleKeyDown = useCallback((e) => {
- if (mode === 'classic') {
- if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
- if (currentPage > 1) {
- setCurrentPage(currentPage - 1);
- } else if (context?.navigation.previous) {
- navigate(`/reader/${context.navigation.previous.id}`);
- }
- } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
- if (currentPage < pages.length) {
- setCurrentPage(currentPage + 1);
- } else if (context?.navigation.next) {
- navigate(`/reader/${context.navigation.next.id}`);
- }
- }
- }
- }, [currentPage, context, mode, navigate, pages.length, setCurrentPage]);
-
- useEffect(() => {
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [handleKeyDown]);
-
- const toggleFullscreen = () => {
- if (!document.fullscreenElement) {
- document.documentElement.requestFullscreen();
- setIsFullscreen(true);
- } else {
- document.exitFullscreen();
- setIsFullscreen(false);
- }
- };
-
- const handleImageClick = (e) => {
- if (mode !== 'classic') return;
-
- const rect = e.target.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const width = rect.width;
-
- if (x < width / 2) {
- // Clic sur la partie gauche
- if (currentPage > 1) {
- setCurrentPage(currentPage - 1);
- } else if (context?.navigation.previous) {
- navigate(`/reader/${context.navigation.previous.id}`);
- }
- } else {
- // Clic sur la partie droite
- if (currentPage < pages.length) {
- setCurrentPage(currentPage + 1);
- } else if (context?.navigation.next) {
- navigate(`/reader/${context.navigation.next.id}`);
- }
- }
- };
-
- const toolbarConfig = {
- leftSection: [
- { icon: faArrowLeft, navigateBack: true }
- ],
- rightSection: [
- {
- icon: mode === 'classic' ? faScroll : faBookOpen,
- onClick: () => setMode(mode === 'classic' ? 'scrolling' : 'classic'),
- label: `Mode ${mode === 'classic' ? 'défilement' : 'page par page'}`
- },
- {
- icon: isFullscreen ? faCompress : faExpand,
- onClick: toggleFullscreen,
- label: isFullscreen ? 'Quitter le plein écran' : 'Plein écran'
- },
- { icon: faList, onClick: () => {}, label: 'Chapitres' }
- ]
- };
-
- if (loading || (!currentPageData && mode === 'classic' && pages.length === 0)) {
- return (
-
-
-
-
Chargement du chapitre...
-
-
- );
- }
-
- if (error) {
- return (
-
-
{error}
-
-
- );
- }
-
- return (
-
- {/* Toolbar */}
-
-
-
-
- {context && (
-
- Chapitre {context.number}
-
- )}
-
-
-
-
- {/* Reader content */}
-
- {mode === 'classic' ? (
- // Mode classique
-
- {currentPageData && (
-

- )}
-
- Page {currentPage} of {pages.length}
-
-
- ) : (
- // Mode scrolling
-
- {pages.map((page, index) => (
-
- {page.base64Content ? (
-

- ) : (
-
- Chargement...
-
- )}
-
- ))}
-
- )}
-
-
- {/* Navigation buttons for classic mode */}
- {mode === 'classic' && (
- <>
-
-
- >
- )}
-
- );
-}
\ No newline at end of file