Search existing books
This commit is contained in:
7
front/package-lock.json
generated
7
front/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "personal-library-manager",
|
"name": "personal-library-manager",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap-icons-vue": "^1.11.3",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.12",
|
||||||
@@ -1961,6 +1962,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/bootstrap-icons-vue": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap-icons-vue": "^1.11.3",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-i18n": "^11.1.12",
|
"vue-i18n": "^11.1.12",
|
||||||
|
|||||||
47
front/src/BookListElement.vue
Normal file
47
front/src/BookListElement.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: String,
|
||||||
|
author: String,
|
||||||
|
imagePath: String,
|
||||||
|
});
|
||||||
|
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "../defaultbook.png" : props.imagePath;
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="box container has-background-dark">
|
||||||
|
<div class="media">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image mb-3">
|
||||||
|
<img v-bind:src="imagePathOrDefault" v-bind:alt="title">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="is-size-4">{{title}}</div>
|
||||||
|
<div class="is-size-5 is-italic">{{author}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
max-height:100px;
|
||||||
|
max-width:100px;
|
||||||
|
height:auto;
|
||||||
|
width:auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
transition:ease-in-out 0.04s;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:hover {
|
||||||
|
transform: scale(1.01);
|
||||||
|
transition: ease-in-out 0.02s;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input v-model="searchterm" class="input" type="text" />
|
<input v-model="searchterm" @keyup.enter="onSearchClick()" class="input" type="text" />
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button @click="onSearchClick()" class="button">
|
<button @click="onSearchClick()" class="button">
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import BookListElement from './BookListElement.vue';
|
||||||
|
import { getSearchBooks } from './api.js'
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
searchterm: String
|
searchterm: String
|
||||||
|
});
|
||||||
|
|
||||||
|
let { data, error } = getSearchBooks(props.searchterm);
|
||||||
|
|
||||||
|
onBeforeRouteUpdate(async (to, from) => {
|
||||||
|
let res = getSearchBooks(to.params.searchterm);
|
||||||
|
data = res.data;
|
||||||
|
error = res.error;
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div> You are searching for {{ searchterm }} </div>
|
<div class="booksearch">
|
||||||
|
<div v-if="error">{{$t('searchbook.error', {error: error.message})}}</div>
|
||||||
|
<div class="booksearchlist" v-else-if="data && data.length > 0" v-for="book in data" :key="book.id">
|
||||||
|
<BookListElement v-bind="book" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="data === null">{{$t('searchbook.loading')}}</div>
|
||||||
|
<div v-else>{{$t('searchbook.noresult')}}</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export function getMyBooks() {
|
|||||||
return useFetch(baseUrl + '/mybooks');
|
return useFetch(baseUrl + '/mybooks');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSearchBooks(searchterm) {
|
||||||
|
return useFetch(baseUrl + '/search/' + searchterm);
|
||||||
|
}
|
||||||
|
|
||||||
export function postBook(book) {
|
export function postBook(book) {
|
||||||
return genericPostCall('/book', book.value)
|
return genericPostCall('/book', book.value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,10 @@
|
|||||||
"bookbrowser": {
|
"bookbrowser": {
|
||||||
"error": "Error when loading books: {error}",
|
"error": "Error when loading books: {error}",
|
||||||
"loading": "Loading..."
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
|
"searchbook": {
|
||||||
|
"error": "Error when loading books: {error}",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"noresult": "No results found."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,10 @@
|
|||||||
"bookbrowser": {
|
"bookbrowser": {
|
||||||
"error": "Erreur pendant le chargement des livres: {error}",
|
"error": "Erreur pendant le chargement des livres: {error}",
|
||||||
"loading": "Chargement..."
|
"loading": "Chargement..."
|
||||||
|
},
|
||||||
|
"searchbook": {
|
||||||
|
"error": "Erreur pendant le chargement des livres: {error}",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"noresult": "Aucun résultat trouvé."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,8 @@ type bookUserGet struct {
|
|||||||
Author string `json:"author" binding:"max=100"`
|
Author string `json:"author" binding:"max=100"`
|
||||||
Rating int `json:"rating" binding:"min=0,max=10"`
|
Rating int `json:"rating" binding:"min=0,max=10"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bookSearchGet struct {
|
||||||
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
|
Author string `json:"author" binding:"max=100"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,3 +33,10 @@ func (u userSignup) toUser() (model.User, error) {
|
|||||||
user.Password = string(hashedPassword)
|
user.Password = string(hashedPassword)
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fromBookDb(b *model.Book) bookSearchGet {
|
||||||
|
return bookSearchGet{
|
||||||
|
Title: b.Title,
|
||||||
|
Author: b.Author,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
|
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
|
||||||
"git.artlef.fr/PersonalLibraryManager/internal/i18nresource"
|
"git.artlef.fr/PersonalLibraryManager/internal/i18nresource"
|
||||||
@@ -23,13 +24,24 @@ func GetMyBooksHanderl(ac appcontext.AppContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ac.Db.Preload("Book").Where("user_id = ?", user.ID).Find(&userbooks)
|
ac.Db.Preload("Book").Where("user_id = ?", user.ID).Find(&userbooks)
|
||||||
var booksDto []bookUserGet
|
booksDto := make([]bookUserGet, 0)
|
||||||
for _, userbook := range userbooks {
|
for _, userbook := range userbooks {
|
||||||
booksDto = append(booksDto, fromUserBookDb(&userbook))
|
booksDto = append(booksDto, fromUserBookDb(&userbook))
|
||||||
}
|
}
|
||||||
ac.C.JSON(http.StatusOK, booksDto)
|
ac.C.JSON(http.StatusOK, booksDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSearchBooksHandler(ac appcontext.AppContext) {
|
||||||
|
searchterm := ac.C.Param("searchterm")
|
||||||
|
var booksDb []model.Book
|
||||||
|
ac.Db.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(searchterm)+"%").Find(&booksDb)
|
||||||
|
books := make([]bookSearchGet, 0)
|
||||||
|
for _, b := range booksDb {
|
||||||
|
books = append(books, fromBookDb(&b))
|
||||||
|
}
|
||||||
|
ac.C.JSON(http.StatusOK, books)
|
||||||
|
}
|
||||||
|
|
||||||
func PostBookHandler(ac appcontext.AppContext) {
|
func PostBookHandler(ac appcontext.AppContext) {
|
||||||
var book bookPostCreate
|
var book bookPostCreate
|
||||||
err := ac.C.ShouldBindJSON(&book)
|
err := ac.C.ShouldBindJSON(&book)
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
r.GET("/mybooks", func(c *gin.Context) {
|
r.GET("/mybooks", func(c *gin.Context) {
|
||||||
api.GetMyBooksHanderl(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
api.GetMyBooksHanderl(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||||
})
|
})
|
||||||
|
r.GET("/search/:searchterm", func(c *gin.Context) {
|
||||||
|
api.GetSearchBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||||
|
})
|
||||||
r.POST("/book", func(c *gin.Context) {
|
r.POST("/book", func(c *gin.Context) {
|
||||||
api.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
api.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||||
})
|
})
|
||||||
|
|||||||
37
searchbook_test.go
Normal file
37
searchbook_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bookSearchGet struct {
|
||||||
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
|
Author string `json:"author" binding:"max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchBook(t *testing.T) {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
req, _ := http.NewRequest("GET", "/search/san", nil)
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
var books []bookSearchGet
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &books)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(books))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user