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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<template> <template>
<div v-if="errorDetails" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <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.' }} {{ errorDetails.message || 'Une erreur est survenue lors du chargement des détails.' }}
</div> </div>
@@ -9,13 +10,14 @@
<Toolbar :config="toolbarConfig" class="sticky top-16 z-10" /> <Toolbar :config="toolbarConfig" class="sticky top-16 z-10" />
<div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500"> <div v-if="isRefreshingDetails" class="absolute top-2 right-2 text-gray-500 z-20">
<ArrowPathIcon class="h-5 w-5 animate-spin" /> <ArrowPathIcon class="h-5 w-5 animate-spin" />
</div> </div>
<MangaHeader :manga="currentManga" /> <MangaHeader :manga="currentManga" />
<!-- Section Volumes --> <!-- Section Volumes avec conteneur mobile -->
<div class="mt-8"> <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 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 class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div> </div>
@@ -41,7 +43,10 @@
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div> <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div> </div>
<div v-else class="text-center text-gray-500 py-10"> Aucun manga sélectionné ou trouvé. </div> <div v-else class="text-center text-gray-500 py-10 px-4">
Aucun manga sélectionné ou trouvé.
</div>
</div>
</template> </template>
<script setup> <script setup>

View File

@@ -2,19 +2,18 @@
<button <button
@click="$emit('click')" @click="$emit('click')"
:class="[ :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 active
? 'text-green-500' // Style actif ? 'text-green-500' // Style actif
: 'hover:text-green-500' // Effet de survol : 'hover:text-green-500' // Effet de survol
]" ]"
:aria-label="label || 'Toolbar button'"> :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" /> <ToolbarLabel :label="label" />
</button> </button>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue';
import ToolbarLabel from './ToolbarLabel.vue'; import ToolbarLabel from './ToolbarLabel.vue';
defineProps({ defineProps({

View File

@@ -3,11 +3,11 @@
<div class="h-full"> <div class="h-full">
<MenuButton <MenuButton
:class="[ :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' : '' active ? 'text-green-500' : ''
]" ]"
:aria-label="label || 'Options'"> :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" /> <ToolbarLabel :label="label" />
</MenuButton> </MenuButton>
</div> </div>
@@ -32,7 +32,7 @@
</template> </template>
<script setup> <script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'; import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue';
import ToolbarLabel from './ToolbarLabel.vue'; import ToolbarLabel from './ToolbarLabel.vue';
defineProps({ defineProps({

View File

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

View File

@@ -18,6 +18,7 @@ readonly class MangaDetail
public string $status, public string $status,
public ?string $externalId, public ?string $externalId,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating public ?float $rating
) {} ) {}
} }

View File

@@ -30,6 +30,7 @@ readonly class GetMangaStateProvider implements ProviderInterface
status: $response->status, status: $response->status,
externalId: $response->externalId, externalId: $response->externalId,
imageUrl: $response->imageUrl, imageUrl: $response->imageUrl,
thumbnailUrl: $response->thumbnailUrl,
rating: $response->rating rating: $response->rating
); );
} }