feat: ajout de la gestion des sources préférées pour les mangas, incluant la récupération et la configuration des sources via l'API, ainsi que l'intégration d'une modale pour l'interface utilisateur.
This commit is contained in:
parent
15d92d1aff
commit
75f8e1686c
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :show="isOpen">
|
||||
<Dialog as="div" class="relative z-50" @close="closeModal">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div>
|
||||
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
|
||||
<Cog6ToothIcon class="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">
|
||||
Sources préférées
|
||||
</DialogTitle>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Configurez l'ordre de priorité des sources pour ce manga. Les sources en haut de la liste seront privilégiées.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="mt-5 flex justify-center items-center py-8">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-else-if="error" class="mt-5 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{{ error.message || 'Une erreur est survenue lors du chargement des sources.' }}
|
||||
</div>
|
||||
|
||||
<!-- Sources list -->
|
||||
<div v-else class="mt-5">
|
||||
<div v-if="localSources.length === 0" class="text-center py-8 text-gray-500">
|
||||
Aucune source disponible
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(source, index) in localSources"
|
||||
:key="source.id"
|
||||
class="flex items-center p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||
>
|
||||
<div class="flex items-center space-x-2 mr-3">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
:disabled="index === 0"
|
||||
@click="moveUp(index)"
|
||||
>
|
||||
<ChevronUpIcon class="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
||||
:disabled="index === localSources.length - 1"
|
||||
@click="moveDown(index)"
|
||||
>
|
||||
<ChevronDownIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-900">{{ source.name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ source.description || source.baseUrl }}</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400">
|
||||
Priorité {{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 sm:col-start-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isSaving || isLoading"
|
||||
@click="saveChanges"
|
||||
>
|
||||
<div v-if="isSaving" class="flex items-center">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Sauvegarde...
|
||||
</div>
|
||||
<span v-else>Sauvegarder</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:col-start-1 sm:mt-0"
|
||||
@click="closeModal"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||
import { ChevronDownIcon, ChevronUpIcon, Cog6ToothIcon } from '@heroicons/vue/24/outline';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
sources: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
// Copie locale des sources pour le drag & drop
|
||||
const localSources = ref([]);
|
||||
|
||||
// Watcher pour mettre à jour la copie locale quand les props changent
|
||||
watch(
|
||||
() => props.sources,
|
||||
(newSources) => {
|
||||
localSources.value = [...newSources];
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const closeModal = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const moveUp = (index) => {
|
||||
if (index > 0) {
|
||||
const sources = [...localSources.value];
|
||||
[sources[index - 1], sources[index]] = [sources[index], sources[index - 1]];
|
||||
localSources.value = sources;
|
||||
}
|
||||
};
|
||||
|
||||
const moveDown = (index) => {
|
||||
if (index < localSources.value.length - 1) {
|
||||
const sources = [...localSources.value];
|
||||
[sources[index], sources[index + 1]] = [sources[index + 1], sources[index]];
|
||||
localSources.value = sources;
|
||||
}
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
// Extraire seulement les IDs dans l'ordre actuel
|
||||
const sourceIds = localSources.value.map(source => source.id);
|
||||
emit('save', sourceIds);
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user