372 lines
19 KiB
Vue
372 lines
19 KiB
Vue
<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-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
|
|
<div class="mb-6">
|
|
<DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900">
|
|
Edit Manga
|
|
</DialogTitle>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div v-if="error" class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
|
{{ error.message || 'Une erreur est survenue lors de la sauvegarde.' }}
|
|
</div>
|
|
|
|
<!-- Form -->
|
|
<form @submit.prevent="saveChanges" class="space-y-6">
|
|
<!-- Titre et Slug -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Titre</label>
|
|
<input
|
|
id="title"
|
|
v-model="formData.title"
|
|
type="text"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="Titre du manga"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">Slug</label>
|
|
<input
|
|
id="slug"
|
|
:value="manga?.slug || ''"
|
|
type="text"
|
|
disabled
|
|
class="block w-full rounded-md border-gray-300 bg-gray-50 shadow-sm sm:text-sm text-gray-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Année de publication -->
|
|
<div>
|
|
<label for="publicationYear" class="block text-sm font-medium text-gray-700 mb-2">Année de publication</label>
|
|
<input
|
|
id="publicationYear"
|
|
v-model.number="formData.publicationYear"
|
|
type="number"
|
|
min="1900"
|
|
:max="new Date().getFullYear()"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="2023"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div>
|
|
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
|
<textarea
|
|
id="description"
|
|
v-model="formData.description"
|
|
rows="4"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="Description du manga"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Auteur et Statut -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<label for="author" class="block text-sm font-medium text-gray-700 mb-2">Auteur</label>
|
|
<input
|
|
id="author"
|
|
v-model="formData.author"
|
|
type="text"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="Auteur du manga"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">Statut</label>
|
|
<input
|
|
id="status"
|
|
v-model="formData.status"
|
|
type="text"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="ongoing"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Note -->
|
|
<div>
|
|
<label for="rating" class="block text-sm font-medium text-gray-700 mb-2">Note</label>
|
|
<input
|
|
id="rating"
|
|
v-model.number="formData.rating"
|
|
type="number"
|
|
min="0"
|
|
max="10"
|
|
step="0.001"
|
|
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="9.541"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Slugs alternatifs -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Slugs alternatifs</label>
|
|
<div class="space-y-2">
|
|
<div v-if="formData.alternativeSlugs.length > 0" class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="(slug, index) in formData.alternativeSlugs"
|
|
:key="index"
|
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
|
|
>
|
|
{{ slug }}
|
|
<button
|
|
type="button"
|
|
@click="removeAlternativeSlug(index)"
|
|
class="ml-1.5 inline-flex items-center justify-center w-4 h-4 rounded-full text-green-400 hover:text-green-600"
|
|
>
|
|
<XMarkIcon class="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
@click="showAlternativeSlugInput = !showAlternativeSlugInput"
|
|
class="text-green-600 hover:text-green-700 text-sm font-medium"
|
|
>
|
|
+ Ajouter un slug alternatif
|
|
</button>
|
|
<div v-if="showAlternativeSlugInput" class="flex gap-2">
|
|
<input
|
|
v-model="newAlternativeSlug"
|
|
type="text"
|
|
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="Nouveau slug alternatif"
|
|
@keyup.enter="addAlternativeSlug"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="addAlternativeSlug"
|
|
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
|
>
|
|
Ajouter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Genres -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">Genres</label>
|
|
<div class="space-y-3">
|
|
<div v-if="formData.genres.length > 0" class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
|
<span
|
|
v-for="(genre, index) in formData.genres"
|
|
:key="index"
|
|
class="inline-flex items-center justify-between px-3 py-1 rounded-md text-sm font-medium bg-gray-100 text-gray-800"
|
|
>
|
|
{{ genre }}
|
|
<button
|
|
type="button"
|
|
@click="removeGenre(index)"
|
|
class="ml-2 inline-flex items-center justify-center w-4 h-4 text-gray-400 hover:text-gray-600"
|
|
>
|
|
<XMarkIcon class="w-3 h-3" />
|
|
</button>
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
@click="showGenreInput = !showGenreInput"
|
|
class="text-green-600 hover:text-green-700 text-sm font-medium"
|
|
>
|
|
+ Ajouter un genre
|
|
</button>
|
|
<div v-if="showGenreInput" class="flex gap-2">
|
|
<input
|
|
v-model="newGenre"
|
|
type="text"
|
|
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
|
placeholder="Nouveau genre"
|
|
@keyup.enter="addGenre"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="addGenre"
|
|
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
|
>
|
|
Ajouter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Boutons -->
|
|
<div class="mt-8 flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
|
@click="closeModal"
|
|
:disabled="isSaving"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:disabled="isSaving"
|
|
@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>
|
|
Saving...
|
|
</div>
|
|
<span v-else>Save</span>
|
|
</button>
|
|
</div>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</TransitionRoot>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
|
import { XMarkIcon } from '@heroicons/vue/24/outline';
|
|
import { ref, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
isOpen: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
manga: {
|
|
type: Object,
|
|
default: null
|
|
},
|
|
isSaving: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
error: {
|
|
type: Object,
|
|
default: null
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close', 'save']);
|
|
|
|
// Données du formulaire
|
|
const formData = ref({
|
|
title: '',
|
|
description: '',
|
|
author: '',
|
|
publicationYear: null,
|
|
status: '',
|
|
rating: null,
|
|
genres: [],
|
|
alternativeSlugs: []
|
|
});
|
|
|
|
// Contrôle de l'affichage des inputs
|
|
const showGenreInput = ref(false);
|
|
const showAlternativeSlugInput = ref(false);
|
|
|
|
// Champs temporaires pour ajouter des genres et slugs
|
|
const newGenre = ref('');
|
|
const newAlternativeSlug = ref('');
|
|
|
|
// Initialiser le formulaire avec les données du manga
|
|
watch(() => props.manga, (newManga) => {
|
|
if (newManga) {
|
|
formData.value = {
|
|
title: newManga.title || '',
|
|
description: newManga.description || '',
|
|
author: newManga.author || '',
|
|
publicationYear: newManga.publicationYear || null,
|
|
status: newManga.status || '',
|
|
rating: newManga.rating || null,
|
|
genres: Array.isArray(newManga.genres) ? [...newManga.genres] : [],
|
|
alternativeSlugs: Array.isArray(newManga.alternativeSlugs) ? [...newManga.alternativeSlugs] : []
|
|
};
|
|
}
|
|
}, { immediate: true });
|
|
|
|
const closeModal = () => {
|
|
// Réinitialiser les états d'affichage
|
|
showGenreInput.value = false;
|
|
showAlternativeSlugInput.value = false;
|
|
newGenre.value = '';
|
|
newAlternativeSlug.value = '';
|
|
emit('close');
|
|
};
|
|
|
|
const saveChanges = () => {
|
|
// Nettoyer les données avant de les envoyer
|
|
const dataToSave = {
|
|
title: formData.value.title || undefined,
|
|
description: formData.value.description || undefined,
|
|
author: formData.value.author || undefined,
|
|
publicationYear: formData.value.publicationYear || undefined,
|
|
status: formData.value.status || undefined,
|
|
rating: formData.value.rating || undefined,
|
|
genres: formData.value.genres.length > 0 ? formData.value.genres : undefined,
|
|
alternativeSlugs: formData.value.alternativeSlugs.length > 0 ? formData.value.alternativeSlugs : undefined
|
|
};
|
|
|
|
// Supprimer les valeurs undefined
|
|
Object.keys(dataToSave).forEach(key => {
|
|
if (dataToSave[key] === undefined) {
|
|
delete dataToSave[key];
|
|
}
|
|
});
|
|
|
|
emit('save', dataToSave);
|
|
};
|
|
|
|
const addGenre = () => {
|
|
if (newGenre.value.trim() && !formData.value.genres.includes(newGenre.value.trim())) {
|
|
formData.value.genres.push(newGenre.value.trim());
|
|
newGenre.value = '';
|
|
showGenreInput.value = false;
|
|
}
|
|
};
|
|
|
|
const removeGenre = (index) => {
|
|
formData.value.genres.splice(index, 1);
|
|
};
|
|
|
|
const addAlternativeSlug = () => {
|
|
if (newAlternativeSlug.value.trim() && !formData.value.alternativeSlugs.includes(newAlternativeSlug.value.trim())) {
|
|
formData.value.alternativeSlugs.push(newAlternativeSlug.value.trim());
|
|
newAlternativeSlug.value = '';
|
|
showAlternativeSlugInput.value = false;
|
|
}
|
|
};
|
|
|
|
const removeAlternativeSlug = (index) => {
|
|
formData.value.alternativeSlugs.splice(index, 1);
|
|
};
|
|
</script>
|