feat: mark book as read

This commit is contained in:
2025-10-22 14:34:08 +02:00
parent c4ca073b4a
commit 517765114d
10 changed files with 94 additions and 34 deletions

View File

@@ -4,7 +4,8 @@
title: String, title: String,
author: String, author: String,
imagePath: String, imagePath: String,
rating: Number rating: Number,
read: Boolean
}); });
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "defaultbook.png" : props.imagePath; const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "defaultbook.png" : props.imagePath;
@@ -24,6 +25,13 @@ const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath ===
<div class="is-size-5 is-italic">{{author}}</div> <div class="is-size-5 is-italic">{{author}}</div>
<p>{{rating}}/10</p> <p>{{rating}}/10</p>
</div> </div>
<nav v-if="read" class="level">
<div class="level-left">
<span class="level-item icon" :title="$t('booklistelement.read')">
<b-icon-check-circle />
</span>
</div>
</nav>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { postUserBook } from './api.js' import { postReadBook } from './api.js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter(); const router = useRouter();
@@ -8,14 +8,14 @@
const props = defineProps({ const props = defineProps({
title: String, title: String,
author: String, author: String,
id: BigInt, id: Number,
imagePath: String, imagePath: String,
}); });
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "../defaultbook.png" : props.imagePath; const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "../defaultbook.png" : props.imagePath;
const error = ref(null) const error = ref(null)
async function onUserBookAdd() { async function onUserBookRead() {
const res = await postUserBook({bookId: props.id}); const res = await postReadBook({bookId: props.id});
if (res.ok) { if (res.ok) {
router.push('/books') router.push('/books')
} else { } else {
@@ -29,15 +29,8 @@ async function onUserBookAdd() {
<div v-if="error" class="notification is-danger"> <div v-if="error" class="notification is-danger">
<p>{{error}}</p> <p>{{error}}</p>
</div> </div>
<div class="columns box container has-background-dark"> <div class="columns no-padding box container has-background-dark">
<div class="column is-narrow"> <div class="media column no-margin">
<button @click="onUserBookAdd" class="button">
<span class="icon" title="Add">
<b-icon-plus />
</span>
</button>
</div>
<div class="media column">
<div class="media-left"> <div class="media-left">
<figure class="image mb-3"> <figure class="image mb-3">
<img v-bind:src="imagePathOrDefault" v-bind:alt="title"> <img v-bind:src="imagePathOrDefault" v-bind:alt="title">
@@ -48,13 +41,30 @@ async function onUserBookAdd() {
<div class="is-size-5 is-italic">{{author}}</div> <div class="is-size-5 is-italic">{{author}}</div>
</div> </div>
</div> </div>
<div class="column is-narrow">
<button @click="" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.wantread')">
<b-icon-eye />
</span>
</button>
<button @click="" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.startread')">
<b-icon-book />
</span>
</button>
<button @click="onUserBookRead" class="button is-large verticalbutton">
<span class="icon" :title="$t('booklistelement.read')">
<b-icon-check-circle />
</span>
</button>
</div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
img { img {
max-height:100px; max-height:180px;
max-width:100px; max-width:180px;
height:auto; height:auto;
width:auto; width:auto;
} }
@@ -69,4 +79,16 @@ img {
transition: ease-in-out 0.02s; transition: ease-in-out 0.02s;
} }
.verticalbutton {
display: block;
}
.no-padding {
padding: 0px;
}
.no-margin {
margin: 0px;
}
</style> </style>

View File

@@ -36,8 +36,8 @@ export function postBook(book) {
return genericPostCall('/book', book.value) return genericPostCall('/book', book.value)
} }
export async function postUserBook(userbook) { export async function postReadBook(userbook) {
return genericPostCall('/userbook', userbook) return genericPostCall('/book/read', userbook)
} }
export function postLogin(user) { export function postLogin(user) {

View File

@@ -33,5 +33,10 @@
"error": "Error when loading books: {error}", "error": "Error when loading books: {error}",
"loading": "Loading...", "loading": "Loading...",
"noresult": "No results found." "noresult": "No results found."
},
"booklistelement": {
"read": "Read",
"startread": "Start reading",
"wantread": "I want to read it"
} }
} }

View File

@@ -33,5 +33,10 @@
"error": "Erreur pendant le chargement des livres: {error}", "error": "Erreur pendant le chargement des livres: {error}",
"loading": "Chargement...", "loading": "Chargement...",
"noresult": "Aucun résultat trouvé." "noresult": "Aucun résultat trouvé."
},
"booklistelement": {
"read": "Lu",
"startread": "Commencer la lecture",
"wantread": "Je veux le lire"
} }
} }

View File

