style(manga-overview): réécriture complète de MangaOverview.vue
All checks were successful
Deploy / deploy (push) Successful in 2m50s
All checks were successful
Deploy / deploy (push) Successful in 2m50s
Remplace les grandes cartes verbeux par des lignes compactes avec cover, titre (text-2xl), badge statut, résumé tronqué et 3 boutons d'action verticaux (éditer, sources, rafraîchir) — cohérent avec MangaTable. Archivage de la tâche [UI] Améliorer la vue Overview dans TASK.md.
This commit is contained in:
parent
74f903d78d
commit
10d10d2c2f
22
TASK.md
22
TASK.md
@@ -75,27 +75,5 @@
|
||||
|
||||
---
|
||||
|
||||
## [UI] Simplifier l'affichage overview des mangas
|
||||
|
||||
**Objectif :** Améliorer l'ergonomie de la page d'accueil mangas en simplifiant l'affichage et en ajoutant des raccourcis rapides vers les actions courantes.
|
||||
|
||||
### Vue table
|
||||
|
||||
- [ ] **Raccourcis actions rapides** : dans chaque ligne de la table, ajouter un menu d'actions (icônes ou dropdown) permettant d'ouvrir directement les modales : éditer le manga, gérer les sources, lancer un scrape, supprimer
|
||||
- [ ] **Colonne statut de lecture** : afficher le statut (en cours / terminé / abandonné…) directement dans la ligne sans ouvrir la modale
|
||||
- [ ] **Colonne dernière activité** : date du dernier chapitre scrapé ou lu
|
||||
- [ ] **Raccourci ouverture rapide** : clic sur le titre ouvre le reader / detail page directement
|
||||
|
||||
### Vue grille (MangaCard)
|
||||
|
||||
- [ ] **Overlay actions** : au survol d'une card, afficher des boutons d'action rapide superposés (éditer, sources, scrape)
|
||||
- [ ] **Badge statut source préférée** : indicateur visuel si la source préférée est configurée ou non
|
||||
|
||||
### Modales accessibles
|
||||
|
||||
- [ ] **Modale "Éditer manga"** : accessible depuis la table (icône crayon) et la card (overlay)
|
||||
- [ ] **Modale "Gérer sources"** : accessible depuis la table et la card
|
||||
- [ ] **Confirmation scrape rapide** : déclencher un scrape depuis la liste sans naviguer vers le détail
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="manga in mangas"
|
||||
:key="manga.id"
|
||||
class="flex bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg p-4 space-x-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
@click="$emit('manga-click', manga)">
|
||||
<!-- Cover Image -->
|
||||
<div class="flex-shrink-0">
|
||||
<img :src="manga.imageUrl || '/placeholder-cover.png'" alt="" class="h-48 w-32 object-cover rounded" referrerpolicy="no-referrer" />
|
||||
<!-- TODO: Add placeholder image -->
|
||||
</div>
|
||||
|
||||
<!-- Manga Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-lg leading-7 font-medium text-gray-900 dark:text-gray-100 truncate">{{
|
||||
manga.title
|
||||
}}</h3>
|
||||
<p v-if="manga.publicationYear" class="text-sm text-gray-500 dark:text-gray-400 mt-1">{{
|
||||
manga.publicationYear
|
||||
}}</p>
|
||||
<p v-if="manga.description" class="text-sm text-gray-700 dark:text-gray-300 mt-2">
|
||||
{{ truncateDescription(manga.description) }}
|
||||
</p>
|
||||
<p v-if="manga.createdAt" class="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
Added: {{ formatDate(manga.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits, defineProps } from 'vue';
|
||||
|
||||
const emit = defineEmits(['manga-click']);
|
||||
|
||||
const props = defineProps({
|
||||
mangas: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const formatDate = dateString => {
|
||||
if (!dateString) return '';
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString(undefined, options);
|
||||
} catch (e) {
|
||||
console.error('Error formatting date:', e);
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const truncateDescription = description => {
|
||||
if (!description) return '';
|
||||
return description.length > 500 ? description.slice(0, 500) + '...' : description;
|
||||
};
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.description-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
v-for="manga in mangas"
|
||||
:key="manga.id"
|
||||
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">
|
||||
|
||||
<!-- Cover -->
|
||||
<img
|
||||
:src="manga.thumbnailUrl || manga.imageUrl || '/placeholder-cover.png'"
|
||||
alt=""
|
||||
class="h-36 w-24 object-cover flex-shrink-0 self-start"
|
||||
referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Titre + méta + résumé -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start gap-2 flex-wrap">
|
||||
<RouterLink
|
||||
:to="{ name: 'manga-details', params: { id: manga.id } }"
|
||||
class="text-2xl font-semibold text-gray-900 dark:text-gray-100 hover:text-green-500 dark:hover:text-green-400 transition-colors"
|
||||
@click.stop>
|
||||
{{ manga.title }}
|
||||
</RouterLink>
|
||||
<span
|
||||
v-if="manga.status"
|
||||
class="text-xs font-medium px-2 py-0.5 rounded-full flex-shrink-0"
|
||||
:class="statusClass(manga.status)">
|
||||
{{ manga.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="manga.description" class="text-sm text-gray-600 dark:text-gray-300 mt-2 line-clamp-4">
|
||||
{{ manga.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions verticales -->
|
||||
<div class="flex flex-col items-center justify-center gap-0.5 flex-shrink-0 self-stretch">
|
||||
<button
|
||||
class="p-1.5 rounded-md text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
title="Éditer"
|
||||
@click.stop="openEdit(manga)">
|
||||
<PencilIcon class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-md text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
title="Sources préférées"
|
||||
@click.stop="openSources(manga)">
|
||||
<Cog6ToothIcon class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-md transition-colors"
|
||||
:class="refreshingId === manga.id
|
||||
? 'text-blue-400 cursor-not-allowed'
|
||||
: 'text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600'"
|
||||
title="Rafraîchir"
|
||||
:disabled="refreshingId === manga.id"
|
||||
@click.stop="doRefresh(manga)">
|
||||
<ArrowPathIcon
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': refreshingId === manga.id }" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowPathIcon, Cog6ToothIcon, PencilIcon } from '@heroicons/vue/24/outline';
|
||||
import { computed, ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useMangaEdit } from '../composables/useMangaEdit';
|
||||
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
|
||||
import { useMangaRefresh } from '../composables/useMangaRefresh';
|
||||
import MangaEditModal from './MangaEditModal.vue';
|
||||
import MangaPreferredSourcesModal from './MangaPreferredSourcesModal.vue';
|
||||
|
||||
const emit = defineEmits(['manga-click']);
|
||||
|
||||
const props = defineProps({
|
||||
mangas: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
function statusClass(status) {
|
||||
if (status === 'ongoing') return 'text-blue-600 bg-blue-50 dark:bg-blue-900/20';
|
||||
if (status === 'completed') return 'text-green-600 bg-green-50 dark:bg-green-900/20';
|
||||
return 'text-gray-500 bg-gray-100 dark:bg-gray-700';
|
||||
}
|
||||
|
||||
// ── Selected manga ────────────────────────────────────────
|
||||
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>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<!-- Résultats de recherche -->
|
||||
<div class="max-w-full overflow-hidden">
|
||||
<MangaList v-if="searchResults.length > 0" :mangas="searchResults" @manga-click="openMangaModal" />
|
||||
<MangaOverview v-if="searchResults.length > 0" :mangas="searchResults" @manga-click="openMangaModal" />
|
||||
<p v-else-if="!loading && searchQuery" class="text-center text-gray-600 dark:text-gray-400">Aucun résultat trouvé</p>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ import { storeToRefs } from 'pinia';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
import MangaList from '../components/MangaList.vue';
|
||||
import MangaOverview from '../components/MangaOverview.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="overflow-y-auto flex-1">
|
||||
<div class="w-full">
|
||||
<MangaGrid v-if="viewMode === 'grid'" :mangas="pagedItems" />
|
||||
<MangaList
|
||||
<MangaOverview
|
||||
v-else-if="viewMode === 'list'"
|
||||
:mangas="pagedItems"
|
||||
@manga-click="handleMangaClick" />
|
||||
@@ -45,7 +45,7 @@ import Pagination from '../../../../shared/components/ui/Pagination.vue';
|
||||
import Toolbar from '../../../../shared/components/ui/Toolbar.vue';
|
||||
import { useMangaStore } from '../../application/store/mangaStore';
|
||||
import MangaGrid from '../components/MangaGrid.vue';
|
||||
import MangaList from '../components/MangaList.vue';
|
||||
import MangaOverview from '../components/MangaOverview.vue';
|
||||
import MangaTable from '../components/MangaTable.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
Reference in New Issue
Block a user