Untitled
unknown
plain_text
10 months ago
10 kB
6
Indexable
<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>Editor is loading...
Leave a Comment