create book modal

This commit is contained in:
Guamss 2025-12-15 21:34:23 +01:00
parent 41ea3ef924
commit 2c0054671e
7 changed files with 281 additions and 42 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>My book list</title>
</head>
<body>
<div id="app"></div>

View File

@ -11,8 +11,6 @@ const { isAuthenticated, logout } = useAuth();
const toastService = useToast();
const authenticationService = new AuthenticationService();
const { login } = useAuth();
const fetchUser = async () => {
if (isAuthenticated.value) {
try {
@ -47,9 +45,6 @@ function handleLogout() {
<nav>
<RouterLink class="nav-logo-div nav-link" to="/">My book list</RouterLink>
<div class="nav-links-div">
<RouterLink class="nav-link" to="/">Accueil</RouterLink>
<RouterLink class="nav-link" to="/">About</RouterLink>
<RouterLink class="nav-link" to="/">Blabla</RouterLink>
</div>
<div class="nav-signin-div">
<RouterLink v-if="!isAuthenticated" class="nav-link" to="/login">Se connecter</RouterLink>

View File

@ -1,9 +1,23 @@
<script setup lang="ts">
import { useAuth } from '@/composables/useAuth';
import type { Book } from '@/models/book';
import { convertState } from '@/utils';
import { ref } from 'vue';
const API_URL = import.meta.env.VITE_MBL_API_URL
const props = defineProps<{ book: Book }>()
const showButtons = ref<boolean>(false);
const { isAuthenticated } = useAuth();
function showButtonsOnHover() {
showButtons.value = true
}
function unshowButtonOnLeave() {
showButtons.value = false
}
const API_URL = import.meta.env.VITE_MBL_API_URL
const props = defineProps<{ book: Book }>()
</script>
<template>
@ -14,7 +28,12 @@ import { convertState } from '@/utils';
<span class="title">{{ props.book.title }}</span>
</a>
<p class="desc">
Livre écrit par <strong>{{ props.book.author }}</strong>
Livre écrit par <strong>{{ props.book.author }}</strong><br>
Ajouté le <strong>{{ props.book.added_at.toLocaleDateString() }}</strong> à <strong>{{
props.book.added_at.getHours() }}:{{ props.book.added_at.getMinutes() }}</strong><br>
<p v-if="props.book.added_at.getTime() !== props.book.updated_at.getTime()">Mis à jour le <strong>{{
props.book.updated_at.toLocaleDateString() }}</strong> à <strong>{{ props.book.updated_at.getHours() }}:{{
props.book.updated_at.getMinutes() }}</strong></p>
</p>
<p v-if="book.state === 'COMPLETED' || props.book.state === 'DROPPED'">Note
{{ props.book.note }}/5</p>
@ -23,11 +42,15 @@ import { convertState } from '@/utils';
<div :class="props.book.state" class="state-indicator"></div>
</div>
</div>
<p v-if="isAuthenticated" @mouseover="showButtonsOnHover" @mouseleave="unshowButtonOnLeave" class="chevron"><font-awesome-icon icon="fa-chevron-down" /></p>
<div class="buttons" v-show="showButtons">
<button>Modifier</button>
<button>Supprimer</button>
</div>
</div>
</template>
<style>
.COMPLETED {
background-color: #2d4276;
}
@ -44,6 +67,13 @@ import { convertState } from '@/utils';
background-color: #832f30;
}
.buttons {
display: flex;
align-items: stretch;
gap: 1em;
justify-content: center;
}
.state-div {
display: flex;
align-items: center;
@ -75,6 +105,16 @@ import { convertState } from '@/utils';
border: 1px solid transparent;
}
.chevron {
margin-top: 0;
align-self: center;
transition: background-color 0.5s ease, transform 0.5s ease;
}
.chevron:hover {
transform: rotate(180deg);
}
.card a {
text-decoration: none
}
@ -100,5 +140,4 @@ import { convertState } from '@/utils';
font-size: 0.875rem;
line-height: 1.25rem;
}
</style>

View File

@ -1,29 +1,135 @@
<script setup lang="ts">
import { useAuth } from "@/composables/useAuth";
import type { Book } from "@/models/book";
import { User } from "@/models/user";
import { AuthenticationService } from "@/services/authentication.service";
import { BookService } from "@/services/book.service";
import { onMounted, ref } from "vue";
const props = defineProps<{ isActive: boolean }>();
const emit = defineEmits(['closeBookFormModal'])
function sendBookData() {
console.log("PROUT")
emit('closeBookFormModal')
const emit = defineEmits(["closeBookFormModal", "newBook"]);
const title = ref("");
const author = ref("");
const state = ref("PLAN");
const note = ref<number>(0);
const illustrationFile = ref<File | null>(null);
const concernedUser = ref<User | undefined>(undefined);
const authenticationService = new AuthenticationService();
const bookService = new BookService();
const { isAuthenticated } = useAuth();
onMounted(async () => {
{
if (isAuthenticated.value) {
try {
concernedUser.value = await authenticationService.findMe();
} catch (error: any) {
console.error(error);
}
} else {
concernedUser.value = undefined;
}
}
});
function handleFileUpload($event: Event) {
const target = $event.target as HTMLInputElement;
if (target && target.files) {
illustrationFile.value = target.files[0];
}
}
async function sendBookData() {
const form = new FormData();
form.append("title", title.value);
form.append("author", author.value);
form.append("note", `${note.value}`);
form.append("state", state.value);
form.append("illustration", illustrationFile.value!);
console.log(concernedUser.value)
if (concernedUser.value?.id) {
form.append("user", concernedUser.value.id.toString());
}
const newBook: Book = await bookService.createBook(form);
emit('newBook', newBook);
emit("closeBookFormModal");
}
</script>
<template>
<div :class="props.isActive ? 'opened-modal' : 'closed-modal'">
<div class="overlay"></div>
<div class="overlay" @click="$emit('closeBookFormModal')"></div>
<div class="modale card">
<button class="btn-close" @click="$emit('closeBookFormModal')">X</button>
<h2>COUCOU</h2>
<h2>Ajouter un livre</h2>
<form @submit.prevent="sendBookData">
<label for="title">Titre</label>
<input v-model="title" type="text" id="title" name="title" required />
<label for="author">Auteur</label>
<input v-model="author" type="text" id="author" name="author" required />
<label for="state">État</label>
<select v-model="state" id="state" name="state">
<option value="PLAN">À lire</option>
<option value="READING">En lecture</option>
<option value="COMPLETED">Complété</option>
<option value="DROPPED">Lâché</option>
</select>
<label for="note">Note</label>
<input v-model="note" type="number" id="note" name="note" max="5" min="0" />
<label for="illustration">Illustration</label>
<input
@change="handleFileUpload($event)"
id="illustration"
name="illustration"
accept="image/png, image/jpeg"
type="file"
/>
<div class="actions">
<button class="send" @keyup.enter="sendBookData" @click="sendBookData">prout</button>
<button class="send" type="submit">Créer</button>
</div>
</form>
</div>
</div>
</template>
<style scoped>
input[type="file"]::file-selector-button {
border-radius: 0.5em;
margin-right: 8px;
border: none;
background: #1e90ff;
padding: 8px 12px;
color: #fff;
cursor: pointer;
}
input[type="file"]::file-selector-button:hover {
transition: 0.3s;
background-color: #176cc0;
}
form {
display: flex;
flex-direction: column;
gap: 0.5em;
}
input,
select {
padding: 12px;
border-radius: 0.5rem;
}
.opened-modal {
position: fixed;
top: 0;

View File

@ -1,14 +1,20 @@
import { createApp } from 'vue'
import App from './App.vue'
import ToastPlugin from 'vue-toast-notification'
import { router } from './routes'
import 'vue-toast-notification/dist/theme-sugar.css'
import { createApp } from "vue";
import App from "./App.vue";
import ToastPlugin from "vue-toast-notification";
import { router } from "./routes";
import "vue-toast-notification/dist/theme-sugar.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faArrowRightFromBracket } from "@fortawesome/free-solid-svg-icons";
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
library.add(faArrowRightFromBracket);
library.add(faPlus);
library.add(faChevronDown);
createApp(App).use(router).use(ToastPlugin).component("font-awesome-icon", FontAwesomeIcon).mount('#app')
createApp(App)
.use(router)
.use(ToastPlugin)
.component("font-awesome-icon", FontAwesomeIcon)
.mount("#app");

View File

@ -1,14 +1,62 @@
import type { Book } from "@/models/book";
import axios from "axios";
import axios, { type AxiosInstance, type AxiosError } from "axios";
export class BookService {
private client: AxiosInstance;
constructor() {
const apiURL = import.meta.env.VITE_MBL_API_URL;
axios.defaults.baseURL = `${apiURL}/api/books`;
this.client = axios.create({
baseURL: `${apiURL}/api/books`,
});
this.setupInterceptors();
}
async findBooks() {
return await axios
private setupInterceptors() {
this.client.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem("access");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as any;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newData = await this.refreshToken();
sessionStorage.setItem("access", newData.access);
originalRequest.headers.Authorization = `Bearer ${newData.access}`;
return this.client(originalRequest);
} catch (refreshError) {
console.error("Session expirée, impossible de rafraîchir.");
sessionStorage.removeItem("access");
localStorage.removeItem("refresh");
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
}
async findBooks() {
return await this.client
.get("")
.then((res) => {
return res.data as Book[];
@ -17,4 +65,30 @@ export class BookService {
throw error;
});
}
async createBook(formData: FormData) {
return await this.client
.post("", formData)
.then((res) => {
return res.data as Book;
})
.catch((error) => {
throw error;
});
}
async refreshToken() {
const refreshToken = localStorage.getItem("refresh");
const apiURL = import.meta.env.VITE_MBL_API_URL;
return await axios
.post(`${apiURL}/api/token/refresh/`, {
refresh: refreshToken,
})
.then((res) => {
return res.data;
})
.catch((error) => {
throw error;
});
}
}

View File

@ -4,10 +4,35 @@ import { BookService } from "@/services/book.service";
import { onMounted, ref } from "vue";
import BookCard from "@/components/BookCard.vue";
import CreateBookForm from "@/components/CreateBookForm.vue";
import { useAuth } from "@/composables/useAuth";
import { useToast } from "vue-toast-notification";
const books = ref<Book[]>([]);
const bookService = new BookService();
const isCreatingBookFormOpened = ref<boolean>(false);
const { isAuthenticated } = useAuth();
const toastService = useToast();
function sortBooks(a: Book, b: Book) {
if (a.updated_at > b.updated_at) {
return -1;
} else if (a.updated_at < b.updated_at) {
return 1;
}
return 0;
}
function pushNewBook(book: Book) {
book.added_at = new Date(book.added_at);
book.updated_at = new Date(book.updated_at);
books.value.push(book);
books.value.sort(sortBooks);
toastService.success(`Livre créé avec succès"`, {
position: "bottom-right",
pauseOnHover: true,
dismissible: true,
});
}
function closeBookFormModal() {
isCreatingBookFormOpened.value = false;
@ -18,28 +43,24 @@ onMounted(async () => {
books.value.map((book) => {
book.added_at = new Date(book.added_at);
book.updated_at = new Date(book.updated_at);
});
books.value.sort((a, b) => {
if (a.updated_at > b.updated_at) {
return -1;
} else if (a.updated_at < b.updated_at) {
return 1;
}
return 0;
});
books.value.sort(sortBooks);
});
</script>
<template>
<CreateBookForm
v-if="isAuthenticated"
:is-active="isCreatingBookFormOpened"
@new-book="(book) => pushNewBook(book)"
@close-book-form-modal="closeBookFormModal"
/>
<main class="cards">
<BookCard v-for="book in books" :book="book" :key="book.id" class="card" />
</main>
<div class="add-button-container">
<div v-if="isAuthenticated" class="add-button-container">
<button @click="isCreatingBookFormOpened = true" class="add-button">
<h3><font-awesome-icon icon="fa-plus" /> Ajouter un livre</h3>
</button>
@ -47,7 +68,6 @@ onMounted(async () => {
</template>
<style scoped>
.add-button-container {
position: fixed;
bottom: 0;
@ -110,5 +130,4 @@ onMounted(async () => {
justify-content: center;
}
}
</style>