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
This commit was merged in pull request #23.
This commit is contained in:
2026-03-15 22:52:56 +01:00
4 changed files with 169 additions and 229 deletions

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
@click="$emit('edit', source)" @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 --> <!-- Header avec URL et icône externe -->
<div class="flex items-center justify-between mb-4"> <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"> <h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate" :title="source.cleanBaseUrl">
@@ -20,14 +20,14 @@
<!-- Badge type de scraping --> <!-- Badge type de scraping -->
<span <span
:class="getScrapingTypeBadgeClass(source.scrapingType)" :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' }} {{ source.scrapingType?.toLowerCase() || 'N/A' }}
</span> </span>
<!-- Badge orientation basé sur les sélecteurs --> <!-- Badge orientation basé sur les sélecteurs -->
<span <span
:class="getOrientationBadgeClass(source)" :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) }} {{ getOrientation(source) }}
</span> </span>
</div> </div>

View File

@@ -1,17 +1,7 @@
<template> <template>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"> <div>
<!-- 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>
<!-- Form --> <!-- Form -->
<form @submit.prevent="handleSubmit" class="p-6 space-y-6"> <form @submit.prevent="handleSubmit" class="space-y-6">
<!-- Base URL --> <!-- Base URL -->
<div> <div>
<label for="baseUrl" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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" v-model="form.baseUrl"
type="url" type="url"
required 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" /> placeholder="https://example.com" />
</div> </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 --> <!-- 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"> <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> Chapter URL Format <span class="text-gray-500">({slug}, {chapterNumber})</span>
</label> </label>
@@ -49,87 +26,89 @@
v-model="form.chapterUrlFormat" v-model="form.chapterUrlFormat"
type="text" type="text"
required 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}/" /> placeholder="https://example.com/manga/{slug}-{chapterNumber}/" />
</div> </div>
<!-- Next Page Selector --> <!-- Selectors -->
<div> <div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
<label for="nextPageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <div>
Next Page Selector <span class="text-gray-500">(let empty if vertical reader)</span> <label for="imageSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label> Image Selector
<input </label>
id="nextPageSelector" <input
v-model="form.nextPageSelector" id="imageSelector"
type="text" v-model="form.imageSelector"
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" type="text"
placeholder=".next-page" /> 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> </div>
<!-- Chapter Selector --> <!-- Scraping Type + Token -->
<div> <div class="border-t border-gray-200 dark:border-gray-700 pt-6 space-y-4">
<label for="chapterSelector" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <div>
Chapter Selector <span class="text-gray-500">(required for Javascript scraping)</span> <label for="scrapingType" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label> Scraping Type
<input </label>
id="chapterSelector" <select
v-model="form.chapterSelector" id="scrapingType"
type="text" v-model="form.scrapingType"
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" required
placeholder=".chapter-selector" /> 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">
</div> <option value="html">HTML</option>
<option value="javascript">Javascript</option>
</select>
</div>
<!-- Scraping Type --> <div>
<div> <label for="token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label for="scrapingType" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> Token
Scraping Type </label>
</label> <input
<select id="token"
id="scrapingType" v-model="form.token"
v-model="form.scrapingType" type="text"
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"
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" />
<option value="html">HTML</option> </div>
<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> </div>
<!-- Error message --> <!-- 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 }} {{ error }}
</div> </div>
</form> </form>
<!-- Test Configuration Section --> <!-- 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="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
<div class="flex items-center space-x-2 mb-4"> <div class="flex items-center space-x-2 mb-6">
<WrenchScrewdriverIcon class="w-5 h-5 text-gray-600 dark:text-gray-400" /> <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>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
@@ -141,40 +120,38 @@
id="testMangaSlug" id="testMangaSlug"
v-model="testData.mangaSlug" v-model="testData.mangaSlug"
type="text" 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" /> placeholder="manga-slug" />
</div> </div>
<div> <div>
<label for="testChapterNumber" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <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> </label>
<input <input
id="testChapterNumber" id="testChapterNumber"
v-model="testData.chapterNumber" v-model="testData.chapterNumber"
type="number" type="number"
step="0.1" 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" /> placeholder="1" />
</div> </div>
</div> </div>
<!-- Preview de l'URL qui sera testée --> <!-- Preview URL -->
<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 v-if="generatedTestUrl" class="mb-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="text-sm text-blue-800 dark:text-blue-200"> <p class="text-xs text-gray-500 dark:text-gray-400 mb-1">URL qui sera testée</p>
<strong>URL qui sera testée :</strong> <code class="text-xs text-gray-700 dark:text-gray-300 break-all">{{ generatedTestUrl }}</code>
<div class="mt-1 font-mono text-xs break-all">{{ generatedTestUrl }}</div>
</div>
</div> </div>
<button <button
type="button" type="button"
@click="testConfiguration" @click="testConfiguration"
:disabled="testing || !canTest" :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" /> <ArrowPathIcon v-if="testing" class="w-4 h-4 animate-spin" />
<PlayIcon v-else class="w-4 h-4" /> <PlayIcon v-else class="w-4 h-4" />
<span>Test Configuration</span> <span>Lancer le test</span>
</button> </button>
</div> </div>
</div> </div>
@@ -183,8 +160,6 @@
<script setup> <script setup>
import { import {
ArrowPathIcon, ArrowPathIcon,
Cog6ToothIcon,
PencilSquareIcon,
PlayIcon, PlayIcon,
WrenchScrewdriverIcon WrenchScrewdriverIcon
} from '@heroicons/vue/24/outline'; } from '@heroicons/vue/24/outline';
@@ -242,7 +217,6 @@ const generatedTestUrl = computed(() => {
.replace('{chapterNumber}', testData.value.chapterNumber); .replace('{chapterNumber}', testData.value.chapterNumber);
}); });
// Initialize form with source data if editing, clear if creating new
watch(() => props.source, (newSource) => { watch(() => props.source, (newSource) => {
if (newSource) { if (newSource) {
form.value = { form.value = {
@@ -255,7 +229,6 @@ watch(() => props.source, (newSource) => {
token: newSource.token || '' token: newSource.token || ''
}; };
} else { } else {
// Reset form when no source (creating new)
form.value = { form.value = {
baseUrl: '', baseUrl: '',
imageSelector: '', imageSelector: '',
@@ -272,6 +245,8 @@ const handleSubmit = () => {
emit('submit', { ...form.value }); emit('submit', { ...form.value });
}; };
defineExpose({ submitForm: handleSubmit });
const testConfiguration = async () => { const testConfiguration = async () => {
testing.value = true; testing.value = true;
try { try {

View File

@@ -3,72 +3,54 @@
<Toolbar :config="toolbarConfig" /> <Toolbar :config="toolbarConfig" />
<div class="overflow-y-auto flex-1"> <div class="overflow-y-auto flex-1">
<div class="container mx-auto px-4 py-6"> <div class="px-6 py-8">
<!-- 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>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loadingSources" class="flex justify-center py-12"> <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> </div>
<!-- Error State --> <!-- 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"> <div class="flex items-center">
<ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" /> <ExclamationTriangleIcon class="w-5 h-5 text-red-400 mr-2" />
<p class="text-red-800 dark:text-red-200">{{ sourcesError }}</p> <p class="text-red-800 dark:text-red-200">{{ sourcesError }}</p>
</div> </div>
<button <button
@click="contentSourceStore.loadSources()" @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 Réessayer
</button> </button>
</div> </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 --> <!-- Sources Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <section v-else class="border-t border-gray-200 dark:border-gray-700 pt-6">
<!-- Existing Sources --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ContentSourceCard <!-- Existing Sources -->
v-for="source in sources" <ContentSourceCard
:key="source.id" v-for="source in sources"
:source="source" :key="source.id"
@edit="editSource" :source="source"
@open-link="openSourceLink" /> @edit="editSource"
@open-link="openSourceLink" />
<!-- Add New Configuration Card --> <!-- Add New Configuration Card -->
<div <div
@click="addNewSource" @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"> 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" /> <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"> <span class="text-lg font-medium text-gray-600 dark:text-gray-400 mb-2">
Add New Configuration Add New Configuration
</span> </span>
</div>
</div> </div>
</div> </section>
<!-- Import/Export Success Messages --> <!-- 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 ! Configuration importée avec succès !
</div> </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 ! Configuration exportée !
</div> </div>
</div> </div>
@@ -76,12 +58,12 @@
<!-- Import Modal --> <!-- 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 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"> <div class="p-6">
<h3 class="text-lg font-semibold mb-4">Importer des configurations</h3> <h3 class="text-lg font-semibold mb-4">Importer des configurations</h3>
<textarea <textarea
v-model="importData" 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> placeholder="Collez ici le JSON des configurations à importer..."></textarea>
<div class="flex justify-end space-x-3 mt-4"> <div class="flex justify-end space-x-3 mt-4">
@@ -93,7 +75,7 @@
<button <button
@click="handleImport" @click="handleImport"
:disabled="importing || !importData.trim()" :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' }} {{ importing ? 'Import...' : 'Importer' }}
</button> </button>
</div> </div>
@@ -149,29 +131,13 @@ onMounted(async () => {
// Toolbar configuration // Toolbar configuration
const toolbarConfig = computed(() => ({ const toolbarConfig = computed(() => ({
leftSection: [ leftSection: [
{ { type: 'label', text: 'Scrapers', class: 'text-sm font-medium' },
icon: ArrowPathIcon,
label: 'Actualiser',
type: 'button',
onClick: () => contentSourceStore.loadSources(),
active: loadingSources.value
}
], ],
rightSection: [ rightSection: [
{ { type: 'button', icon: ArrowPathIcon, label: 'Actualiser', onClick: () => contentSourceStore.loadSources(), disabled: loadingSources.value },
icon: ArrowDownTrayIcon, { type: 'button', icon: ArrowDownTrayIcon, label: 'Exporter', onClick: handleExport, disabled: exporting.value },
label: 'Exporter', { type: 'button', icon: ArrowUpTrayIcon, label: 'Importer', onClick: () => showImportModal.value = true },
type: 'button', ],
onClick: handleExport,
disabled: exporting.value
},
{
icon: ArrowUpTrayIcon,
label: 'Importer',
type: 'button',
onClick: () => showImportModal.value = true
}
]
})); }));
// Actions // Actions

View File

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