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.
172 lines
6.9 KiB
Vue
172 lines
6.9 KiB
Vue
<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>
|