style/restyling-manga-grid #12
11
DONE.md
11
DONE.md
@@ -27,3 +27,14 @@
|
||||
- [x] Lien du titre : passer le hover de bleu (`hover:text-blue-600`) à vert (`hover:text-green-500`)
|
||||
- [x] Icône monitoring : remplacer `BellIcon` / `BellSlashIcon` par `BookmarkIcon` / `BookmarkSlashIcon`
|
||||
- [x] Supprimer le padding du wrapper + `container mx-auto` pour tableau pleine largeur
|
||||
|
||||
---
|
||||
|
||||
## [UI] Restyling vue grille des mangas — 2026-03-14
|
||||
|
||||
> Branche : `style/restyling-manga-grid` | Commit : `9a4fb26`
|
||||
|
||||
- [x] **Réduire la taille des cards** : grille plus dense (cols-3/4/5/7/8 selon breakpoint, gap-2)
|
||||
- [x] **Supprimer les arrondis** : retrait de `rounded-lg` et `hover:scale-105`
|
||||
- [x] **Overlay icônes au survol** : gradient + 3 boutons (éditer, sources, rafraîchir) en bas à gauche de la cover, visibles au `group-hover`
|
||||
- [x] MangaCard émet les événements, MangaGrid gère les modales (edit, sources, refresh)
|
||||
|
||||
@@ -1,27 +1,60 @@
|
||||
<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 + année -->
|
||||
<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>
|
||||
<span v-if="manga.publicationYear" class="text-xs text-gray-500 dark:text-gray-400">{{ manga.publicationYear }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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>
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
<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-3 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 { 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({
|
||||
mangas: {
|
||||
@@ -13,4 +43,54 @@
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user