231 lines
8.5 KiB
Vue
231 lines
8.5 KiB
Vue
<template>
|
|
<div>
|
|
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
|
|
|
|
<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>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- 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 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">
|
|
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" />
|
|
|
|
<!-- 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>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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">
|
|
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">
|
|
Configuration exportée !
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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="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"
|
|
placeholder="Collez ici le JSON des configurations à importer..."></textarea>
|
|
|
|
<div class="flex justify-end space-x-3 mt-4">
|
|
<button
|
|
@click="showImportModal = false"
|
|
class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
|
Annuler
|
|
</button>
|
|
<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">
|
|
{{ importing ? 'Import...' : 'Importer' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
ArrowDownTrayIcon,
|
|
ArrowPathIcon,
|
|
ArrowUpTrayIcon,
|
|
ExclamationTriangleIcon,
|
|
PlusIcon
|
|
} from '@heroicons/vue/24/outline';
|
|
import { storeToRefs } from 'pinia';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
|
import { useContentSourceStore } from '../../application/store/contentSourceStore';
|
|
import ContentSourceCard from '../components/ContentSourceCard.vue';
|
|
|
|
const router = useRouter();
|
|
const contentSourceStore = useContentSourceStore();
|
|
|
|
const {
|
|
sources,
|
|
loadingSources,
|
|
sourcesError,
|
|
importing,
|
|
exporting
|
|
} = storeToRefs(contentSourceStore);
|
|
|
|
// Local state
|
|
const showImportModal = ref(false);
|
|
const showExportSuccess = ref(false);
|
|
const showImportSuccess = ref(false);
|
|
const importData = ref('');
|
|
|
|
// Load sources on mount and clear current source
|
|
onMounted(async () => {
|
|
try {
|
|
contentSourceStore.clearCurrentSource(); // Clear any previously loaded source
|
|
contentSourceStore.clearErrors(); // Clear any previous errors
|
|
await contentSourceStore.loadSources();
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des sources:', error);
|
|
}
|
|
});
|
|
|
|
// Toolbar configuration
|
|
const toolbarConfig = computed(() => ({
|
|
leftSection: [
|
|
{
|
|
icon: ArrowPathIcon,
|
|
label: 'Actualiser',
|
|
type: 'button',
|
|
onClick: () => contentSourceStore.loadSources(),
|
|
active: loadingSources.value
|
|
}
|
|
],
|
|
rightSection: [
|
|
{
|
|
icon: ArrowDownTrayIcon,
|
|
label: 'Exporter',
|
|
type: 'button',
|
|
onClick: handleExport,
|
|
disabled: exporting.value
|
|
},
|
|
{
|
|
icon: ArrowUpTrayIcon,
|
|
label: 'Importer',
|
|
type: 'button',
|
|
onClick: () => showImportModal.value = true
|
|
}
|
|
]
|
|
}));
|
|
|
|
// Actions
|
|
const editSource = (source) => {
|
|
router.push({
|
|
name: 'scrapper-edit',
|
|
params: { id: source.id }
|
|
});
|
|
};
|
|
|
|
const addNewSource = () => {
|
|
router.push({ name: 'scrapper-new' });
|
|
};
|
|
|
|
const openSourceLink = (url) => {
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
async function handleExport() {
|
|
try {
|
|
const exportData = await contentSourceStore.exportSources();
|
|
|
|
// Create and download file
|
|
const dataStr = JSON.stringify(exportData, null, 2);
|
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
const url = URL.createObjectURL(dataBlob);
|
|
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `scrapper-configurations-${new Date().toISOString().split('T')[0]}.json`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
showExportSuccess.value = true;
|
|
setTimeout(() => showExportSuccess.value = false, 3000);
|
|
} catch (error) {
|
|
console.error('Erreur lors de l\'export:', error);
|
|
}
|
|
}
|
|
|
|
async function handleImport() {
|
|
try {
|
|
const data = JSON.parse(importData.value);
|
|
await contentSourceStore.importSources(data);
|
|
|
|
showImportModal.value = false;
|
|
importData.value = '';
|
|
showImportSuccess.value = true;
|
|
setTimeout(() => showImportSuccess.value = false, 3000);
|
|
} catch (error) {
|
|
console.error('Erreur lors de l\'import:', error);
|
|
alert('Erreur: Format JSON invalide ou erreur serveur');
|
|
}
|
|
}
|
|
</script>
|