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
This commit is contained in:
parent
367b361eef
commit
71d6bb5ee9
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user