feat: amélioration de l'interface utilisateur des composants MangaHeader, MangaVolume et MangaVolumeList, avec des ajustements de style pour une meilleure réactivité et une expérience utilisateur optimisée sur mobile. Ajout de la gestion de la taille de la fenêtre pour adapter l'affichage des éléments.

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-06-29 23:59:02 +02:00
parent d23c82631e
commit 896c57ac34
9 changed files with 171 additions and 128 deletions

View File

@@ -1,46 +1,46 @@
<template>
<div class="shadow-lg text-white">
<div class="relative h-96 bg-cover bg-center" :style="{ backgroundImage: `url('${manga.imageUrl}')` }">
<div class="relative h-64 sm:h-80 lg:h-96 bg-cover bg-center" :style="{ backgroundImage: `url('${manga.imageUrl}')` }">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="absolute inset-0 flex flex-row justify-center p-4">
<!-- Image de couverture -->
<div class="hidden mr-12 xl:block 2xl:block">
<img :src="manga.thumbnailUrl" :alt="manga.title" class="max-w-72 max-h-72 ml-4" />
<div class="absolute inset-0 flex flex-col lg:flex-row justify-center p-4 lg:p-6">
<!-- Image de couverture - cachée sur mobile, visible sur desktop -->
<div class="hidden lg:block mr-12">
<img :src="manga.thumbnailUrl" :alt="manga.title" class="max-w-48 lg:max-w-72 max-h-48 lg:max-h-72" />
</div>
<!-- Informations du manga -->
<div class="flex flex-col">
<div class="flex items-center mb-4">
<BookmarkIcon class="h-8 w-8 text-white" />
<h1 class="text-3xl font-bold ml-4">{{ manga.title }}</h1>
<div class="flex flex-col space-y-3 lg:space-y-4 flex-1 min-w-0">
<div class="flex items-start lg:items-center space-x-3">
<BookmarkIcon class="h-6 w-6 lg:h-8 lg:w-8 text-white flex-shrink-0 mt-1 lg:mt-0" />
<h1 class="text-xl sm:text-2xl lg:text-3xl font-bold leading-tight">{{ manga.title }}</h1>
</div>
<div class="flex items-center mb-4">
<span class="mr-4">{{ manga.year }}</span>
<div class="flex flex-wrap items-center gap-4 text-sm lg:text-base">
<span>{{ manga.year }}</span>
<span>Chapitres: {{ manga.totalChapters }}</span>
</div>
<div class="flex items-center mb-4">
<FolderIcon class="h-6 w-6 text-gray-400 mr-2" />
<span class="truncate">/media/mangas/{{ manga.title }} ({{ manga.year }})</span>
<span class="ml-auto bg-green-600 py-1 px-2 rounded">{{ manga.status || 'Terminé' }}</span>
</div>
<div class="flex items-center mb-4">
<template v-if="manga.tags?.length">
<template v-for="(tag, index) in manga.tags.slice(0, 5)" :key="index">
<span class="bg-gray-700 py-1 px-2 rounded-sm mr-2">{{ tag }}</span>
</template>
<span v-if="manga.tags.length > 5" class="bg-gray-700 py-1 px-2 rounded-sm mr-2">...</span>
</template>
</div>
<div class="mb-4">
<div class="flex items-center mb-2">
<HeartIcon class="h-6 w-6 text-red-500 mr-2" />
<span>{{ manga.rating }}</span>
<div class="flex items-start lg:items-center space-x-2">
<FolderIcon class="h-5 w-5 lg:h-6 lg:w-6 text-gray-400 flex-shrink-0 mt-0.5 lg:mt-0" />
<div class="flex flex-col lg:flex-row lg:items-center lg:space-x-4 min-w-0 flex-1">
<span class="truncate text-sm lg:text-base">/media/mangas/{{ manga.title }} ({{ manga.year }})</span>
<span class="bg-green-600 py-1 px-2 rounded text-xs lg:text-sm self-start lg:self-auto">{{ manga.status || 'Terminé' }}</span>
</div>
<p class="line-clamp-5">{{ manga.description }}</p>
</div>
<div class="flex flex-wrap gap-2" v-if="manga.tags?.length">
<template v-for="(tag, index) in manga.tags.slice(0, isMobile ? 3 : 5)" :key="index">
<span class="bg-gray-700 py-1 px-2 rounded-sm text-xs lg:text-sm">{{ tag }}</span>
</template>
<span v-if="manga.tags.length > (isMobile ? 3 : 5)" class="bg-gray-700 py-1 px-2 rounded-sm text-xs lg:text-sm">...</span>
</div>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<HeartIcon class="h-5 w-5 lg:h-6 lg:w-6 text-red-500" />
<span class="text-sm lg:text-base">{{ manga.rating }}</span>
</div>
<p class="text-sm lg:text-base line-clamp-3 lg:line-clamp-5">{{ manga.description }}</p>
</div>
</div>
</div>
@@ -50,6 +50,7 @@
<script setup>
import { BookmarkIcon, FolderIcon, HeartIcon } from '@heroicons/vue/24/outline';
import { computed, onMounted, onUnmounted, ref } from 'vue';
defineProps({
manga: {
@@ -57,10 +58,37 @@
required: true
}
});
// Détection du mobile
const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value < 1024);
const updateWindowWidth = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener('resize', updateWindowWidth);
});
onUnmounted(() => {
window.removeEventListener('resize', updateWindowWidth);
});
</script>
<style scoped>
/* Pour s'assurer que line-clamp fonctionne */
@supports (-webkit-line-clamp: 3) {
.line-clamp-3 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
}
}
@supports (-webkit-line-clamp: 5) {
.line-clamp-5 {
overflow: hidden;

View File

@@ -1,55 +1,62 @@
<template>
<div class="bg-white rounded-sm shadow mb-2">
<!-- En-tête du volume -->
<div class="relative flex items-center justify-between bg-white p-4 rounded-t-sm">
<!-- Partie gauche -->
<div class="flex items-center space-x-4">
<BookmarkIcon class="h-8 w-8 text-gray-500" />
<h2 class="text-xl font-semibold w-28">Volume {{ String(volume.number).padStart(2, '0') }}</h2>
<div class="flex items-center w-16">
<span
:class="[
'px-2 py-1 text-sm rounded w-full text-center text-white',
{
'bg-red-500': volume.downloadedChapter === 0,
'bg-yellow-500':
volume.downloadedChapter < volume.totalChapter && volume.downloadedChapter > 0,
'bg-green-500': volume.downloadedChapter === volume.totalChapter
}
]">
{{ volume.downloadedChapter }}/{{ volume.totalChapter }}
</span>
<div class="relative bg-white p-3 sm:p-4 rounded-t-sm">
<!-- Layout mobile/desktop -->
<div class="flex items-center justify-between">
<!-- Partie gauche -->
<div class="flex items-center space-x-1 sm:space-x-4 flex-1 min-w-0">
<BookmarkIcon class="h-6 w-6 sm:h-8 sm:w-8 text-gray-500 flex-shrink-0" />
<h2 class="text-lg sm:text-xl font-semibold w-20 sm:w-28 flex-shrink-0">Vol {{ String(volume.number).padStart(2, '0') }}</h2>
<div class="flex items-center">
<span
:class="[
'px-2 py-1 text-xs sm:text-sm rounded text-center text-white min-w-[3rem] sm:min-w-[4rem]',
{
'bg-red-500': volume.downloadedChapter === 0,
'bg-yellow-500':
volume.downloadedChapter < volume.totalChapter && volume.downloadedChapter > 0,
'bg-green-500': volume.downloadedChapter === volume.totalChapter
}
]">
{{ volume.downloadedChapter }}/{{ volume.totalChapter }}
</span>
</div>
</div>
<!-- Actions du volume -->
<div class="flex items-center space-x-1 sm:space-x-2">
<button
class="w-8 sm:w-10 h-8 sm:h-10 flex items-center justify-center"
@click="handleSearch"
:class="{
'text-yellow-500 cursor-wait': isSearching,
'text-gray-500 hover:text-green-500': !isSearching
}">
<MagnifyingGlassIcon class="h-5 w-5 sm:h-6 sm:w-6" />
</button>
<button
class="w-8 sm:w-10 h-8 sm:h-10 flex items-center justify-center"
@click="handleDownload"
:class="{
'text-yellow-500 cursor-wait': isDownloading,
'text-gray-500 hover:text-green-500': !isDownloading
}"
:disabled="isDownloading">
<ArrowDownTrayIcon class="h-5 w-5 sm:h-6 sm:w-6" />
</button>
</div>
</div>
<!-- Bouton toggle -->
<!-- Bouton toggle centré -->
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
<component
:is="isOpen ? ChevronUpIcon : ChevronDownIcon"
class="h-6 w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"
@click="toggleVolume" />
</div>
<!-- Actions du volume -->
<div class="flex space-x-2 text-xl text-bold">
<button
class="w-8 text-center"
@click="handleSearch"
:class="{
'text-yellow-500 cursor-wait': isSearching,
'text-gray-500 hover:text-green-500': !isSearching
}">
<MagnifyingGlassIcon class="h-6 w-6" />
</button>
<button
class="w-8 text-center"
@click="handleDownload"
:class="{
'text-yellow-500 cursor-wait': isDownloading,
'text-gray-500 hover:text-green-500': !isDownloading
}"
:disabled="isDownloading">
<ArrowDownTrayIcon class="h-6 w-6" />
@click="toggleVolume"
class="w-8 sm:w-10 h-8 sm:h-10 flex items-center justify-center">
<component
:is="isOpen ? ChevronUpIcon : ChevronDownIcon"
class="h-5 w-5 sm:h-6 sm:w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"
/>
</button>
</div>
</div>
@@ -58,10 +65,12 @@
<MangaChapterList v-show="isOpen" :chapters="volume.chapters" :manga-slug="mangaSlug" :manga-id="mangaId" />
<!-- Chevron de fermeture -->
<div v-show="isOpen" class="flex justify-center p-2 py bg-white rounded-b-sm">
<ChevronUpIcon
class="h-6 w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"
@click="toggleVolume" />
<div v-show="isOpen" class="flex justify-center p-2 bg-white rounded-b-sm">
<button @click="toggleVolume" class="w-8 h-8 flex items-center justify-center">
<ChevronUpIcon
class="h-5 w-5 sm:h-6 sm:w-6 bg-gray-400 rounded-full p-1 text-white hover:bg-green-500 cursor-pointer"
/>
</button>
</div>
</div>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<div class="p-4">
<div>
<MangaVolume
v-for="(volume, index) in volumes"
:key="volume.number"

