8 Commits

Author SHA1 Message Date
b0ce36096f Merge pull request 'feat(ui): harmoniser les pages Scrapers sur le design system Mangarr' (#23) from feat/ui-scrapers-harmonization into main
All checks were successful
Deploy / deploy (push) Successful in 2m58s
Reviewed-on: #23
2026-03-15 22:52:56 +01:00
ext.jeremy.guillot@maxicoffee.domains
da8a19cbcb feat(ui): harmoniser les pages Scrapers sur le design system Mangarr
- Layout canonique px-6 py-8 + sections border-t (suppression container mx-auto)
- Toolbar : label titre + bouton retour (ScrapperEdit) + boutons actions (ScrapperConfigurations)
- Bouton submit déplacé dans la toolbar droite via defineExpose/ref
- ContentSourceForm aplati (suppression du wrapper carte et du header)
- Séparation des sections du formulaire par border-t
- Suppression de tous les rounded-* sur les 4 composants
- Suppression du bloc debug "aucune source" et du h1 volant
2026-03-15 22:52:23 +01:00
ext.jeremy.guillot@maxicoffee.domains
367b361eef fix(manga): afficher la plage de chapitres au lieu du numéro de volume dans la liste
All checks were successful
Deploy / deploy (push) Successful in 2m58s
Pour les chapitres regroupés en volume (isVolumeGroup), la colonne "#" affichait
"Vol. X" au lieu du numéro/plage de chapitres. Remplacé par volumeChaptersRange.
2026-03-15 22:21:19 +01:00
ext.jeremy.guillot@maxicoffee.domains
9c5ae4bf16 fix(scheduler): désactiver MainSchedule legacy au profit de MonitoringSchedule DDD
All checks were successful
Deploy / deploy (push) Successful in 2m53s
MainSchedule (toutes les 6h) et MonitoringSchedule (toutes les 2h) tournaient
en parallèle sur les mêmes mangas surveillés, causant des doubles appels MangaDex
et des doublons de scraping.
2026-03-15 22:08:46 +01:00
ext.jeremy.guillot@maxicoffee.domains
6b58e94fc3 fix(manga): corriger le conflit de shortName sur MangaDiscoverResource
All checks were successful
Deploy / deploy (push) Successful in 2m56s
2026-03-15 21:56:41 +01:00
e78bc890ef Merge pull request 'feat(manga): implémenter la page Découvrir avec recommandations MangaDex' (#22) from feat/discover-page into main
All checks were successful
Deploy / deploy (push) Successful in 2m50s
Reviewed-on: #22
2026-03-15 21:44:43 +01:00
47c33d549b Merge branch 'main' into feat/discover-page 2026-03-15 21:44:28 +01:00
1478b460ba Merge pull request 'style(manga): refondre la page d'ajout de manga sur le design system' (#21) from style/add-manga-ui-redesign into main
All checks were successful
Deploy / deploy (push) Successful in 2m45s
Reviewed-on: #21
2026-03-15 20:56:29 +01:00
7 changed files with 172 additions and 233 deletions

View File

@@ -1,7 +1,7 @@
<template>
<tr class="border-t dark:border-gray-700 hover:bg-green-100 dark:hover:bg-green-900/20">
<td class="px-4 py-2 text-gray-900 dark:text-gray-100" :class="{ 'text-green-500 dark:text-green-400': chapter.isAvailable }">
<template v-if="chapter.isVolumeGroup">Vol. {{ chapter.volume }}</template>
<template v-if="chapter.isVolumeGroup">{{ chapter.volumeChaptersRange }}</template>
<template v-else>{{ String(chapter.number).padStart(2, '0') }}</template>
</td>
<td class="px-4 py-2 w-full text-left text-gray-900 dark:text-gray-100">

View File

@@ -1,7 +1,7 @@
<template>
<div
@click="$emit('edit', source)"
class="bg-white dark:bg-gray-800 rounded-lg shadow-md border border-gray-200 dark:border-gray-700 p-6 hover:shadow-lg transition-shadow duration-200 cursor-pointer">
class="bg-white dark:bg-gray-800 shadow-md border border-gray-200 dark:border-gray-700 p-6 hover:shadow-lg transition-shadow duration-200 cursor-pointer">
<!-- Header avec URL et icône externe -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate" :title="source.cleanBaseUrl">
@@ -20,14 +20,14 @@
<!-- Badge type de scraping -->
<span
:class="getScrapingTypeBadgeClass(source.scrapingType)"
class="px-2 py-1 text-xs font-medium rounded-md">
class="px-2 py-1 text-xs font-medium">
{{ source.scrapingType?.toLowerCase() || 'N/A' }}
</span>
<!-- Badge orientation basé sur les sélecteurs -->
<span
:class="getOrientationBadgeClass(source)"
class="px-2 py-1 text-xs font-medium rounded-md">
class="px-2 py-1 text-xs font-medium">
{{ getOrientation(source) }}
</span>
</div>

View File

@@ -1,17 +1,7 @@
<template>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
<!-- Header -->
<div class="bg-gray-50 dark:bg-gray-700 px-6 py-4 border-b border-gray-200 dark:border-gray-600 rounded-t-lg">
<div class="flex items-center space-x-2">
<Cog6ToothIcon class="w-5 h-5 text-gray-600 dark:text-gray-400" />
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ isEditing ? 'Edit Scrapper Configuration' : 'New Scrapper Configuration' }}
</h2>
</div>
</div>
<div>
<!-- Form -->
<form @submit.prevent="handleSubmit" class="p-6 space-y-6">
<form @submit.prevent="handleSubmit" class="space-y-6">
<!-- Base URL -->
<div>
<label for="baseUrl" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@@ -22,25 +12,12 @@
v-model="form.baseUrl"
type="url"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="https://example.com" />
</div>
<!-- Image Selector -->
<div>
<label for="imageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Image Selector
</label>
<input
id="imageSelector"
v-model="form.imageSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".reading-content .page-break img" />
</div>
<!-- Chapter URL Format -->
<div>
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<label for="chapterUrlFormat" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Chapter URL Format <span class="text-gray-500">({slug}, {chapterNumber})</span>
</label>
@@ -49,87 +26,89 @@
v-model="form.chapterUrlFormat"
type="text"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="https://example.com/manga/{slug}-{chapterNumber}/" />
</div>
<!-- Next Page Selector -->
<div>
<label for="nextPageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Next Page Selector <span class="text-gray-500">(let empty if vertical reader)</span>
</label>
<input
id="nextPageSelector"
v-model="form.nextPageSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".next-page" />
<!-- Selectors -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
<div>
<label for="imageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Image Selector
</label>
<input
id="imageSelector"
v-model="form.imageSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".reading-content .page-break img" />
</div>
<div>
<label for="nextPageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Next Page Selector <span class="text-gray-500">(laisser vide si lecteur vertical)</span>
</label>
<input
id="nextPageSelector"
v-model="form.nextPageSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".next-page" />
</div>
<div>
<label for="chapterSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Chapter Selector <span class="text-gray-500">(requis pour le scraping Javascript)</span>
</label>
<input
id="chapterSelector"
v-model="form.chapterSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".chapter-selector" />
</div>
</div>
<!-- Chapter Selector -->
<div>
<label for="chapterSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Chapter Selector <span class="text-gray-500">(required for Javascript scraping)</span>
</label>
<input
id="chapterSelector"
v-model="form.chapterSelector"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder=".chapter-selector" />
</div>
<!-- Scraping Type + Token -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
<div>
<label for="scrapingType" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Scraping Type
</label>
<select
id="scrapingType"
v-model="form.scrapingType"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white">
<option value="html">HTML</option>
<option value="javascript">Javascript</option>
</select>
</div>
<!-- Scraping Type -->
<div>
<label for="scrapingType" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Scraping Type
</label>
<select
id="scrapingType"
v-model="form.scrapingType"
required
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white">
<option value="html">HTML</option>
<option value="javascript">Javascript</option>
</select>
</div>
<!-- Token (optionnel) -->
<div>
<label for="token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Token
</label>
<input
id="token"
v-model="form.token"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="Optional authentication token" />
</div>
<!-- Submit Button -->
<div class="flex justify-end">
<button
type="submit"
:disabled="saving"
class="px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white font-medium rounded-md transition-colors duration-200 flex items-center space-x-2">
<ArrowPathIcon v-if="saving" class="w-4 h-4 animate-spin" />
<span>{{ isEditing ? 'Update Configuration' : 'Create Configuration' }}</span>
<PencilSquareIcon v-if="!saving" class="w-4 h-4" />
</button>
<div>
<label for="token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Token
</label>
<input
id="token"
v-model="form.token"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="Optional authentication token" />
</div>
</div>
<!-- Error message -->
<div v-if="error" class="text-red-600 dark:text-red-400 text-sm">
<div v-if="error" class="border-t border-gray-200 dark:border-gray-700 pt-6 text-red-600 dark:text-red-400 text-sm">
{{ error }}
</div>
</form>
<!-- Test Configuration Section -->
<div class="border-t border-gray-200 dark:border-gray-600 p-6 bg-gray-50 dark:bg-gray-700 rounded-b-lg">
<div class="flex items-center space-x-2 mb-4">
<div class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
<div class="flex items-center space-x-2 mb-6">
<WrenchScrewdriverIcon class="w-5 h-5 text-gray-600 dark:text-gray-400" />
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Test Configuration</h3>
<h3 class="text-sm font-medium text-gray-900 dark:text-white">Test de la configuration</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
@@ -141,40 +120,38 @@
id="testMangaSlug"
v-model="testData.mangaSlug"
type="text"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="manga-slug" />
</div>
<div>
<label for="testChapterNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Chapter Number
Numéro de chapitre
</label>
<input
id="testChapterNumber"
v-model="testData.chapterNumber"
type="number"
step="0.1"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
placeholder="1" />
</div>
</div>
<!-- Preview de l'URL qui sera testée -->
<div v-if="generatedTestUrl" class="mb-4 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md">
<div class="text-sm text-blue-800 dark:text-blue-200">
<strong>URL qui sera testée :</strong>
<div class="mt-1 font-mono text-xs break-all">{{ generatedTestUrl }}</div>
</div>
<!-- Preview URL -->
<div v-if="generatedTestUrl" class="mb-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-xs text-gray-500 dark:text-gray-400 mb-1">URL qui sera testée</p>
<code class="text-xs text-gray-700 dark:text-gray-300 break-all">{{ generatedTestUrl }}</code>
</div>
<button
type="button"
@click="testConfiguration"
:disabled="testing || !canTest"
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium rounded-md transition-colors duration-200 flex items-center justify-center space-x-2">
class="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium transition-colors duration-200 flex items-center justify-center space-x-2">
<ArrowPathIcon v-if="testing" class="w-4 h-4 animate-spin" />
<PlayIcon v-else class="w-4 h-4" />
<span>Test Configuration</span>
<span>Lancer le test</span>
</button>
</div>
</div>
@@ -183,8 +160,6 @@
<script setup>
import {
ArrowPathIcon,
Cog6ToothIcon,
PencilSquareIcon,
PlayIcon,
WrenchScrewdriverIcon
} from '@heroicons/vue/24/outline';
@@ -242,7 +217,6 @@ const generatedTestUrl = computed(() => {
.replace('{chapterNumber}', testData.value.chapterNumber);
});
// Initialize form with source data if editing, clear if creating new
watch(() => props.source, (newSource) => {
if (newSource) {
form.value = {
@@ -255,7 +229,6 @@ watch(() => props.source, (newSource) => {
token: newSource.token || ''
};
} else {
// Reset form when no source (creating new)
form.value = {
baseUrl: '',
imageSelector: '',
@@ -272,6 +245,8 @@ const handleSubmit = () => {
emit('submit', { ...form.value });
};
defineExpose({ submitForm: handleSubmit });
const testConfiguration = async () => {
testing.value = true;
try {

View File

@@ -3,72 +3,54 @@
<Toolbar :config="toolbarConfig" />
<div class="overflow-y-auto flex-1">
<div class="container mx-auto px-4 py-6">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Scrapper Configurations
</h1>
<p class="text-gray-600 dark:text-gray-400">
Gérez les configurations de scraping pour les différentes sources de manga
</p>
</div>
<div class="px-6 py-8">
<!-- Loading State -->
<div v-if="loadingSources" class="flex justify-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<div class="animate-spin h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div v-else-if="sourcesError" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-6">
<div v-else-if="sourcesError" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 p-4 mb-6">
<div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" />
<p class="text-red-800 dark:text-red-200">{{ sourcesError }}</p>
</div>
<button
@click="contentSourceStore.loadSources()"
class="mt-3 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
class="mt-3 px-4 py-2 bg-red-600 text-white hover:bg-red-700">
Réessayer
</button>
</div>
<!-- Debug Info (temporary) -->
<div v-if="!loadingSources && !sourcesError && sources.length === 0" class="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg p-4 mb-6">
<p class="text-blue-800 dark:text-blue-200">Aucune source trouvée. Rechargement en cours...</p>
<button
@click="contentSourceStore.loadSources()"
class="mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Actualiser
</button>
</div>
<!-- Sources Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Existing Sources -->
<ContentSourceCard
v-for="source in sources"
:key="source.id"
:source="source"
@edit="editSource"
@open-link="openSourceLink" />
<section v-else class="border-t border-gray-200 dark:border-gray-700 pt-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Existing Sources -->
<ContentSourceCard
v-for="source in sources"
:key="source.id"
:source="source"
@edit="editSource"
@open-link="openSourceLink" />
<!-- Add New Configuration Card -->
<div
@click="addNewSource"
class="bg-gray-50 dark:bg-gray-700 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 hover:border-gray-400 dark:hover:border-gray-500 transition-colors cursor-pointer flex flex-col items-center justify-center h-full">
<PlusIcon class="w-8 h-8 text-gray-400 dark:text-gray-500 mb-3" />
<span class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-2">
Add New Configuration
</span>
<!-- Add New Configuration Card -->
<div
@click="addNewSource"
class="bg-gray-50 dark:bg-gray-700 border-2 border-dashed border-gray-300 dark:border-gray-600 p-6 hover:border-gray-400 dark:hover:border-gray-500 transition-colors cursor-pointer flex flex-col items-center justify-center h-full">
<PlusIcon class="w-8 h-8 text-gray-400 dark:text-gray-500 mb-3" />
<span class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-2">
Add New Configuration
</span>
</div>
</div>
</div>
</section>
<!-- Import/Export Success Messages -->
<div v-if="showImportSuccess" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg">
<div v-if="showImportSuccess" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 shadow-lg">
Configuration importée avec succès !
</div>
<div v-if="showExportSuccess" class="fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg">
<div v-if="showExportSuccess" class="fixed bottom-4 right-4 bg-blue-500 text-white px-4 py-2 shadow-lg">
Configuration exportée !
</div>
</div>
@@ -76,12 +58,12 @@
<!-- Import Modal -->
<div v-if="showImportModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
<div class="bg-white dark:bg-gray-800 shadow-xl w-full max-w-md">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Importer des configurations</h3>
<textarea
v-model="importData"
class="w-full h-40 p-3 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white"
class="w-full h-40 p-3 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Collez ici le JSON des configurations à importer..."></textarea>
<div class="flex justify-end space-x-3 mt-4">
@@ -93,7 +75,7 @@
<button
@click="handleImport"
:disabled="importing || !importData.trim()"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-md">
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white">
{{ importing ? 'Import...' : 'Importer' }}
</button>
</div>
@@ -149,29 +131,13 @@ onMounted(async () => {
// Toolbar configuration
const toolbarConfig = computed(() => ({
leftSection: [
{
icon: ArrowPathIcon,
label: 'Actualiser',
type: 'button',
onClick: () => contentSourceStore.loadSources(),
active: loadingSources.value
}
{ type: 'label', text: 'Scrapers', class: 'text-sm font-medium' },
],
rightSection: [
{
icon: ArrowDownTrayIcon,
label: 'Exporter',
type: 'button',
onClick: handleExport,
disabled: exporting.value
},
{
icon: ArrowUpTrayIcon,
label: 'Importer',
type: 'button',
onClick: () => showImportModal.value = true
}
]
{ type: 'button', icon: ArrowPathIcon, label: 'Actualiser', onClick: () => contentSourceStore.loadSources(), disabled: loadingSources.value },
{ type: 'button', icon: ArrowDownTrayIcon, label: 'Exporter', onClick: handleExport, disabled: exporting.value },
{ type: 'button', icon: ArrowUpTrayIcon, label: 'Importer', onClick: () => showImportModal.value = true },
],
}));
// Actions

View File

@@ -3,43 +3,36 @@
<Toolbar :config="toolbarConfig" />
<div class="overflow-y-auto flex-1">
<div class="container mx-auto px-4 py-6">
<!-- Back Navigation -->
<div class="mb-6">
<button
@click="goBack"
class="flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors">
<ArrowLeftIcon class="w-5 h-5" />
<span>Retour aux configurations</span>
</button>
</div>
<!-- Loading State -->
<div v-if="loadingCurrentSource" class="flex justify-center py-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<!-- Error State -->
<div v-else-if="currentSourceError" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-6">
<div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" />
<p class="text-red-800 dark:text-red-200">{{ currentSourceError }}</p>
<div class="px-6 py-8">
<section class="border-t border-gray-200 dark:border-gray-700 pt-6">
<!-- Loading State -->
<div v-if="loadingCurrentSource" class="flex justify-center py-12">
<div class="animate-spin h-12 w-12 border-b-2 border-blue-500"></div>
</div>
</div>
<!-- Form -->
<div v-else class="max-w-4xl mx-auto">
<ContentSourceForm
:source="currentSource"
:saving="saving"
:error="saveError"
@submit="handleSubmit"
@test="handleTest" />
</div>
<!-- Error State -->
<div v-else-if="currentSourceError" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 p-4 mb-6">
<div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" />
<p class="text-red-800 dark:text-red-200">{{ currentSourceError }}</p>
</div>
</div>
<!-- Form -->
<div v-else>
<ContentSourceForm
ref="formRef"
:source="currentSource"
:saving="saving"
:error="saveError"
@submit="handleSubmit"
@test="handleTest" />
</div>
</section>
<!-- Test Results Modal -->
<div v-if="showTestResults" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl max-h-[80vh] overflow-hidden">
<div class="bg-white dark:bg-gray-800 shadow-xl w-full max-w-4xl max-h-[80vh] overflow-hidden">
<div class="p-6 border-b border-gray-200 dark:border-gray-600">
<div class="flex justify-between items-center">
<h3 class="text-lg font-semibold">Résultats du test</h3>
@@ -54,7 +47,7 @@
<div class="p-6 overflow-y-auto">
<!-- Loading state during test -->
<div v-if="testingConfiguration" class="flex items-center justify-center py-8">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<div class="animate-spin h-8 w-8 border-b-2 border-blue-500 mr-3"></div>
<span class="text-gray-600">Test en cours...</span>
</div>
@@ -65,7 +58,7 @@
<span class="font-medium">Test réussi !</span>
</div>
<div class="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg p-4">
<div class="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 p-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium text-green-800 dark:text-green-200">URL testée:</span>
@@ -92,10 +85,10 @@
<img
:src="imageUrl"
:alt="`Image ${index + 1}`"
class="w-full h-32 object-cover rounded border border-gray-200 dark:border-gray-600"
class="w-full h-32 object-cover border border-gray-200 dark:border-gray-600"
@error="handleImageError"
@load="handleImageLoad" />
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-opacity rounded flex items-center justify-center">
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-opacity flex items-center justify-center">
<span class="text-white opacity-0 group-hover:opacity-100 text-sm font-medium">
Page {{ index + 1 }}
</span>
@@ -107,7 +100,7 @@
</p>
</div>
<div v-else class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-lg p-4">
<div v-else class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 p-4">
<div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-yellow-400 mr-2" />
<p class="text-yellow-800 dark:text-yellow-200">
@@ -125,7 +118,7 @@
<span class="font-medium">Test échoué</span>
</div>
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg p-4 mb-4">
<div class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 p-4 mb-4">
<div class="text-sm text-red-800 dark:text-red-200">
<div><strong>URL testée:</strong> {{ testResults.testedUrl || 'N/A' }}</div>
<div><strong>Type de scraping:</strong> {{ testResults.scrapingType || 'N/A' }}</div>
@@ -138,14 +131,14 @@
<div
v-for="(error, index) in testResults.errors"
:key="index"
class="bg-red-100 dark:bg-red-800 border-l-4 border-red-400 p-4 rounded">
class="bg-red-100 dark:bg-red-800 border-l-4 border-red-400 p-4">
<div class="flex items-start">
<div class="flex-shrink-0">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400" />
</div>
<div class="ml-3 flex-1">
<div class="flex items-center mb-1">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200 mr-2">
<span class="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-200 text-red-800 dark:bg-red-700 dark:text-red-200 mr-2">
{{ formatErrorType(error.type) }}
</span>
<span class="text-sm font-medium text-red-800 dark:text-red-200">
@@ -155,7 +148,7 @@
<p class="text-sm text-red-700 dark:text-red-300 mb-2">
{{ error.message }}
</p>
<div class="bg-red-50 dark:bg-red-900 rounded p-2">
<div class="bg-red-50 dark:bg-red-900 p-2">
<p class="text-xs text-red-600 dark:text-red-400">
<strong>Suggestion :</strong> {{ error.suggestion }}
</p>
@@ -166,7 +159,7 @@
</div>
<!-- Generic Error -->
<div v-else-if="testResults.error" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded p-3">
<div v-else-if="testResults.error" class="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 p-3">
<code class="text-sm text-red-800 dark:text-red-200">
{{ testResults.error }}
</code>
@@ -177,7 +170,7 @@
</div>
<!-- Success Message -->
<div v-if="showSuccessMessage" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg">
<div v-if="showSuccessMessage" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 shadow-lg">
Configuration {{ isEditing ? 'mise à jour' : 'créée' }} avec succès !
</div>
</div>
@@ -190,6 +183,7 @@ import {
ArrowLeftIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
PencilSquareIcon,
XCircleIcon,
XMarkIcon
} from '@heroicons/vue/24/outline';
@@ -214,6 +208,9 @@ const {
saveError
} = storeToRefs(contentSourceStore);
// Form ref
const formRef = ref(null);
// Local state
const showTestResults = ref(false);
const showSuccessMessage = ref(false);
@@ -233,16 +230,18 @@ onMounted(async () => {
});
// Toolbar configuration
const toolbarConfig = {
leftSection: [],
rightSection: []
};
const toolbarConfig = computed(() => ({
leftSection: [
{ type: 'button', icon: ArrowLeftIcon, label: 'Retour', onClick: () => router.push({ name: 'scrapper-configurations' }) },
{ type: 'divider' },
{ type: 'label', text: isEditing.value ? 'Modifier la configuration' : 'Nouvelle configuration', class: 'text-sm font-medium' },
],
rightSection: [
{ type: 'button', icon: PencilSquareIcon, label: isEditing.value ? 'Mettre à jour' : 'Créer', onClick: () => formRef.value?.submitForm(), disabled: saving.value },
],
}));
// Actions
const goBack = () => {
router.push({ name: 'scrapper-configurations' });
};
const handleSubmit = async (formData) => {
try {
if (isEditing.value) {

View File

@@ -8,7 +8,7 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaSearchCollection;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\DiscoverMangaStateProvider;
#[ApiResource(
shortName: 'Mangadex',
shortName: 'MangaDiscover',
operations: [
new Get(
uriTemplate: '/manga-discover',

View File

@@ -3,13 +3,12 @@
namespace App\Scheduler;
use App\Message\RefreshAndDownloadChapters;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;
use Symfony\Contracts\Cache\CacheInterface;
#[AsSchedule]
// Désactivé : remplacé par MonitoringSchedule (DDD) dans src/Domain/Manga/Infrastructure/Scheduler/
class MainSchedule implements ScheduleProviderInterface
{
public function __construct(private CacheInterface $cache)