feat(frontend): init book-farm ui
This commit is contained in:
commit
0ce3ad0718
4
.browserslistrc
Normal file
4
.browserslistrc
Normal file
@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
6
.editorconfig
Normal file
6
.editorconfig
Normal file
@ -0,0 +1,6 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM node:lts-alpine
|
||||
|
||||
ARG api=http://192.168.1.51:8000
|
||||
# "https://app.bookfarm.spacesheep.ovh"
|
||||
ENV VITE_BOOK_API_URL=$api
|
||||
|
||||
# install simple http server for serving static content
|
||||
RUN npm install -g http-server
|
||||
|
||||
# make the 'app' folder the current working directory
|
||||
WORKDIR /app
|
||||
|
||||
# copy both 'package.json' and 'package-lock.json' (if available)
|
||||
COPY package*.json ./
|
||||
# install project dependencies
|
||||
|
||||
RUN npm install
|
||||
|
||||
# copy project files and folders to the current working directory (i.e. 'app' folder)
|
||||
COPY . .
|
||||
|
||||
# build app for production with minification
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 8080
|
||||
CMD [ "http-server", "dist" ]
|
||||
|
||||
|
||||
#sudo docker build -t book-farm/frontend .
|
||||
#sudo docker run -it -p 8080:8080 --rm --name book-farm-frontend book-farm/frontend
|
||||
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Vuetify (Default)
|
||||
|
||||
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||
|
||||
## ❗️ Important Links
|
||||
|
||||
- 📄 [Docs](https://vuetifyjs.com/)
|
||||
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||
|
||||
## 💿 Install
|
||||
|
||||
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||
|
||||
| Package Manager | Command |
|
||||
|---------------------------------------------------------------|----------------|
|
||||
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||
|
||||
After completing the installation, your environment is ready for Vuetify development.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||
|
||||
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||
|
||||
## 💡 Usage
|
||||
|
||||
This section covers how to start the development server and build your project for production.
|
||||
|
||||
### Starting the Development Server
|
||||
|
||||
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||
|
||||
### Building for Production
|
||||
|
||||
To build your project for production, use:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||
|
||||
## 💪 Support Vuetify Development
|
||||
|
||||
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||
|
||||
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||
|
||||
## 📑 License
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2016-present Vuetify, LLC
|
||||
26
components.d.ts
vendored
Normal file
26
components.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ActiveFilters: typeof import('./src/components/filter/ActiveFilters.vue')['default']
|
||||
BookCard: typeof import('./src/components/book/BookCard.vue')['default']
|
||||
BookCardSkeleton: typeof import('./src/components/skeletons/BookCardSkeleton.vue')['default']
|
||||
BookDetailsSkeleton: typeof import('./src/components/skeletons/BookDetailsSkeleton.vue')['default']
|
||||
BookLightCard: typeof import('./src/components/book/BookLightCard.vue')['default']
|
||||
CollectionsFilter: typeof import('./src/components/filter/CollectionsFilter.vue')['default']
|
||||
Filters: typeof import('./src/components/filter/Filters.vue')['default']
|
||||
FiltersPanel: typeof import('./src/components/filter/FiltersPanel.vue')['default']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
NavBar: typeof import('./src/components/NavBar.vue')['default']
|
||||
PageNumberFilter: typeof import('./src/components/filter/PageNumberFilter.vue')['default']
|
||||
PublishedDateFilter: typeof import('./src/components/filter/PublishedDateFilter.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Suggestion: typeof import('./src/components/Suggestion.vue')['default']
|
||||
}
|
||||
}
|
||||
2
env.d.ts
vendored
Normal file
2
env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="unplugin-vue-router/client" />
|
||||
36
eslint.config.js
Normal file
36
eslint.config.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* .eslint.js
|
||||
*
|
||||
* ESLint configuration file.
|
||||
*/
|
||||
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import vueTsEslintConfig from '@vue/eslint-config-typescript'
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'app/files-to-ignore',
|
||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
...pluginVue.configs['flat/recommended'],
|
||||
...vueTsEslintConfig(),
|
||||
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
},
|
||||
],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
}
|
||||
}
|
||||
]
|
||||
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BookFarm</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
12862
package-lock.json
generated
Normal file
12862
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "library",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.7.9",
|
||||
"core-js": "^3.37.1",
|
||||
"pinia": "^2.3.1",
|
||||
"roboto-fontface": "*",
|
||||
"vue": "^3.4.31",
|
||||
"vuetify": "^3.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.9.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/eslint-config-typescript": "^14.1.3",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/nuxt": "^12.5.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-vue": "^9.30.0",
|
||||
"npm-run-all2": "^7.0.1",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.77.8",
|
||||
"typescript": "~5.6.3",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"unplugin-vue-router": "^0.10.0",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
BIN
public/background.jpg
Normal file
BIN
public/background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
33
src/App.vue
Normal file
33
src/App.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main class="bookfarm-app">
|
||||
<div class="library">
|
||||
<v-app-bar
|
||||
:elevation="2"
|
||||
color="#EEEBE0"
|
||||
>
|
||||
<div style="width:64px">
|
||||
<v-img
|
||||
src="@/assets/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<v-app-bar-title class="font-weight-medium">
|
||||
BookFarm
|
||||
</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
<router-view />
|
||||
</div>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
<style lang="css">
|
||||
.library {
|
||||
margin-top: 8px;
|
||||
padding: 0 12px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
||||
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
23
src/components/NavBar.vue
Normal file
23
src/components/NavBar.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar
|
||||
:elevation="2"
|
||||
color="#F0EAD2"
|
||||
class="bookfarm-bar"
|
||||
>
|
||||
<div style="width:64px">
|
||||
<v-img
|
||||
src="@/assets/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<v-app-bar-title class="font-weight-medium">
|
||||
BookFarm
|
||||
</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
35
src/components/README.md
Normal file
35
src/components/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Components
|
||||
|
||||
Vue template files in this folder are automatically imported.
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it.
|
||||
|
||||
The following example assumes a component located at `src/components/MyComponent.vue`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
```
|
||||
|
||||
When your template is rendered, the component's import will automatically be inlined, which renders to this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<MyComponent />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MyComponent from '@/components/MyComponent.vue'
|
||||
</script>
|
||||
```
|
||||
100
src/components/Suggestion.vue
Normal file
100
src/components/Suggestion.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, type Ref, ref, watch} from "vue";
|
||||
import {getBookRecommandations} from "@/services/bookApi";
|
||||
import type {Book} from "@/services/models/book";
|
||||
import BookLightCard from "@/components/book/BookLightCard.vue";
|
||||
import type {BookSimilarParams} from "@/services/models/filter";
|
||||
|
||||
const model = ref(null);
|
||||
const books: Ref<Book[]> = ref([]);
|
||||
|
||||
const {book} = defineProps<{ book: Book }>()
|
||||
onMounted(getSimilarBooks);
|
||||
|
||||
async function getSimilarBooks() {
|
||||
const params : BookSimilarParams= {
|
||||
author: undefined,
|
||||
fast: undefined,
|
||||
collection: undefined
|
||||
};
|
||||
if(selectedOption.value === 0){
|
||||
params.fast = true;
|
||||
}
|
||||
else if(selectedOption.value === 1){
|
||||
params.author = true;
|
||||
} else if(selectedOption.value === 2){
|
||||
params.collection = true;
|
||||
}
|
||||
|
||||
return getBookRecommandations(book.id, params)
|
||||
.then(result => {
|
||||
books.value = result;
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
})
|
||||
}
|
||||
|
||||
const suggestionOptions = [
|
||||
{label: 'Vous aimeriez aussi', key:0},
|
||||
{label: 'Du même auteur', key:1},
|
||||
{label: 'Dans la même collection', key:2}
|
||||
]
|
||||
|
||||
const selectedOption = ref(0)
|
||||
|
||||
watch(selectedOption, getSimilarBooks)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-sheet>
|
||||
<span class="d-flex ga-1 bg-white pa-2">
|
||||
<v-select
|
||||
v-model="selectedOption"
|
||||
class="font-weight-medium font-italic"
|
||||
hide-details
|
||||
item-title="label"
|
||||
item-value="key"
|
||||
:items="suggestionOptions"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon
|
||||
icon="mdi-heart"
|
||||
color="pink"
|
||||
/>
|
||||
</template>
|
||||
</v-select>
|
||||
</span>
|
||||
<v-slide-group
|
||||
v-model="model"
|
||||
class="pa-0"
|
||||
center-active
|
||||
show-arrows
|
||||
>
|
||||
<v-slide-group-item
|
||||
v-for="book in books"
|
||||
:key="book.id"
|
||||
v-slot="{ toggle }"
|
||||
>
|
||||
<v-card
|
||||
color="primary"
|
||||
class="ma-2"
|
||||
height="118"
|
||||
width="118"
|
||||
@click="toggle"
|
||||
>
|
||||
<div class="d-flex fill-height align-center justify-center">
|
||||
<BookLightCard :book />
|
||||
</div>
|
||||
</v-card>
|
||||
</v-slide-group-item>
|
||||
</v-slide-group>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-slide-group__prev, .v-slide-group__next {
|
||||
flex-basis: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
</style>
|
||||
84
src/components/book/BookCard.vue
Normal file
84
src/components/book/BookCard.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import type {Book} from "@/services/models/book";
|
||||
|
||||
defineProps<{ book: Book }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
width="100%"
|
||||
hover
|
||||
height="120px"
|
||||
rounded="lg"
|
||||
color="white"
|
||||
:to="`book/${book.id}`"
|
||||
>
|
||||
<div class="d-flex ga-1">
|
||||
<v-avatar
|
||||
rounded="lg"
|
||||
size="120"
|
||||
>
|
||||
<v-img
|
||||
:src="book.image_url ?? `https://cdn.vuetifyjs.com/images/cards/sunshine.jpg`"
|
||||
/>
|
||||
</v-avatar>
|
||||
<div class="book-information">
|
||||
<div class="book-author-title ">
|
||||
<p
|
||||
v-if="book.product_title"
|
||||
class="text-primary font-weight-bold"
|
||||
>
|
||||
{{ book.product_title }}
|
||||
</p>
|
||||
<p
|
||||
v-if="book.author"
|
||||
class="text-secondary font-weight-medium text-capitalize"
|
||||
>
|
||||
{{ book.author.split("_").join(" ") }}
|
||||
</p>
|
||||
</div>
|
||||
<v-chip-group
|
||||
variant="flat"
|
||||
column
|
||||
>
|
||||
<v-chip
|
||||
class="ma-2"
|
||||
label
|
||||
size="x-small"
|
||||
>🗓️
|
||||
{{ book.date_de_parution }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
class="ma-2"
|
||||
label
|
||||
size="x-small"
|
||||
>
|
||||
📚 {{ book.nb_de_pages }} pages
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.book-information {
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.book-author-title {
|
||||
padding: 4px 4px 0 4px;
|
||||
|
||||
> * {
|
||||
padding: 0 2px;
|
||||
font-size: 16px;
|
||||
text-wrap: wrap;
|
||||
line-height: 1em;
|
||||
max-height: 2em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
58
src/components/book/BookLightCard.vue
Normal file
58
src/components/book/BookLightCard.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import type {Book} from "@/services/models/book";
|
||||
|
||||
defineProps<{ book: Book }>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
width="100%"
|
||||
hover
|
||||
rounded="lg"
|
||||
color="white"
|
||||
:to="`/book/${book.id}`"
|
||||
>
|
||||
<v-img
|
||||
:src="book.image_url ?? `https://cdn.vuetifyjs.com/images/cards/sunshine.jpg`"
|
||||
class="align-end"
|
||||
gradient="to bottom, rgba(0,0,0,.1), rgba(0,0,0,.5)"
|
||||
height="118px"
|
||||
cover
|
||||
>
|
||||
<v-card-title>
|
||||
<div class="book-author-title ">
|
||||
<p
|
||||
v-if="book.product_title"
|
||||
class="font-weight-bold"
|
||||
>
|
||||
{{ book.product_title }}
|
||||
</p>
|
||||
<p
|
||||
v-if="book.author"
|
||||
class="font-weight-medium text-capitalize"
|
||||
>
|
||||
{{ book.author.split("_").join(" ") }}
|
||||
</p>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-img>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.book-author-title {
|
||||
color: white;
|
||||
|
||||
& p {
|
||||
font-size: 12px;
|
||||
text-wrap: wrap;
|
||||
line-height: 1em;
|
||||
max-height: 2em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
45
src/components/filter/ActiveFilters.vue
Normal file
45
src/components/filter/ActiveFilters.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
|
||||
|
||||
const {filters} = storeToRefs(useBooksStore());
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="selected-filters">
|
||||
<v-chip-group column>
|
||||
<template v-if="filters.mot_clef">
|
||||
<v-chip>Mot-clef: {{ filters.mot_clef }}</v-chip>
|
||||
</template>
|
||||
<template v-if="filters.collections">
|
||||
<v-chip
|
||||
v-for="collection in filters.collections"
|
||||
:key="collection"
|
||||
>
|
||||
Collection: {{ collection }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-if="filters.nb_de_pages.min">
|
||||
<v-chip>📚 Nombre de pages minimum: {{ filters.nb_de_pages.min }}</v-chip>
|
||||
</template>
|
||||
<template v-if="filters.nb_de_pages.max">
|
||||
<v-chip>📚 Nombre de pages maximum: {{ filters.nb_de_pages.max }}</v-chip>
|
||||
</template>
|
||||
<template v-if="filters.date_de_parution.après">
|
||||
<v-chip>🗓️ Publié après: {{ filters.date_de_parution.après }}</v-chip>
|
||||
</template>
|
||||
<template v-if="filters.date_de_parution.avant">
|
||||
<v-chip>🗓️ Publié avant : {{ filters.date_de_parution.avant }}</v-chip>
|
||||
</template>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selected-filters {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
36
src/components/filter/CollectionsFilter.vue
Normal file
36
src/components/filter/CollectionsFilter.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, type Ref, ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
import {getCollections} from "@/services/bookApi";
|
||||
|
||||
|
||||
const {filters} = storeToRefs(useBooksStore());
|
||||
|
||||
const collections: Ref<string[]> = ref([]);
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getCollections()
|
||||
.then((result) => {
|
||||
collections.value = result;
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card-item>
|
||||
<p class="font-weight-regular">
|
||||
Collections
|
||||
</p>
|
||||
<v-select
|
||||
v-model="filters.collections"
|
||||
clearable
|
||||
chips
|
||||
label="Filtrer sur des collections"
|
||||
:items="collections"
|
||||
multiple
|
||||
/>
|
||||
</v-card-item>
|
||||
</template>
|
||||
46
src/components/filter/Filters.vue
Normal file
46
src/components/filter/Filters.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ref} from "vue";
|
||||
import FiltersPanel from "@/components/filter/FiltersPanel.vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
import ActiveFilters from "@/components/filter/ActiveFilters.vue";
|
||||
|
||||
const isPanelOpened = ref(false);
|
||||
|
||||
function togglePanel() {
|
||||
isPanelOpened.value = !isPanelOpened.value;
|
||||
}
|
||||
|
||||
const {filters} = storeToRefs(useBooksStore());
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex ga-2 align-center">
|
||||
<v-text-field
|
||||
v-model="filters.mot_clef"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
clearable
|
||||
bg-color="#FFFFFF"
|
||||
placeholder="Search a book"
|
||||
hide-details
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-filter"
|
||||
color="secondary"
|
||||
density="comfortable"
|
||||
size="small"
|
||||
@click="togglePanel"
|
||||
/>
|
||||
</div>
|
||||
<ActiveFilters />
|
||||
<FiltersPanel
|
||||
:is-panel-opened="isPanelOpened"
|
||||
@close="togglePanel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
45
src/components/filter/FiltersPanel.vue
Normal file
45
src/components/filter/FiltersPanel.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import PageNumberFilter from "@/components/filter/PageNumberFilter.vue";
|
||||
import CollectionsFilter from "@/components/filter/CollectionsFilter.vue";
|
||||
import PublishedDateFilter from "@/components/filter/PublishedDateFilter.vue";
|
||||
|
||||
defineProps<{ isPanelOpened: boolean }>();
|
||||
defineEmits(["close"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-expand-x-transition class="panel">
|
||||
<v-card
|
||||
v-show="isPanelOpened"
|
||||
class="mx-auto"
|
||||
bg-color="white"
|
||||
height="100%"
|
||||
width="100%"
|
||||
@keydown.esc="$emit('close')"
|
||||
>
|
||||
<v-card-title class="d-flex justify-space-between pa-2 mt-2">
|
||||
<span>Filtres</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
density="compact"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<PageNumberFilter />
|
||||
<CollectionsFilter />
|
||||
<PublishedDateFilter />
|
||||
</v-card>
|
||||
</v-expand-x-transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
position: absolute;
|
||||
width: 80%;
|
||||
top: 64px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
49
src/components/filter/PageNumberFilter.vue
Normal file
49
src/components/filter/PageNumberFilter.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {type Ref, ref, watch} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
|
||||
const {filters} = storeToRefs(useBooksStore());
|
||||
|
||||
const pageSize: Ref<number[]> = ref([0, 2500]);
|
||||
|
||||
|
||||
watch(() => pageSize.value, () => {
|
||||
const [min, max] = pageSize.value;
|
||||
if (min !== 0) {
|
||||
filters.value.nb_de_pages.min = min;
|
||||
} else {
|
||||
filters.value.nb_de_pages.min = undefined;
|
||||
}
|
||||
if (max !== 2500) {
|
||||
filters.value.nb_de_pages.max = max;
|
||||
} else {
|
||||
filters.value.nb_de_pages.max = undefined;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card-item>
|
||||
<p class="font-weight-regular">
|
||||
Nombre de pages
|
||||
</p>
|
||||
<v-range-slider
|
||||
v-model="pageSize"
|
||||
:min="0"
|
||||
:step="10"
|
||||
:max="2500"
|
||||
class="align-center"
|
||||
hide-details
|
||||
>
|
||||
<template #prepend>
|
||||
{{ pageSize[0] }}
|
||||
</template>
|
||||
<template #append>
|
||||
{{ pageSize[1] }}
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-card-item>
|
||||
</template>
|
||||
|
||||
49
src/components/filter/PublishedDateFilter.vue
Normal file
49
src/components/filter/PublishedDateFilter.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {type Ref, ref, watch} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
|
||||
const {filters} = storeToRefs(useBooksStore());
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const publishedDate: Ref<number[]> = ref([1950, currentYear]);
|
||||
|
||||
watch(() => publishedDate.value, () => {
|
||||
const [min, max] = publishedDate.value;
|
||||
if (min !== 1950) {
|
||||
filters.value.date_de_parution.après = min.toString();
|
||||
} else {
|
||||
filters.value.date_de_parution.après = undefined;
|
||||
}
|
||||
if (max !== currentYear) {
|
||||
filters.value.date_de_parution.avant = max.toString();
|
||||
} else {
|
||||
filters.value.date_de_parution.avant = undefined;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card-item>
|
||||
<p class="font-weight-regular">
|
||||
Date de parution
|
||||
</p>
|
||||
<v-range-slider
|
||||
v-model="publishedDate"
|
||||
:min="1950"
|
||||
:max="currentYear"
|
||||
:step="1"
|
||||
class="align-center"
|
||||
hide-details
|
||||
>
|
||||
<template #prepend>
|
||||
{{ publishedDate[0] }}
|
||||
</template>
|
||||
<template #append>
|
||||
{{ publishedDate[1] }}
|
||||
</template>
|
||||
</v-range-slider>
|
||||
</v-card-item>
|
||||
</template>
|
||||
|
||||
11
src/components/skeletons/BookCardSkeleton.vue
Normal file
11
src/components/skeletons/BookCardSkeleton.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div
|
||||
class="v-skeleton-loader"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg v-skeleton-loader__bone v-skeleton-loader__image"
|
||||
style="height: 120px;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
20
src/components/skeletons/BookDetailsSkeleton.vue
Normal file
20
src/components/skeletons/BookDetailsSkeleton.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
class="v-skeleton-loader"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg v-skeleton-loader__bone v-skeleton-loader__image"
|
||||
style="height: 300px; width: 300px"
|
||||
/>
|
||||
</div>
|
||||
<v-skeleton-loader
|
||||
type="sentences"
|
||||
width="100"
|
||||
class="bg-transparent"
|
||||
/>
|
||||
<v-skeleton-loader
|
||||
type="paragraph"
|
||||
width="300"
|
||||
class="bg-transparent"
|
||||
/>
|
||||
</template>
|
||||
20
src/main.ts
Normal file
20
src/main.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
192
src/pages/BookDetails.vue
Normal file
192
src/pages/BookDetails.vue
Normal file
@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import Suggestion from "@/components/Suggestion.vue";
|
||||
import {useBookStore} from "@/services/stores/bookStore";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
watch(() => router.currentRoute, () => {
|
||||
console.log("watch", router.currentRoute.value.params.id)
|
||||
bookStore.loadBook(router.currentRoute.value.params.id as string)
|
||||
},)
|
||||
|
||||
const bookStore = useBookStore();
|
||||
const {book, isLoading} = storeToRefs(bookStore)
|
||||
|
||||
onMounted(() => {
|
||||
bookStore.loadBook(router.currentRoute.value.params.id as string);
|
||||
})
|
||||
|
||||
const shouldShowReco = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-fab
|
||||
icon="mdi-arrow-left"
|
||||
location="top left"
|
||||
size="small"
|
||||
class="back-button"
|
||||
position="absolute"
|
||||
:to="{name: 'search'}"
|
||||
/>
|
||||
|
||||
<div class="book-details mt-4 mb-10 d-flex flex-column align-center">
|
||||
<template v-if="isLoading">
|
||||
<div
|
||||
class="v-skeleton-loader"
|
||||
>
|
||||
<div
|
||||
class="rounded-lg v-skeleton-loader__bone v-skeleton-loader__image"
|
||||
style="height: 300px; width: 300px"
|
||||
/>
|
||||
</div>
|
||||
<v-skeleton-loader
|
||||
type="sentences"
|
||||
width="100"
|
||||
class="bg-transparent"
|
||||
/>
|
||||
<v-skeleton-loader
|
||||
type="paragraph"
|
||||
width="300"
|
||||
class="bg-transparent"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-avatar
|
||||
rounded="lg"
|
||||
size="300"
|
||||
>
|
||||
<v-img
|
||||
:src="book?.image_url ?? `https://cdn.vuetifyjs.com/images/cards/sunshine.jpg`"
|
||||
/>
|
||||
</v-avatar>
|
||||
<div class="book-author-title d-flex flex-column align-center">
|
||||
<p
|
||||
v-if="book?.product_title"
|
||||
class="text-primary font-weight-bold"
|
||||
>
|
||||
{{ book.product_title }}
|
||||
</p>
|
||||
<p
|
||||
v-if="book?.author"
|
||||
class="text-secondary font-weight-medium text-capitalize"
|
||||
>
|
||||
{{ book.author.split("_").join(" ") }}
|
||||
</p>
|
||||
</div>
|
||||
<section
|
||||
v-if="book?.resume"
|
||||
class="ma-1"
|
||||
>
|
||||
<span class="text-decoration-underline text-primary font-weight-medium">Résumé</span>
|
||||
<span class="book-summary">{{ book.resume }}</span>
|
||||
</section>
|
||||
<section class="ma-1 book-characteristics">
|
||||
<span class="text-decoration-underline text-primary font-weight-medium">Caractéristiques</span>
|
||||
|
||||
<div>
|
||||
<span class="charac">
|
||||
<p class="font-weight-medium">Date de parution</p>
|
||||
<p>{{ book?.date_de_parution }}</p>
|
||||
</span>
|
||||
<span class="charac">
|
||||
<p class="font-weight-medium">Éditeur</p>
|
||||
<p>{{ book?.editeur }}</p>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="charac">
|
||||
<p class="font-weight-medium">Collection</p>
|
||||
<p>{{ book?.collection?.split("_").join(" ") }}</p>
|
||||
</span>
|
||||
<span class="charac">
|
||||
<p class="font-weight-medium">Nombre de pages</p>
|
||||
<p>{{ book?.nb_de_pages }}</p>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
<v-card
|
||||
class="recommandations"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<v-slide-y-reverse-transition v-if="!isLoading">
|
||||
<v-card v-show="shouldShowReco">
|
||||
<Suggestion :book="book!"/>
|
||||
</v-card>
|
||||
</v-slide-y-reverse-transition>
|
||||
<v-btn
|
||||
width="100%"
|
||||
@click="shouldShowReco = !shouldShowReco"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<p>
|
||||
{{ shouldShowReco ? "Cacher" : "Voir" }} les recommandations
|
||||
</p>
|
||||
<v-btn
|
||||
size="x-small"
|
||||
flat
|
||||
:icon="shouldShowReco ? 'mdi-chevron-down' : 'mdi-chevron-up'"
|
||||
/>
|
||||
</div>
|
||||
</v-btn>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.book-details {
|
||||
width: 300px;
|
||||
margin-left: calc((100% - 300px) / 2);
|
||||
}
|
||||
|
||||
.recommandations {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0
|
||||
|
||||
}
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.book-summary {
|
||||
width: 100%;
|
||||
line-height: 1.2em;
|
||||
max-height: 7em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 7;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.book-characteristics {
|
||||
& .charac {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
line-height: 1.2em;
|
||||
margin-top: 4px;
|
||||
|
||||
& > p:first-child {
|
||||
background-color: #DDE5B6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.back-button {
|
||||
height: fit-content !important;
|
||||
width: 64px;
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
5
src/pages/README.md
Normal file
5
src/pages/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Pages
|
||||
|
||||
Vue components created in this folder will automatically be converted to navigatable routes.
|
||||
|
||||
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.
|
||||
45
src/pages/Search.vue
Normal file
45
src/pages/Search.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted} from "vue";
|
||||
import BookCard from "@/components/book/BookCard.vue";
|
||||
import Filters from "@/components/filter/Filters.vue";
|
||||
import {useBooksStore} from "@/services/stores/booksStore";
|
||||
import {storeToRefs} from "pinia";
|
||||
import BookCardSkeleton from "@/components/skeletons/BookCardSkeleton.vue";
|
||||
|
||||
const booksStore = useBooksStore();
|
||||
const {books, areBooksLoading, noData} = storeToRefs(booksStore)
|
||||
onMounted(booksStore.searchBooks);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Filters />
|
||||
<div class="books">
|
||||
<template v-if="areBooksLoading">
|
||||
<BookCardSkeleton
|
||||
v-for="i in [1,2,3]"
|
||||
:key="i"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="noData">
|
||||
Aucun livre trouvé ...
|
||||
</template>
|
||||
<template v-else>
|
||||
<BookCard
|
||||
v-for="book in books"
|
||||
:key="book.id"
|
||||
:book
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.books {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
7
src/pages/index.vue
Normal file
7
src/pages/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<HelloWorld />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
3
src/plugins/README.md
Normal file
3
src/plugins/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.
|
||||
20
src/plugins/index.ts
Normal file
20
src/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* plugins/index.ts
|
||||
*
|
||||
* Automatically included in `./src/main.ts`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
import router from '../router'
|
||||
import { createPinia } from 'pinia'
|
||||
// Types
|
||||
import type { App } from 'vue'
|
||||
|
||||
const pinia = createPinia()
|
||||
export function registerPlugins (app: App) {
|
||||
app
|
||||
.use(pinia)
|
||||
.use(vuetify)
|
||||
.use(router)
|
||||
}
|
||||
42
src/plugins/vuetify.ts
Normal file
42
src/plugins/vuetify.ts
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* plugins/vuetify.ts
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import {createVuetify, type ThemeDefinition} from 'vuetify'
|
||||
|
||||
const myCustomLightTheme: ThemeDefinition = {
|
||||
dark: false,
|
||||
colors: {
|
||||
background: '#F0EAD2',
|
||||
surface: '#f5f4f0',
|
||||
primary: '#6C584C',
|
||||
'primary-darken-1': '#3700B3',
|
||||
secondary: '#A98467',
|
||||
'secondary-darken-1': '#018786',
|
||||
error: '#B00020',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
warning: '#FB8C00',
|
||||
},
|
||||
}
|
||||
//red: E07A5F
|
||||
// yellowish: F2CC8F
|
||||
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'myCustomLightTheme',
|
||||
themes: {
|
||||
myCustomLightTheme,
|
||||
},
|
||||
//defaultTheme: 'dark',
|
||||
},
|
||||
})
|
||||
47
src/router/index.ts
Normal file
47
src/router/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* router/index.ts
|
||||
*
|
||||
* Automatic routes for `./src/pages/*.vue`
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import {createRouter, createWebHistory} from 'vue-router/auto'
|
||||
import Search from "@/pages/Search.vue";
|
||||
import BookDetails from "@/pages/BookDetails.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [{
|
||||
path: "/",
|
||||
name: "search",
|
||||
component: Search
|
||||
},
|
||||
{
|
||||
path: "/book/:id",
|
||||
name: "book",
|
||||
component: BookDetails
|
||||
}
|
||||
|
||||
],
|
||||
})
|
||||
|
||||
// Workaround for https://github.com/vitejs/vite/issues/11804
|
||||
router.onError((err, to) => {
|
||||
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
|
||||
if (!localStorage.getItem('vuetify:dynamic-reload')) {
|
||||
console.log('Reloading page to fix dynamic import error')
|
||||
localStorage.setItem('vuetify:dynamic-reload', 'true')
|
||||
location.assign(to.fullPath)
|
||||
} else {
|
||||
console.error('Dynamic import error, reloading page did not fix it', err)
|
||||
}
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.isReady().then(() => {
|
||||
localStorage.removeItem('vuetify:dynamic-reload')
|
||||
})
|
||||
|
||||
export default router
|
||||
47
src/services/bookApi.ts
Normal file
47
src/services/bookApi.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type {Book} from "@/services/models/book";
|
||||
import axios from "axios";
|
||||
import type {BookFilters, BookSimilarParams} from "@/services/models/filter";
|
||||
|
||||
const BASE_API = import.meta.env.VITE_BOOK_API_URL;
|
||||
const api = axios.create({baseURL: BASE_API, headers: {"Content-Type": "application/json"}})
|
||||
|
||||
/*
|
||||
api.interceptors.request.use((config) => {
|
||||
config.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return config;
|
||||
})
|
||||
*/
|
||||
|
||||
export async function search(filters: BookFilters): Promise<Book[]> {
|
||||
const books = await api.post<Book[]>(`/search`, filters)
|
||||
return Promise.resolve(books.data.map(book => ({
|
||||
...book,
|
||||
collection: book.collection?.split("_").join(" "),
|
||||
editeur: book.editeur?.split("_").join(" ")
|
||||
})));
|
||||
}
|
||||
|
||||
export async function getBookById(id: string): Promise<Book> {
|
||||
const book = await api.get<Book>(`/book/${id}`)
|
||||
return Promise.resolve({
|
||||
...book.data,
|
||||
collection: book.data.collection?.split("_").join(" "),
|
||||
editeur: book.data.editeur?.split("_").join(" ")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function getBookRecommandations(id: string, params: BookSimilarParams): Promise<Book[]> {
|
||||
const books = await api.get<Book[]>(`/books/${id}/similar`, {params})
|
||||
return Promise.resolve(books.data.map(book => ({
|
||||
...book,
|
||||
collection: book.collection?.split("_").join(" "),
|
||||
editeur: book.editeur?.split("_").join(" ")
|
||||
})));
|
||||
}
|
||||
|
||||
|
||||
export async function getCollections(): Promise<string[]> {
|
||||
const collections = await api.get<string[]>(`/collections`)
|
||||
return Promise.resolve(collections.data.map(coll => coll.split("_").join(" ")));
|
||||
}
|
||||
19
src/services/models/book.ts
Normal file
19
src/services/models/book.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export type Book = {
|
||||
id: string;
|
||||
product_title?: string;
|
||||
author?: string;
|
||||
resume?: string;
|
||||
image_url?: string;
|
||||
collection?: string;
|
||||
date_de_parution?: string;
|
||||
ean?: number;
|
||||
editeur?: string;
|
||||
format?: string;
|
||||
isbn?: string;
|
||||
nb_de_pages?: number;
|
||||
presentation?: string | null;
|
||||
width?: number;
|
||||
height?: number;
|
||||
depth?: number;
|
||||
poids ?:number;
|
||||
}
|
||||
12
src/services/models/filter.ts
Normal file
12
src/services/models/filter.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export type BookFilters = {
|
||||
mot_clef: string;
|
||||
collections: string[];
|
||||
nb_de_pages: { min?: number, max?: number },
|
||||
date_de_parution: { après?: string, avant?: string }
|
||||
}
|
||||
|
||||
export type BookSimilarParams = {
|
||||
author?: boolean,
|
||||
fast?: boolean,
|
||||
collection?: boolean,
|
||||
}
|
||||
24
src/services/stores/bookStore.ts
Normal file
24
src/services/stores/bookStore.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import type {Ref} from "vue";
|
||||
import {ref} from "vue";
|
||||
import type {Book} from "@/services/models/book";
|
||||
import {getBookById} from "@/services/bookApi";
|
||||
|
||||
export const useBookStore = defineStore('book', () => {
|
||||
const book: Ref<Book | undefined> = ref(undefined)
|
||||
const isLoading = ref(false);
|
||||
|
||||
async function loadBook(id: string) {
|
||||
isLoading.value = true;
|
||||
return getBookById(id)
|
||||
.then(result => {
|
||||
console.log("get book")
|
||||
book.value = result
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
return {book, isLoading, loadBook}
|
||||
})
|
||||
50
src/services/stores/booksStore.ts
Normal file
50
src/services/stores/booksStore.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import type {Ref} from "vue";
|
||||
import {computed, ref} from "vue";
|
||||
import type {Book} from "@/services/models/book";
|
||||
import {search} from "@/services/bookApi";
|
||||
import {watchDebounced} from '@vueuse/core'
|
||||
import type {BookFilters} from "@/services/models/filter";
|
||||
|
||||
export const useBooksStore = defineStore('books', () => {
|
||||
const books: Ref<Book[]> = ref([])
|
||||
const areBooksLoading = ref(false);
|
||||
const noData = computed(() => books.value.length === 0)
|
||||
const filters: Ref<BookFilters> = ref({
|
||||
mot_clef: "", collections: [],
|
||||
nb_de_pages: {min: undefined, max: undefined},
|
||||
date_de_parution: {après: undefined, avant: undefined}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
watchDebounced(
|
||||
filters.value,
|
||||
searchBooksWithFilters,
|
||||
{debounce: 1000, maxWait: 1000},
|
||||
)
|
||||
|
||||
function searchBooks() {
|
||||
if (noData.value) {
|
||||
return searchBooksWithFilters();
|
||||
}
|
||||
}
|
||||
|
||||
async function searchBooksWithFilters() {
|
||||
areBooksLoading.value = true;
|
||||
try {
|
||||
books.value = await search({
|
||||
...filters.value,
|
||||
collections: filters.value.collections.map(c => (c as string).split(" ").join("_")),
|
||||
date_de_parution: {
|
||||
après: filters.value.date_de_parution.après,
|
||||
avant: filters.value.date_de_parution.avant,
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
areBooksLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {books, filters, areBooksLoading, noData, searchBooks}
|
||||
})
|
||||
3
src/styles/README.md
Normal file
3
src/styles/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Styles
|
||||
|
||||
This directory is for configuring the styles of the application.
|
||||
10
src/styles/settings.scss
Normal file
10
src/styles/settings.scss
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
||||
14
tsconfig.app.json
Normal file
14
tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tsconfig.node.json
Normal file
19
tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
25
typed-router.d.ts
vendored
Normal file
25
typed-router.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/BookDetails': RouteRecordInfo<'/BookDetails', '/BookDetails', Record<never, never>, Record<never, never>>,
|
||||
'/Search': RouteRecordInfo<'/Search', '/Search', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
62
vite.config.mts
Normal file
62
vite.config.mts
Normal file
@ -0,0 +1,62 @@
|
||||
// Plugins
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Vuetify, {transformAssetUrls} from 'vite-plugin-vuetify'
|
||||
import ViteFonts from 'unplugin-fonts/vite'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
|
||||
// Utilities
|
||||
import {defineConfig} from 'vite'
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
Vue({
|
||||
template: {transformAssetUrls},
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
autoImport: true,
|
||||
styles: {
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
ViteFonts({
|
||||
google: {
|
||||
families: [{
|
||||
name: 'Roboto',
|
||||
styles: 'wght@100;300;400;500;700;900',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
],
|
||||
define: {'process.env': {}},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
extensions: [
|
||||
'.js',
|
||||
'.json',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port:3000,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
api: 'modern-compiler',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user