Untitled

 avatar
unknown
plain_text
a month ago
10 kB
4
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>
Leave a Comment