Use the same widget for books everywhere

This commit is contained in:
2026-03-27 22:08:24 +01:00
parent 726c640657
commit e05c9f2b45
17 changed files with 70 additions and 146 deletions

View File

@@ -1,67 +0,0 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { getImagePathOrDefault } from './api.js'
import { VRating } from 'vuetify/components/VRating'
const props = defineProps({
id: Number,
title: String,
author: String,
coverPath: String,
rating: Number,
read: Boolean,
})
const imagePathOrDefault = computed(() => getImagePathOrDefault(props.coverPath))
const router = useRouter()
function openBook() {
router.push(`/book/${props.id}`)
}
</script>
<template>
<div class="box container has-background-dark">
<div class="media" @click="openBook">
<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="content">
<div class="is-size-5">{{ title }}</div>
<div class="is-size-5 is-italic">{{ author }}</div>
<VRating
v-if="rating > 0"
half-increments
readonly
:length="5"
size="medium"
:model-value="rating / 2"
active-color="bulma-body-color"
/>
</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>

View File

@@ -1,7 +1,7 @@
<script setup>
import { ref, computed } from 'vue'
import BookCard from './BookCard.vue'
import { getMyBooks } from './api.js'
import BookListElement from './BookListElement.vue'
import Pagination from './Pagination.vue'
const FilterStates = Object.freeze({
@@ -10,7 +10,7 @@ const FilterStates = Object.freeze({
READING: 'reading',
})
const limit = 6
const limit = 5
const pageNumber = ref(1)
const offset = computed(() => (pageNumber.value - 1) * limit)
@@ -76,7 +76,7 @@ function pageChange(newPageNumber) {
<div v-else-if="data">
<div class="">
<div class="" v-for="book in data.books" :key="book.id">
<BookCard v-bind="book" />
<BookListElement v-bind="book" />
</div>
</div>
<Pagination

View File

@@ -19,7 +19,7 @@ func TestFetchAllBooks(t *testing.T) {
assert.Equal(t, 15, len(result.Books))
}
func testFetchBooks(t *testing.T, limit string, offset string) dto.BookSearchGet {
func testFetchBooks(t *testing.T, limit string, offset string) dto.BookItemsGet {
router := testutils.TestSetup()
u, err := url.Parse("/ws/books")
@@ -47,7 +47,7 @@ func testFetchBooks(t *testing.T, limit string, offset string) dto.BookSearchGet
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var result dto.BookSearchGet
var result dto.BookItemsGet
s := w.Body.String()
err = json.Unmarshal([]byte(s), &result)
if err != nil {

View File

@@ -15,7 +15,7 @@ import (
func TestGetBook_Ok(t *testing.T) {
book := testGetBook(t, "3", http.StatusOK)
assert.Equal(t,
dto.BookGet{
dto.FullBookGet{
Title: "D'un château l'autre",
Author: "Louis-Ferdinand Céline",
AuthorID: 2,
@@ -29,7 +29,7 @@ func TestGetBook_Ok(t *testing.T) {
func TestGetBook_NoUserBook(t *testing.T) {
book := testGetBook(t, "18", http.StatusOK)
assert.Equal(t,
dto.BookGet{
dto.FullBookGet{
Title: "De sang-froid",
Author: "Truman Capote",
AuthorID: 14,
@@ -41,7 +41,7 @@ func TestGetBook_NoUserBook(t *testing.T) {
func TestGetBook_Description(t *testing.T) {
book := testGetBook(t, "22", http.StatusOK)
assert.Equal(t,
dto.BookGet{
dto.FullBookGet{
Title: "Le complot contre l'Amérique",
Author: "Philip Roth",
AuthorID: 17,
@@ -61,7 +61,7 @@ func TestGetBook_IdNotInt(t *testing.T) {
testGetBook(t, "wrong", http.StatusBadRequest)
}
func testGetBook(t *testing.T, id string, status int) dto.BookGet {
func testGetBook(t *testing.T, id string, status int) dto.FullBookGet {
router := testutils.TestSetup()
token := testutils.ConnectDemoUser(router)
@@ -70,7 +70,7 @@ func testGetBook(t *testing.T, id string, status int) dto.BookGet {
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var book dto.BookGet
var book dto.FullBookGet
err := json.Unmarshal(w.Body.Bytes(), &book)
if err != nil {
t.Error(err)

View File

@@ -60,14 +60,14 @@ func TestGetReadBooksHandler_CheckOneBook(t *testing.T) {
token := testutils.ConnectDemo2User(router)
result := testGetReadBooksHandler(t, router, token, 200, "100", "")
var book dto.BookUserGetBook
var book dto.BookItemGet
for _, b := range result.Books {
if b.Title == "De sang-froid" {
book = b
}
}
assert.Equal(t,
dto.BookUserGetBook{
dto.BookItemGet{
ID: 18,
Title: "De sang-froid",
Author: "Truman Capote",
@@ -77,7 +77,7 @@ func TestGetReadBooksHandler_CheckOneBook(t *testing.T) {
}, book)
}
func testGetReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookUserGet {
func testGetReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookItemsGet {
u, err := url.Parse("/ws/mybooks/read")
if err != nil {
t.Error(err)

View File

@@ -28,7 +28,7 @@ func TestGetReadingBooksHandler_Demo2(t *testing.T) {
assert.Equal(t, int64(0), result.Count)
}
func testGetReadingBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookUserGet {
func testGetReadingBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, limit string, offset string) dto.BookItemsGet {
u, err := url.Parse("/ws/mybooks/reading")
if err != nil {
t.Error(err)

View File

@@ -13,14 +13,14 @@ import (
"github.com/stretchr/testify/assert"
)
func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, url string) dto.BookUserGet {
func testGetbooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int, url string) dto.BookItemsGet {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", userToken))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var parsedResponse dto.BookUserGet
var parsedResponse dto.BookItemsGet
err := json.Unmarshal(w.Body.Bytes(), &parsedResponse)
if err != nil {
t.Error(err)

View File

@@ -27,6 +27,6 @@ func TestGetWantReadBooksHandler_Demo2(t *testing.T) {
assert.Equal(t, int64(0), result.Count)
}
func testGetWantReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) dto.BookUserGet {
func testGetWantReadBooksHandler(t *testing.T, router *gin.Engine, userToken string, expectedCode int) dto.BookItemsGet {
return testGetbooksHandler(t, router, userToken, expectedCode, "/ws/mybooks/wantread")
}

View File

@@ -25,7 +25,7 @@ func TestSearchBook_OneBookNotUserBook(t *testing.T) {
result := testSearchBook(t, "iliade", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]dto.BookSearchGetBook{{
[]dto.BookItemGet{{
Title: "Iliade",
Author: "Homère",
ID: 29,
@@ -41,7 +41,7 @@ func TestSearchBook_OneBookRead(t *testing.T) {
result := testSearchBook(t, "dieux", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]dto.BookSearchGetBook{{
[]dto.BookItemGet{{
Title: "Les dieux ont soif",
Author: "Anatole France",
ID: 4,
@@ -58,7 +58,7 @@ func TestSearchBook_OneBookStartRead(t *testing.T) {
result := testSearchBook(t, "Recherches", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]dto.BookSearchGetBook{{
[]dto.BookItemGet{{
Title: "Recherches philosophiques",
Author: "Ludwig Wittgenstein",
ID: 30,
@@ -75,7 +75,7 @@ func TestSearchBook_ISBN(t *testing.T) {
result := testSearchBook(t, "9782070337903", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]dto.BookSearchGetBook{{
[]dto.BookItemGet{{
Title: "Le complot contre l'Amérique",
Author: "Philip Roth",
ID: 22,
@@ -91,7 +91,7 @@ func TestSearchBook_ISBNInventaire(t *testing.T) {
result := testSearchBook(t, "9782253158400", "", "")
assert.Equal(t, int64(1), result.Count)
assert.Equal(t,
[]dto.BookSearchGetBook{{
[]dto.BookItemGet{{
ID: 0,
Title: "Les premières enquêtes de Maigret",
Author: "Georges Simenon",
@@ -117,7 +117,7 @@ func TestSearchBook_Offset(t *testing.T) {
assert.Equal(t, 3, len(result.Books))
}
func testSearchBook(t *testing.T, searchterm string, limit string, offset string) dto.BookSearchGet {
func testSearchBook(t *testing.T, searchterm string, limit string, offset string) dto.BookItemsGet {
router := testutils.TestSetup()
u, err := url.Parse("/ws/search/" + searchterm)
@@ -145,7 +145,7 @@ func testSearchBook(t *testing.T, searchterm string, limit string, offset string
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var result dto.BookSearchGet
var result dto.BookItemsGet
s := w.Body.String()
err = json.Unmarshal([]byte(s), &result)
if err != nil {

View File

@@ -6,7 +6,7 @@ type AppInfo struct {
DemoUsername string `json:"demoUsername"`
}
type BookGet struct {
type FullBookGet struct {
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
AuthorID uint `json:"authorId"`
@@ -23,28 +23,13 @@ type BookGet struct {
CoverPath string `json:"coverPath"`
}
type BookUserGet struct {
Count int64 `json:"count"`
Books []BookUserGetBook `json:"books"`
}
type BookUserGetBook struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`
Rating int `json:"rating" binding:"min=0,max=10"`
Read bool `json:"read" binding:"boolean"`
WantRead bool `json:"wantread" binding:"boolean"`
CoverPath string `json:"coverPath"`
}
type BookSearchGet struct {
type BookItemsGet struct {
Count int64 `json:"count"`
Inventaire bool `json:"inventaire"`
Books []BookSearchGetBook `json:"books"`
Books []BookItemGet `json:"books"`
}
type BookSearchGetBook struct {
type BookItemGet struct {
ID uint `json:"id"`
Title string `json:"title" binding:"required,max=300"`
Author string `json:"author" binding:"max=100"`

View File

@@ -10,8 +10,8 @@ import (
"gorm.io/gorm"
)
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.BookGet, error) {
var book dto.BookGet
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.FullBookGet, error) {
var book dto.FullBookGet
query := db.Model(&model.Book{})
selectQueryString := "books.title, authors.name as author, authors.id as author_id, books.isbn, books.inventaire_id, books.open_library_id, books.summary, " +
"user_books.review, user_books.rating, user_books.read, user_books.want_read, " +
@@ -27,8 +27,8 @@ func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (dto.BookGet, error)
return book, res.Error
}
func FetchReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
var books []dto.BookUserGetBook
func FetchReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchReadUserBookQuery(db, userId)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -43,8 +43,8 @@ func FetchReadUserBookCount(db *gorm.DB, userId uint) (int64, error) {
return count, res.Error
}
func FetchReadingUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
var books []dto.BookUserGetBook
func FetchReadingUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchReadingUserBookQuery(db, userId)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -70,8 +70,8 @@ func fetchReadingUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
return query
}
func FetchWantReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookUserGetBook, error) {
var books []dto.BookUserGetBook
func FetchWantReadUserBook(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchWantReadUserBookQuery(db, userId)
query = query.Limit(limit)
@@ -93,9 +93,10 @@ func fetchWantReadUserBookQuery(db *gorm.DB, userId uint) *gorm.DB {
return query
}
// fetch only books where userbook exists
func fetchUserBookGet(db *gorm.DB, userId uint) *gorm.DB {
query := db.Model(&model.UserBook{})
query = query.Select("books.id, books.title, authors.name as author, user_books.rating, user_books.read, user_books.want_read, " + selectStaticFilesPath())
query = query.Select(selectBookItem())
query = query.Joins("left join books on (books.id = user_books.book_id)")
query = joinAuthors(query)
query = joinStaticFiles(query)
@@ -103,8 +104,8 @@ func fetchUserBookGet(db *gorm.DB, userId uint) *gorm.DB {
return query
}
func FetchAllBooks(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookSearchGetBook, error) {
var books []dto.BookSearchGetBook
func FetchAllBooks(db *gorm.DB, userId uint, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchBookQueryBuilder(db, userId)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -120,8 +121,8 @@ func FetchAllBooksCount(db *gorm.DB, userId uint) (int64, error) {
return count, res.Error
}
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]dto.BookSearchGetBook, error) {
var books []dto.BookSearchGetBook
func FetchBookSearchByAuthorGet(db *gorm.DB, userId uint, authorId uint64, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchBookSearchByAuthorQuery(db, userId, authorId)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -141,8 +142,8 @@ func fetchBookSearchByAuthorQuery(db *gorm.DB, userId uint, authorId uint64) *go
return query.Where("authors.id = ?", authorId)
}
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]dto.BookSearchGetBook, error) {
var books []dto.BookSearchGetBook
func FetchBookSearchGet(db *gorm.DB, userId uint, searchterm string, limit int, offset int) ([]dto.BookItemGet, error) {
var books []dto.BookItemGet
query := fetchBookSearchQuery(db, userId, searchterm)
query = query.Limit(limit)
query = query.Offset(offset)
@@ -169,15 +170,20 @@ func fetchBookSearchQuery(db *gorm.DB, userId uint, searchterm string) *gorm.DB
return query
}
// fetch all books even whithout user books
func fetchBookQueryBuilder(db *gorm.DB, userId uint) *gorm.DB {
query := db.Model(&model.Book{})
query = query.Select("books.id, books.title, authors.name as author, books.small_description as description, books.inventaire_id, user_books.rating, user_books.read, DATE(user_books.start_read_date) as start_read_date, user_books.want_read, " + selectStaticFilesPath())
query = query.Select(selectBookItem())
query = joinAuthors(query)
query = query.Joins("left join user_books on (user_books.book_id = books.id and user_books.user_id = ?)", userId)
query = joinStaticFiles(query)
return query
}
func selectBookItem() string {
return "books.id, books.title, authors.name as author, books.small_description as description, books.inventaire_id, user_books.rating, user_books.read, DATE(user_books.start_read_date) as start_read_date, user_books.want_read, " + selectStaticFilesPath()
}
func selectStaticFilesPath() string {
return "(CASE COALESCE(static_files.path, '') WHEN '' THEN '' ELSE concat('" + fileutils.GetWsLinkPrefix() + "', static_files.path) END) as CoverPath"
}

View File

@@ -50,5 +50,5 @@ func GetAuthorBooksHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.BookSearchGet{Books: books, Count: count})
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Books: books, Count: count})
}

View File

@@ -37,7 +37,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
var returnedBooks dto.BookSearchGet
var returnedBooks dto.BookItemsGet
if !params.Inventaire {
books, err := query.FetchBookSearchGet(ac.Db, user.ID, searchterm, limit, offset)
if err != nil {
@@ -49,7 +49,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
returnedBooks = dto.BookSearchGet{Count: count, Inventaire: false, Books: books}
returnedBooks = dto.BookItemsGet{Count: count, Inventaire: false, Books: books}
}
if params.Inventaire || len(returnedBooks.Books) == 0 {
returnedBooksPtr, err := searchInInventaireAPI(ac.Config.InventaireUrl, searchterm, limit, offset, params)
@@ -62,7 +62,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
ac.C.JSON(http.StatusOK, returnedBooks)
}
func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, offset int, params dto.BookSearchGetParam) (*dto.BookSearchGet, error) {
func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, offset int, params dto.BookSearchGetParam) (*dto.BookItemsGet, error) {
isIsbn, err := regexp.Match(`\d{10,13}`, []byte(searchterm))
if err != nil {
@@ -74,11 +74,11 @@ func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, o
if err != nil {
return nil, err
}
var bookSearchGet dto.BookSearchGet
var bookSearchGet dto.BookItemsGet
if queryResult != nil {
bookSearchGet = inventaireEditionToBookSearchGet(*queryResult)
} else {
bookSearchGet = dto.BookSearchGet{Count: 0, Inventaire: true}
bookSearchGet = dto.BookItemsGet{Count: 0, Inventaire: true}
}
return &bookSearchGet, err
} else {
@@ -91,9 +91,9 @@ func searchInInventaireAPI(inventaireUrl string, searchterm string, limit int, o
}
}
func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetailedSingleResult) dto.BookSearchGet {
var books []dto.BookSearchGetBook
bookSearchGetBook := dto.BookSearchGetBook{
func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetailedSingleResult) dto.BookItemsGet {
var books []dto.BookItemGet
bookSearchGetBook := dto.BookItemGet{
ID: 0,
Title: result.Title,
Author: result.Author.Name,
@@ -106,17 +106,17 @@ func inventaireEditionToBookSearchGet(result inventaire.InventaireEditionDetaile
CoverPath: result.Image,
}
books = append(books, bookSearchGetBook)
return dto.BookSearchGet{Count: 1, Inventaire: true, Books: books}
return dto.BookItemsGet{Count: 1, Inventaire: true, Books: books}
}
func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.InventaireSearchResult) dto.BookSearchGet {
var books []dto.BookSearchGetBook
func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.InventaireSearchResult) dto.BookItemsGet {
var books []dto.BookItemGet
for _, b := range results.Results {
coverPath := ""
if b.Image != "" && strings.HasPrefix(b.Image, "/") {
coverPath = inventaireUrl + b.Image
}
bookSearchGetBook := dto.BookSearchGetBook{
bookSearchGetBook := dto.BookItemGet{
ID: 0,
Title: b.Label,
Author: "",
@@ -129,5 +129,5 @@ func inventaireBooksToBookSearchGet(inventaireUrl string, results inventaire.Inv
}
books = append(books, bookSearchGetBook)
}
return dto.BookSearchGet{Count: results.Total, Inventaire: true, Books: books}
return dto.BookItemsGet{Count: results.Total, Inventaire: true, Books: books}
}

View File

@@ -36,5 +36,5 @@ func GetBooksHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.BookSearchGet{Count: count, Inventaire: false, Books: books})
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Inventaire: false, Books: books})
}

View File

@@ -35,5 +35,5 @@ func GetMyBooksReadHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
}

View File

@@ -35,5 +35,5 @@ func GetMyBooksReadingHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
}

View File

@@ -35,5 +35,5 @@ func GetMyBooksWantReadHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, dto.BookUserGet{Count: count, Books: userbooks})
ac.C.JSON(http.StatusOK, dto.BookItemsGet{Count: count, Books: userbooks})
}