@@ -9,4 +9,5 @@ type UserBook struct {
BookID uint BookID uint
Book Book Book Book
Rating int Rating int
Read bool
} }

View File

@@ -1,19 +1,21 @@
package routes package routes
import ( import (
"errors"
"net/http" "net/http"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext" "git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/model" "git.artlef.fr/PersonalLibraryManager/internal/model"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator" "git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"gorm.io/gorm"
) )
type userBookPostCreate struct { type bookPostMarkAsRead struct {
BookID uint `json:"bookId" binding:"required"` BookID uint `json:"bookId" binding:"required"`
} }
func PostUserBookHandler(ac appcontext.AppContext) { func PostBookReadHandler(ac appcontext.AppContext) {
var userbook userBookPostCreate var userbook bookPostMarkAsRead
err := ac.C.ShouldBindJSON(&userbook) err := ac.C.ShouldBindJSON(&userbook)
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
@@ -29,18 +31,33 @@ func PostUserBookHandler(ac appcontext.AppContext) {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return return
} }
userbookDb := userBookWsToDb(userbook, &user)
err = ac.Db.Save(&userbookDb).Error var userbookDb model.UserBook
res := ac.Db.Where("user_id = ? AND book_id = ?", user.ID, userbook.BookID).First(&userbookDb)
err = res.Error
if err != nil { if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err) if errors.Is(err, gorm.ErrRecordNotFound) {
return userbookDb = userBookWsToDb(userbook, &user)
err = ac.Db.Save(&userbookDb).Error
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
} else {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
} else {
userbookDb.Read = true
ac.Db.Save(&userbookDb)
} }
ac.C.String(http.StatusOK, "Success") ac.C.String(http.StatusOK, "Success")
} }
func userBookWsToDb(ub userBookPostCreate, user *model.User) model.UserBook { func userBookWsToDb(ub bookPostMarkAsRead, user *model.User) model.UserBook {
return model.UserBook{ return model.UserBook{
UserID: user.ID, UserID: user.ID,
BookID: ub.BookID, BookID: ub.BookID,
Read: true,
} }
} }

View File

@@ -12,6 +12,7 @@ type bookUserGet struct {
Title string `json:"title" binding:"required,max=300"` Title string `json:"title" binding:"required,max=300"`
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"`
Read bool `json:"read" binding:"boolean"`
} }
func GetMyBooksHanderl(ac appcontext.AppContext) { func GetMyBooksHanderl(ac appcontext.AppContext) {
@@ -34,5 +35,6 @@ func userBookDbToWs(b *model.UserBook) bookUserGet {
Title: b.Book.Title, Title: b.Book.Title,
Author: b.Book.Author, Author: b.Book.Author,
Rating: b.Rating, Rating: b.Rating,
Read: b.Read,
} }
} }

View File

@@ -32,8 +32,8 @@ func Setup(config *config.Config) *gin.Engine {
r.POST("/book", func(c *gin.Context) { r.POST("/book", func(c *gin.Context) {
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
}) })
r.POST("/userbook", func(c *gin.Context) { r.POST("/book/read", func(c *gin.Context) {
routes.PostUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) routes.PostBookReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
}) })
r.POST("/auth/signup", func(c *gin.Context) { r.POST("/auth/signup", func(c *gin.Context) {
routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle}) routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})

View File

@@ -12,28 +12,28 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestPostUserBookHandler_Ok(t *testing.T) { func TestPostBookReadHandler_Ok(t *testing.T) {
userBookJson := userBookJson :=
`{ `{
"bookId": 6 "bookId": 6
}` }`
testPostUserBookHandler(t, userBookJson, http.StatusOK) testPostBookReadHandler(t, userBookJson, http.StatusOK)
} }
func TestPostUserBookHandler_IDDoesNotExist(t *testing.T) { func TestPostBookReadHandler_IDDoesNotExist(t *testing.T) {
userBookJson := userBookJson :=
`{ `{
"bookId": 46546 "bookId": 46546
}` }`
testPostUserBookHandler(t, userBookJson, http.StatusBadRequest) testPostBookReadHandler(t, userBookJson, http.StatusBadRequest)
} }
func testPostUserBookHandler(t *testing.T, userBookJson string, expectedCode int) { func testPostBookReadHandler(t *testing.T, userBookJson string, expectedCode int) {
router := testutils.TestSetup() router := testutils.TestSetup()
w := httptest.NewRecorder() w := httptest.NewRecorder()
token := testutils.ConnectDemo2User(router) token := testutils.ConnectDemo2User(router)
req, _ := http.NewRequest("POST", "/userbook", req, _ := http.NewRequest("POST", "/book/read",
strings.NewReader(string(userBookJson))) strings.NewReader(string(userBookJson)))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)