style(manga): refondre la page d'ajout de manga sur le design system #21
@@ -1,108 +1,155 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto h-full">
|
<div class="flex flex-col h-full">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<Toolbar :config="toolbarConfig" />
|
||||||
<!-- Barre de recherche -->
|
|
||||||
<div class="mb-8">
|
<div class="overflow-y-auto flex-1">
|
||||||
<div class="flex gap-4">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
@keyup.enter="performSearch"
|
@keyup.enter="performSearch"
|
||||||
placeholder="Rechercher un manga..."
|
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" />
|
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" />
|
||||||
<button
|
</section>
|
||||||
@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>
|
|
||||||
|
|
||||||
<!-- État de chargement -->
|
<!-- État de chargement -->
|
||||||
<div v-if="loading" class="text-center py-8">
|
<section v-if="loading" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
<div class="flex items-center gap-3 text-gray-600 dark:text-gray-400">
|
||||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Recherche en cours...</p>
|
<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>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Message d'erreur -->
|
<!-- 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">
|
<section v-if="error" class="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
|
||||||
{{ error }}
|
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Résultats de recherche -->
|
<!-- Résultats -->
|
||||||
<div v-if="searchResults.length > 0" class="border-t border-gray-200 dark:border-gray-700">
|
<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
|
<div
|
||||||
v-for="manga in searchResults"
|
v-for="manga in searchResults"
|
||||||
:key="manga.externalId"
|
: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)">
|
@click="openMangaModal(manga)">
|
||||||
<img
|
<img
|
||||||
:src="manga.thumbnailUrl || manga.imageUrl || '/placeholder-cover.png'"
|
:src="manga.thumbnailUrl || manga.imageUrl || '/placeholder-cover.png'"
|
||||||
alt=""
|
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" />
|
referrerpolicy="no-referrer" />
|
||||||
<div class="flex-1 min-w-0">
|
<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>
|
<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>
|
</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">
|
<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">
|
<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">
|
<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]">
|
||||||
<DialogTitle class="text-lg mb-4 text-gray-900 dark:text-gray-100"> Ajouter à la bibliothèque </DialogTitle>
|
|
||||||
|
|
||||||
<div v-if="selectedManga">
|
<!-- En-tête avec couverture -->
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-0 border-b border-gray-200 dark:border-gray-700">
|
||||||
<img
|
<img
|
||||||
:src="selectedManga.imageUrl || '/placeholder-cover.png'"
|
:src="selectedManga.imageUrl || selectedManga.thumbnailUrl || '/placeholder-cover.png'"
|
||||||
:alt="selectedManga.title"
|
:alt="selectedManga.title"
|
||||||
class="h-48 w-32 object-cover" />
|
class="h-64 w-44 object-cover flex-shrink-0"
|
||||||
<div class="flex-1 min-w-0">
|
referrerpolicy="no-referrer" />
|
||||||
<h4 class="text-lg text-gray-900 dark:text-gray-100">{{ selectedManga.title }}</h4>
|
<div class="flex-1 min-w-0 p-6 flex flex-col justify-between">
|
||||||
<p class="mt-2 text-gray-700 dark:text-gray-300">
|
<div>
|
||||||
{{ truncatedDescription }}
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="closeModal"
|
@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
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="addManga"
|
@click="addManga"
|
||||||
:disabled="adding"
|
: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">
|
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">
|
||||||
<span v-if="adding" class="mr-2">
|
<ArrowPathIcon v-if="adding" class="h-4 w-4 animate-spin" />
|
||||||
<ArrowPathIcon class="h-5 w-5 animate-spin" />
|
{{ adding ? 'Ajout en cours...' : 'Ajouter à la bibliothèque' }}
|
||||||
</span>
|
|
||||||
{{ adding ? 'Ajout en cours...' : 'Ajouter' }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue';
|
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 { 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 { useRoute, useRouter } from 'vue-router';
|
||||||
|
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||||
import { useMangaStore } from '../../application/store/mangaStore';
|
import { useMangaStore } from '../../application/store/mangaStore';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -110,20 +157,35 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
const mangaStore = useMangaStore();
|
const mangaStore = useMangaStore();
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
|
const hasSearched = ref(false);
|
||||||
const isModalOpen = ref(false);
|
const isModalOpen = ref(false);
|
||||||
const selectedManga = ref(null);
|
const selectedManga = ref(null);
|
||||||
|
|
||||||
// Récupération des états du store
|
|
||||||
const { searchResults, loadingSearch: loading, searchError: error, addingManga: adding } = storeToRefs(mangaStore);
|
const { searchResults, loadingSearch: loading, searchError: error, addingManga: adding } = storeToRefs(mangaStore);
|
||||||
|
|
||||||
const truncatedDescription = computed(() => {
|
const toolbarConfig = computed(() => ({
|
||||||
if (!selectedManga.value?.description) return '';
|
leftSection: [
|
||||||
return selectedManga.value.description.length > 500
|
{ type: 'label', text: 'Ajouter un manga', class: 'text-sm font-medium' },
|
||||||
? selectedManga.value.description.slice(0, 500) + '...'
|
],
|
||||||
: selectedManga.value.description;
|
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(() => {
|
onMounted(() => {
|
||||||
const queryParam = route.query.q;
|
const queryParam = route.query.q;
|
||||||
if (queryParam) {
|
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(() => {
|
onBeforeUnmount(() => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
mangaStore.clearSearchResults();
|
mangaStore.clearSearchResults();
|
||||||
});
|
});
|
||||||
@@ -142,6 +204,7 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
if (!searchQuery.value.trim()) return;
|
if (!searchQuery.value.trim()) return;
|
||||||
try {
|
try {
|
||||||
await mangaStore.searchMangaDex(searchQuery.value);
|
await mangaStore.searchMangaDex(searchQuery.value);
|
||||||
|
hasSearched.value = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Erreur de recherche:', e);
|
console.error('Erreur de recherche:', e);
|
||||||
}
|
}
|
||||||
@@ -159,7 +222,6 @@ import { useMangaStore } from '../../application/store/mangaStore';
|
|||||||
|
|
||||||
const addManga = async () => {
|
const addManga = async () => {
|
||||||
if (!selectedManga.value) return;
|
if (!selectedManga.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mangaStore.createFromMangaDex(selectedManga.value.externalId);
|
await mangaStore.createFromMangaDex(selectedManga.value.externalId);
|
||||||
router.push('/manga');
|
router.push('/manga');
|
||||||
|
|||||||
Reference in New Issue
Block a user