Untitled
<script lang="ts" setup> import MainNavbar from '@/components/MainNavbar.vue'; import { useRouter } from 'vue-router'; import { ref, computed, onMounted } from 'vue'; import { useApi } from '@/composables/Api'; import { useJwtStore } from '@/stores/JwtStore'; import type { Book } from '@/interfaces/Book'; import { jwtDecode } from 'jwt-decode'; const router = useRouter(); const api = useApi(); const jwtStore = useJwtStore(); const currentTime = new Date('2025-01-16T16:29:18+01:00'); const tenMinutesAgo = new Date(currentTime.getTime() - 10 * 60 * 1000); const threeHoursAgo = new Date(currentTime.getTime() - 3 * 60 * 60 * 1000); const books = ref<(Book & { createdAt: string })[]>([ { title: 'Il Signore degli Anelli', author: 'J.R.R. Tolkien', publisher: 'Bompiani', year: '1954', pages: 1200, location: 'Scaffale A3', isbn: '9788845292613', ean: '9788845292613', state: 'available', abstract: 'Un epico viaggio attraverso la Terra di Mezzo', createdAt: tenMinutesAgo.toISOString() }, { title: 'Il Nome della Rosa', author: 'Umberto Eco', publisher: 'Bompiani', year: '1980', pages: 536, location: 'Scaffale B2', isbn: '9788845278266', ean: '9788845278266', state: 'available', abstract: 'Un thriller storico ambientato in un monastero medievale', createdAt: threeHoursAgo.toISOString() } ]); const isLoading = ref(false); const errorMessage = ref(''); const alertMessage = ref(''); const alertType = ref(''); const searchTerm = ref(''); const selectedBookId = ref<number | null>(null); const isTrashVisible = ref(false); const canDelete = ref(true); const longPressTimer = ref<number | null>(null); const longPressDuration = 500; // milliseconds const filteredBooks = computed(() => { const search = searchTerm.value.toLowerCase().trim(); if (!search) return books.value; return books.value.filter((book: Book & { createdAt: string }) => { return ( book.title?.toLowerCase().includes(search) || book.author?.toLowerCase().includes(search) || book.publisher?.toLowerCase().includes(search) || book.isbn?.toLowerCase().includes(search) || book.ean?.toLowerCase().includes(search) ); }); }); function navigateToCreateBook() { router.push({ name: 'book' }); } onMounted(async () => { await fetchBooks(); const { alert, message } = router.currentRoute.value.query; if (alert === 'success') { alertMessage.value = typeof message === 'string' ? message : 'Operazione completata con successo!'; alertType.value = 'success'; setTimeout(() => { alertMessage.value = ''; alertType.value = ''; }, 5000); } }); async function fetchBooks() { isLoading.value = true; try { const decodedToken = jwtDecode(jwtStore.token || ''); const responseData = await api.request(`book/list`, { method: 'POST', headers: { TokenAuthorization: `Bearer ${jwtStore.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ filters: { created_by: (decodedToken as any).id } }), }); books.value = responseData.data; } catch (error) { errorMessage.value = 'Impossibile caricare i libri.'; } finally { isLoading.value = false; } } function navigateToBookDetails(book: Book & { createdAt: string }) { router.push({ name: 'book-details', params: { book: JSON.stringify({ title: book.title, author: book.author, publisher: book.publisher, year: book.year, pages: book.pages, location: book.location, isbn: book.isbn, ean: book.ean, state: book.state, abstract: book.abstract }) } }); } function handleLongPress(bookId: number, createdAt: string) { const now = new Date(); const bookCreatedAt = new Date(createdAt); console.log(bookCreatedAt); const diffInMinutes = Math.abs(now.getTime() - bookCreatedAt.getTime()) / (1000 * 60); if (diffInMinutes <= 30) { selectedBookId.value = bookId; isTrashVisible.value = true; canDelete.value = true; } else { isTrashVisible.value = false; canDelete.value = false; alertMessage.value = 'Non è possibile eliminare questo libro perché è trascorsa più di mezz\'ora.'; alertType.value = 'danger'; setTimeout(() => { alertMessage.value = ''; alertType.value = ''; }, 5000); } } function handleHover(bookId: number, createdAt: string) { const now = new Date(); const bookCreatedAt = new Date(createdAt); const diffInMinutes = Math.abs(now.getTime() - bookCreatedAt.getTime()) / (1000 * 60); if (diffInMinutes <= 30) { selectedBookId.value = bookId; isTrashVisible.value = true; canDelete.value = true; } } function startLongPress(bookId: number, createdAt: string) { longPressTimer.value = window.setTimeout(() => { handleLongPress(bookId, createdAt); }, longPressDuration); } function cancelLongPress() { if (longPressTimer.value) { clearTimeout(longPressTimer.value); longPressTimer.value = null; } cancelDelete(); } function confirmDelete(bookTitle: string) { if (!canDelete.value || selectedBookId.value === null) return; if (window.confirm(`Sei sicuro di voler eliminare il libro: "${bookTitle}"?`)) { deleteBook(selectedBookId.value); } else { cancelDelete(); } } function deleteBook(bookId: number) { api .request(`book/delete/${bookId}`, { method: 'DELETE', headers: { TokenAuthorization: `Bearer ${jwtStore.token}`, }, }) .then(() => { alertMessage.value = 'Libro eliminato con successo!'; alertType.value = 'success'; books.value = books.value.filter((book: any) => book._id !== bookId); }) .catch(() => { alertMessage.value = 'Errore durante l\'eliminazione del libro.'; alertType.value = 'danger'; }) .finally(() => { setTimeout(() => { alertMessage.value = ''; alertType.value = ''; }, 5000); }); } function cancelDelete() { selectedBookId.value = null; isTrashVisible.value = false; canDelete.value = false; } </script> <template> <MainNavbar /> <div class="container"> <div class="mt-2" v-if="alertMessage" :class="['alert', alertType === 'success' ? 'alert-success' : 'alert-danger']" role="alert"> {{ alertMessage }} </div> <div class="row text-center mt-4"> <div class="col-12"> <button class="btn text-white py-5 px-1 w-100" @click="navigateToCreateBook"> <img class="book-stack mb-2" src="/assets/icons/book-stack-icon.svg" /> <span class="m-2 fs-1 fw-bold">Carica nuovi libri</span> <img class="arrow-right mb-2" src="/assets/icons/arrow-right-icon.svg" /> </button> </div> <div class="col mt-4"> <input v-model="searchTerm" class="form-control p-3 fs-3 fw-medium w-100" type="search" placeholder="Filtro rapido" aria-label="Cerca"> </div> </div> <div class="mt-3"> <div v-if="isLoading" class="text-center"> <p>Caricamento in corso...</p> </div> <div v-else> <div v-if="filteredBooks.length === 0" class="text-center mt-4"> <p>Nessun libro trovato</p> </div> <div v-else class="row g-4"> <div v-for="book in filteredBooks" :key="book.id" class="col-6" > <div class="book-card position-relative" @click="navigateToBookDetails(book)" @touchstart="startLongPress(book.id, book.createdAt)" @touchend="cancelLongPress" @mouseover="handleHover(book.id, book.createdAt)" @mouseleave="cancelDelete"> <div class="card h-100" :class="{ 'fade-out': isTrashVisible && selectedBookId === book.id }"> <img :src="book.cover || '/assets/Placeholder-Image.webp'" :class="{'placeholder-image': !book.cover}" class="card-img-top" alt="Cover" /> <div class="card-body d-flex flex-column"> <h5 class="card-title">{{ book.title }}</h5> </div> </div> <div v-if="isTrashVisible && selectedBookId === book.id" class="trash-overlay"> <button class="trash-button" @click.stop="confirmDelete(book.title)"> <i class="bi bi-trash"></i> </button> </div> </div> </div> </div> </div> </div> </div> </template> <style scoped> .btn { background-color: rgb(253, 148, 68); border: 0.2rem; border-radius: 0.5rem; width: 90%; } input { background-color: #FAFBED; } .book-stack, .arrow-right { max-width: 2rem; } a { text-decoration: none; } .placeholder-image { background-image: url('/assets/Placeholder-Image.webp'); background-size: cover; height: 100%; width: 100%; } .book-card { transition: all 0.3s ease; position: relative; cursor: pointer; } .card { transition: opacity 0.3s ease; position: relative; backface-visibility: hidden; } .card.fade-out { opacity: 0; } .trash-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; opacity: 0; transition: opacity 0.3s ease; background: transparent; z-index: 2; pointer-events: none; } .trash-button { pointer-events: all; background: transparent; border: none; padding: 0; cursor: pointer; transition: transform 0.2s ease; z-index: 3; } .trash-button:hover { transform: scale(1.1); } .trash-button .bi-trash { font-size: 3rem; color: #dc3545; filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3)); } .trash-overlay:hover, .book-card:hover .trash-overlay { opacity: 1; } </style>
Leave a Comment