style(manga-grid): cards sans arrondis, overlay actions au survol, grille plus dense

- Supprime rounded-lg et hover:scale-105 sur MangaCard
- Ajoute overlay gradient + 3 boutons (éditer, sources, rafraîchir) visibles au survol en bas à gauche de la cover
- MangaCard émet les événements edit/sources/refresh vers MangaGrid
- MangaGrid gère les modales et composables (edit, preferredSources, refresh)
- Grille plus dense : cols-3/4/5/7/8 selon breakpoint, gap-2
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-14 00:58:05 +01:00
parent 2cedd14f97
commit 9a4fb26b06
2 changed files with 141 additions and 29 deletions

View File

@@ -1,27 +1,59 @@
<template>
<div class="group relative bg-white dark:bg-gray-800 overflow-hidden shadow-sm">
<!-- Cover avec overlay -->
<div class="relative pb-[140%]">
<RouterLink
:to="{ name: 'manga-details', params: { id: manga.id } }"
class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden cursor-pointer transition-transform hover:scale-105 block">
<div class="relative pb-[130%]">
class="absolute inset-0">
<img
:src="manga.thumbnailUrl || 'https://via.placeholder.com/300x400'"
:alt="manga.title"
class="absolute inset-0 w-full h-full object-cover bg-gray-100" />
</div>
<div class="p-2">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-100 mb-1 truncate">{{ manga.title }}</h3>
<div class="flex items-center">
<span class="text-xs text-gray-500 dark:text-gray-400">{{ manga.publicationYear }}</span>
</div>
</div>
class="w-full h-full object-cover bg-gray-100" />
</RouterLink>
<!-- Gradient + actions au survol -->
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<div class="absolute bottom-2 left-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
class="p-1.5 bg-black/60 hover:bg-black/80 text-white rounded transition-colors"
title="Éditer"
@click="$emit('edit', manga)">
<PencilIcon class="w-3.5 h-3.5" />
</button>
<button
class="p-1.5 bg-black/60 hover:bg-black/80 text-white rounded transition-colors"
title="Sources préférées"
@click="$emit('sources', manga)">
<Cog6ToothIcon class="w-3.5 h-3.5" />
</button>
<button
class="p-1.5 bg-black/60 hover:bg-black/80 text-white rounded transition-colors"
title="Rafraîchir"
@click="$emit('refresh', manga)">
<ArrowPathIcon class="w-3.5 h-3.5" />
</button>
</div>
</div>
<!-- Titre -->
<RouterLink
:to="{ name: 'manga-details', params: { id: manga.id } }"
class="block p-2">
<h3 class="text-xs font-medium text-gray-800 dark:text-gray-100 truncate">{{ manga.title }}</h3>
</RouterLink>
</div>
</template>
<script setup>
defineProps({
import { ArrowPathIcon, Cog6ToothIcon, PencilIcon } from '@heroicons/vue/24/outline';
import { RouterLink } from 'vue-router';
defineProps({
manga: {
type: Object,
required: true
}
});
});
defineEmits(['edit', 'sources', 'refresh']);
</script>

View File

@@ -1,16 +1,96 @@
<template>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3 p-4">
<MangaCard v-for="manga in mangas" :key="manga.id" :manga="manga" />
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-2 p-4">
<MangaCard
v-for="manga in mangas"
:key="manga.id"
:manga="manga"
@edit="openEdit"
@sources="openSources"
@refresh="doRefresh" />
</div>
<!-- Modales -->
<MangaEditModal
:is-open="isEditModalOpen"
:manga="selectedManga"
:is-saving="editIsLoading"
:error="editError"
@close="closeEditModal"
@save="handleSaveEdit" />
<MangaPreferredSourcesModal
:is-open="isSourcesModalOpen"
:sources="preferredSources"
:is-loading="sourcesIsLoading"
:error="sourcesError"
:is-saving="sourcesIsSaving"
@close="isSourcesModalOpen = false"
@save="handleSaveSources" />
</template>
<script setup>
import MangaCard from './MangaCard.vue';
import { computed, ref } from 'vue';
import { useMangaEdit } from '../composables/useMangaEdit';
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
import { useMangaRefresh } from '../composables/useMangaRefresh';
import MangaCard from './MangaCard.vue';
import MangaEditModal from './MangaEditModal.vue';
import MangaPreferredSourcesModal from './MangaPreferredSourcesModal.vue';
defineProps({
defineProps({
mangas: {
type: Array,
required: true
}
});
});
const selectedManga = ref(null);
const isSourcesModalOpen = ref(false);
// ── Edit ──────────────────────────────────────────────────
const { isEditModalOpen, openEditModal, closeEditModal, editManga, isLoading: editIsLoading, error: editError } = useMangaEdit();
function openEdit(manga) {
selectedManga.value = manga;
openEditModal();
}
async function handleSaveEdit(data) {
if (!selectedManga.value) return;
await editManga(selectedManga.value.id, data);
}
// ── Sources préférées ─────────────────────────────────────
const selectedMangaId = computed(() => selectedManga.value?.id ?? null);
const {
sources: preferredSources,
isLoading: sourcesIsLoading,
error: sourcesError,
isSaving: sourcesIsSaving,
savePreferredSources
} = useMangaPreferredSources(selectedMangaId);
function openSources(manga) {
selectedManga.value = manga;
isSourcesModalOpen.value = true;
}
function handleSaveSources(sourceIds) {
savePreferredSources(sourceIds);
isSourcesModalOpen.value = false;
}
// ── Refresh ───────────────────────────────────────────────
const { refreshMetadata } = useMangaRefresh();
const refreshingId = ref(null);
async function doRefresh(manga) {
if (refreshingId.value) return;
refreshingId.value = manga.id;
try {
await refreshMetadata(manga.id);
} finally {
refreshingId.value = null;
}
}
</script>