View File

@@ -1,47 +1,52 @@
<template>
<div v-if="errorDetails" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }}
</div>
<div v-else-if="currentManga" class="relative">
<!-- Composant invisible qui écoute les mises à jour Mercure -->
<MercureListener :manga-id="mangaId" />
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500">
<ArrowPathIcon class="h-5 w-5 animate-spin" />
</div>
<MangaHeader :manga="currentManga" />
<!-- Section Volumes -->
<div class="mt-8">
<div v-if="isLoadingVolumes" class="flex justify-center items-center h-32">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<div v-else-if="errorVolumes" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }}
</div>
<MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" :manga-id="mangaId" />
<div class="min-h-screen bg-gray-50">
<div v-if="errorDetails" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mx-4 mt-4">
{{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }}
</div>
<!-- Modale des sources préférées -->
<MangaPreferredSourcesModal
:is-open="isPreferredSourcesModalOpen"
:sources="preferredSources"
:is-loading="isLoadingSources"
:error="sourcesError"
:is-saving="isSavingSources"
@close="closePreferredSourcesModal"
@save="savePreferredSources"
/>
</div>
<div v-else-if="currentManga" class="relative">
<!-- Composant invisible qui écoute les mises à jour Mercure -->
<MercureListener :manga-id="mangaId" />
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div v-else class="text-center text-gray-500 py-10"> Aucun manga sélectionné ou trouvé. </div>
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500 z-20">
<ArrowPathIcon class="h-5 w-5 animate-spin" />
</div>
<MangaHeader :manga="currentManga" />
<!-- Section Volumes avec conteneur mobile -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8 mt-8 pb-8">
<div v-if="isLoadingVolumes" class="flex justify-center items-center h-32">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<div v-else-if="errorVolumes" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{{ errorVolumes.message || 'Une erreur est survenue lors du chargement des volumes.' }}
</div>
<MangaVolumeList v-else :volumes="volumes" :manga-slug="currentManga.slug" :manga-id="mangaId" />
</div>
<!-- Modale des sources préférées -->
<MangaPreferredSourcesModal
:is-open="isPreferredSourcesModalOpen"
:sources="preferredSources"
:is-loading="isLoadingSources"
:error="sourcesError"
:is-saving="isSavingSources"
@close="closePreferredSourcesModal"
@save="savePreferredSources"
/>
</div>
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
<div v-else class="text-center text-gray-500 py-10 px-4">
Aucun manga sélectionné ou trouvé.
</div>
</div>
</template>
<script setup>

