style(manga): refondre la page d'ajout de manga sur le design system #21
@@ -1,108 +1,155 @@
|
||||
<template>
|
||||
<div class="overflow-y-auto h-full">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Barre de recherche -->
|
||||
<div class="mb-8">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col h-full">
|
||||
<Toolbar :config="toolbarConfig" />
|
||||
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<div class="px-6 py-8">
|
||||
|
||||
<!-- Recherche -->
|
||||
<section class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<h2 class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-4">Recherche</h2>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
@keyup.enter="performSearch"
|
||||
placeholder="Rechercher un manga..."
|
||||
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500" />
|
||||
<button
|
||||
@click="performSearch"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
||||
Rechercher
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500" />
|
||||
</section>
|
||||
|
||||
<!-- État de chargement -->
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Recherche en cours...</p>
|
||||
<section v-if="loading" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||
<div class="flex items-center gap-3 text-gray-600 dark:text-gray-400">
|
||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-green-600"></div>
|
||||
<span class="text-sm">Recherche en cours...</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Message d'erreur -->
|
||||
<div v-if="error" class="bg-red-100 dark:bg-red-900/20 border border-red-400 dark:border-red-700 text-red-700 dark:text-red-400 px-4 py-3 rounded relative mb-6">
|
||||
{{ error }}
|
||||
</div>
|
||||
<section v-if="error" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Résultats de recherche -->
|
||||
<div v-if="searchResults.length > 0" class="border-t border-gray-200 dark:border-gray-700">
|
||||
<!-- Résultats -->
|
||||
<section v-if="searchResults.length > 0" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">Résultats</h2>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ searchResults.length }} manga(s)</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100 dark:divide-gray-700/50">
|
||||
<div
|
||||
v-for="manga in searchResults"
|
||||
:key="manga.externalId"
|
||||
class="flex items-center gap-4 px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors border-b border-gray-100 dark:border-gray-700 cursor-pointer"
|
||||
class="flex items-start gap-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors cursor-pointer px-2"
|
||||
@click="openMangaModal(manga)">
|
||||
<img
|
||||
:src="manga.thumbnailUrl || manga.imageUrl || '/placeholder-cover.png'"
|
||||
alt=""
|
||||
class="h-36 w-24 object-cover flex-shrink-0 self-start"
|
||||
class="h-36 w-24 object-cover flex-shrink-0"
|
||||
referrerpolicy="no-referrer" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xl font-semibold text-gray-900 dark:text-gray-100">{{ manga.title }}</p>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ manga.title }}</p>
|
||||
<p v-if="manga.description" class="text-sm text-gray-600 dark:text-gray-300 mt-2 line-clamp-4">{{ manga.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else-if="!loading && searchQuery" class="text-center text-gray-600 dark:text-gray-400">Aucun résultat trouvé</p>
|
||||
</section>
|
||||
|
||||
<!-- Modal de confirmation -->
|
||||
<!-- Aucun résultat -->
|
||||
<section v-else-if="hasSearched && !loading" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">Aucun résultat trouvé</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de détail -->
|
||||
<Dialog :open="isModalOpen" @close="closeModal" class="relative z-50">
|
||||
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" aria-hidden="true" />
|
||||
|
||||
<div class="fixed inset-0 bg-gray-900/70 dark:bg-gray-900/80 transition-opacity" aria-hidden="true" />
|
||||
<div class="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogPanel class="w-full max-w-lg bg-white dark:bg-gray-800 rounded-xl shadow-xl p-6">
|
||||
<DialogTitle class="text-lg mb-4 text-gray-900 dark:text-gray-100"> Ajouter à la bibliothèque </DialogTitle>
|
||||
<DialogPanel v-if="selectedManga" class="w-full max-w-2xl bg-white dark:bg-gray-800 shadow-xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||
|
||||
<div v-if="selectedManga">
|
||||
<div class="flex gap-4">
|
||||
<!-- En-tête avec couverture -->
|
||||
<div class="flex gap-0 border-b border-gray-200 dark:border-gray-700">
|
||||
<img
|
||||
:src="selectedManga.imageUrl || '/placeholder-cover.png'"
|
||||
:src="selectedManga.imageUrl || selectedManga.thumbnailUrl || '/placeholder-cover.png'"
|
||||
:alt="selectedManga.title"
|
||||
class="h-48 w-32 object-cover" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-lg text-gray-900 dark:text-gray-100">{{ selectedManga.title }}</h4>
|
||||
<p class="mt-2 text-gray-700 dark:text-gray-300">
|
||||
{{ truncatedDescription }}
|
||||
class="h-64 w-44 object-cover flex-shrink-0"
|
||||
referrerpolicy="no-referrer" />
|
||||
<div class="flex-1 min-w-0 p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<DialogTitle class="text-base font-semibold text-gray-900 dark:text-gray-100 leading-snug">
|
||||
{{ selectedManga.title }}
|
||||
</DialogTitle>
|
||||
<div class="mt-3 space-y-1.5">
|
||||
<p v-if="selectedManga.author" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="text-gray-400 dark:text-gray-500">Auteur</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-200">{{ selectedManga.author }}</span>
|
||||
</p>
|
||||
<p v-if="selectedManga.publicationYear" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="text-gray-400 dark:text-gray-500">Publication</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-200">{{ selectedManga.publicationYear }}</span>
|
||||
</p>
|
||||
<p v-if="selectedManga.status" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="text-gray-400 dark:text-gray-500">Statut</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-200">{{ selectedManga.status }}</span>
|
||||
</p>
|
||||
<p v-if="selectedManga.rating" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span class="text-gray-400 dark:text-gray-500">Note</span>
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-200">{{ selectedManga.rating.toFixed(2) }} / 10</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedManga.genres?.length" class="flex flex-wrap gap-1.5 mt-4">
|
||||
<span
|
||||
v-for="genre in selectedManga.genres"
|
||||
:key="genre"
|
||||
class="text-xs px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
|
||||
{{ genre }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<!-- Description -->
|
||||
<div class="px-6 py-4 overflow-y-auto flex-1">
|
||||
<h3 class="text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-2">Description</h3>
|
||||
<p v-if="selectedManga.description" class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
{{ selectedManga.description }}
|
||||
</p>
|
||||
<p v-else class="text-sm text-gray-400 dark:text-gray-500 italic">Aucune description disponible.</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 dark:bg-gray-800">
|
||||
class="text-sm text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors px-4 py-2">
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="addManga"
|
||||
:disabled="adding"
|
||||
class="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center">
|
||||
<span v-if="adding" class="mr-2">
|
||||
<ArrowPathIcon class="h-5 w-5 animate-spin" />
|
||||
</span>
|
||||
{{ adding ? 'Ajout en cours...' : 'Ajouter' }}
|
||||
class="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-4 py-2 font-medium transition-colors inline-flex items-center gap-2">
|
||||
<ArrowPathIcon v-if="adding" class="h-4 w-4 animate-spin" />
|
||||
{{ adding ? 'Ajout en cours...' : 'Ajouter à la bibliothèque' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue';
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid';
|
||||
import { ArrowPathIcon, MagnifyingGlassIcon } from '@heroicons/vue/24/outline';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -110,20 +157,35 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
const mangaStore = useMangaStore();
|
||||
|
||||
const searchQuery = ref('');
|
||||
const hasSearched = ref(false);
|
||||
const isModalOpen = ref(false);
|
||||
const selectedManga = ref(null);
|
||||
|
||||
// Récupération des états du store
|
||||
const { searchResults, loadingSearch: loading, searchError: error, addingManga: adding } = storeToRefs(mangaStore);
|
||||
|
||||
const truncatedDescription = computed(() => {
|
||||
if (!selectedManga.value?.description) return '';
|
||||
return selectedManga.value.description.length > 500
|
||||
? selectedManga.value.description.slice(0, 500) + '...'
|
||||
: selectedManga.value.description;
|
||||
const toolbarConfig = computed(() => ({
|
||||
leftSection: [
|
||||
{ type: 'label', text: 'Ajouter un manga', class: 'text-sm font-medium' },
|
||||
],
|
||||
rightSection: [
|
||||
{
|
||||
type: 'button',
|
||||
icon: MagnifyingGlassIcon,
|
||||
label: 'Rechercher',
|
||||
onClick: performSearch,
|
||||
disabled: !searchQuery.value.trim() || loading.value,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
let debounceTimer = null;
|
||||
watch(searchQuery, newVal => {
|
||||
clearTimeout(debounceTimer);
|
||||
if (newVal.trim().length > 3) {
|
||||
debounceTimer = setTimeout(performSearch, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Effectuer la recherche au chargement si un paramètre q est présent
|
||||
onMounted(() => {
|
||||
const queryParam = route.query.q;
|
||||
if (queryParam) {
|
||||
@@ -132,8 +194,8 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
}
|
||||
});
|
||||
|
||||
// Nettoyer la recherche et les résultats lors du démontage du composant
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(debounceTimer);
|
||||
searchQuery.value = '';
|
||||
mangaStore.clearSearchResults();
|
||||
});
|
||||
@@ -142,6 +204,7 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
if (!searchQuery.value.trim()) return;
|
||||
try {
|
||||
await mangaStore.searchMangaDex(searchQuery.value);
|
||||
hasSearched.value = true;
|
||||
} catch (e) {
|
||||
console.error('Erreur de recherche:', e);
|
||||
}
|
||||
@@ -159,7 +222,6 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
||||
|
||||
const addManga = async () => {
|
||||
if (!selectedManga.value) return;
|
||||
|
||||
try {
|
||||
await mangaStore.createFromMangaDex(selectedManga.value.externalId);
|
||||
router.push('/manga');
|
||||
|
||||
Reference in New Issue
Block a user