View File

@@ -2,19 +2,18 @@
<button
@click="$emit('click')"
:class="[
'flex flex-col items-center justify-around min-h-14 w-min ml-4 rounded group text-white',
'flex flex-col items-center justify-center min-h-12 sm:min-h-14 w-min px-2 sm:ml-4 ml-1 rounded group text-white',
active
? 'text-green-500' // Style actif
: 'hover:text-green-500' // Effet de survol
]"
:aria-label="label || 'Toolbar button'">
<component v-if="icon" :is="icon" class="h-6 w-6" />
<component v-if="icon" :is="icon" class="h-5 w-5 sm:h-6 sm:w-6" />
<ToolbarLabel :label="label" />
</button>
</template>
<script setup>
import { computed } from 'vue';
import ToolbarLabel from './ToolbarLabel.vue';
defineProps({

View File

@@ -3,11 +3,11 @@
<div class="h-full">
<MenuButton
:class="[
'flex flex-col items-center justify-around min-h-14 w-min ml-4 rounded group text-white hover:text-green-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
'flex flex-col items-center justify-center min-h-12 sm:min-h-14 w-min px-2 sm:ml-4 ml-1 rounded group text-white hover:text-green-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75',
active ? 'text-green-500' : ''
]"
:aria-label="label || 'Options'">
<component v-if="icon" :is="icon" class="h-6 w-6" aria-hidden="true" />
<component v-if="icon" :is="icon" class="h-5 w-5 sm:h-6 sm:w-6" aria-hidden="true" />
<ToolbarLabel :label="label" />
</MenuButton>
</div>
@@ -32,8 +32,8 @@
</template>
<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
import ToolbarLabel from './ToolbarLabel.vue';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import ToolbarLabel from './ToolbarLabel.vue';
defineProps({
icon: {

View File

@@ -1,5 +1,5 @@
<template>
<span v-if="label" class="text-xs">{{ label }}</span>
<span v-if="label" class="text-xs hidden sm:inline">{{ label }}</span>
</template>
<script setup>