feat: mark book as read
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
title: String,
|
||||
author: String,
|
||||
imagePath: String,
|
||||
rating: Number
|
||||
rating: Number,
|
||||
read: Boolean
|
||||
});
|
||||
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>
|
||||
<p>{{rating}}/10</p>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { postUserBook } from './api.js'
|
||||
import { postReadBook } from './api.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
@@ -8,14 +8,14 @@
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
author: String,
|
||||
id: BigInt,
|
||||
id: Number,
|
||||
imagePath: String,
|
||||
});
|
||||
const imagePathOrDefault = (props.imagePath == "" || typeof props.imagePath === 'undefined') ? "../defaultbook.png" : props.imagePath;
|
||||
const error = ref(null)
|
||||
|
||||
async function onUserBookAdd() {
|
||||
const res = await postUserBook({bookId: props.id});
|
||||
async function onUserBookRead() {
|
||||
const res = await postReadBook({bookId: props.id});
|
||||
if (res.ok) {
|
||||
router.push('/books')
|
||||
} else {
|
||||
@@ -29,15 +29,8 @@ async function onUserBookAdd() {
|
||||
<div v-if="error" class="notification is-danger">
|
||||
<p>{{error}}</p>
|
||||
</div>
|
||||
<div class="columns box container has-background-dark">
|
||||
<div class="column is-narrow">
|
||||
<button @click="onUserBookAdd" class="button">
|
||||
<span class="icon" title="Add">
|
||||
<b-icon-plus />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="media column">
|
||||
<div class="columns no-padding box container has-background-dark">
|
||||
<div class="media column no-margin">
|
||||
<div class="media-left">
|
||||
<figure class="image mb-3">
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
max-height:100px;
|
||||
max-width:100px;
|
||||
max-height:180px;
|
||||
max-width:180px;
|
||||
height:auto;
|
||||
width:auto;
|
||||
}
|
||||
@@ -69,4 +79,16 @@ img {
|
||||
transition: ease-in-out 0.02s;
|
||||
}
|
||||
|
||||
.verticalbutton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -36,8 +36,8 @@ export function postBook(book) {
|
||||
return genericPostCall('/book', book.value)
|
||||
}
|
||||
|
||||
export async function postUserBook(userbook) {
|
||||
return genericPostCall('/userbook', userbook)
|
||||
export async function postReadBook(userbook) {
|
||||
return genericPostCall('/book/read', userbook)
|
||||
}
|
||||
|
||||
export function postLogin(user) {
|
||||
|
||||
@@ -33,5 +33,10 @@
|
||||
"error": "Error when loading books: {error}",
|
||||
"loading": "Loading...",
|
||||
"noresult": "No results found."
|
||||
},
|
||||
"booklistelement": {
|
||||
"read": "Read",
|
||||
"startread": "Start reading",
|
||||
"wantread": "I want to read it"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,10 @@
|
||||
"error": "Erreur pendant le chargement des livres: {error}",
|
||||
"loading": "Chargement...",
|
||||
"noresult": "Aucun résultat trouvé."
|
||||
},
|
||||
"booklistelement": {
|
||||
"read": "Lu",
|
||||
"startread": "Commencer la lecture",
|
||||
"wantread": "Je veux le lire"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,5 @@ type UserBook struct {
|
||||
BookID uint
|
||||
Book Book
|
||||
Rating int
|
||||
Read bool
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
|
||||
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
||||
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type userBookPostCreate struct {
|
||||
type bookPostMarkAsRead struct {
|
||||
BookID uint `json:"bookId" binding:"required"`
|
||||
}
|
||||
|
||||
func PostUserBookHandler(ac appcontext.AppContext) {
|
||||
var userbook userBookPostCreate
|
||||
func PostBookReadHandler(ac appcontext.AppContext) {
|
||||
var userbook bookPostMarkAsRead
|
||||
err := ac.C.ShouldBindJSON(&userbook)
|
||||
if err != nil {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
@@ -29,18 +31,33 @@ func PostUserBookHandler(ac appcontext.AppContext) {
|
||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||
return
|
||||
}
|
||||
userbookDb := userBookWsToDb(userbook, &user)
|
||||
|
||||
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 errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
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")
|
||||
}
|
||||
|
||||
func userBookWsToDb(ub userBookPostCreate, user *model.User) model.UserBook {
|
||||
func userBookWsToDb(ub bookPostMarkAsRead, user *model.User) model.UserBook {
|
||||
return model.UserBook{
|
||||
UserID: user.ID,
|
||||
BookID: ub.BookID,
|
||||
Read: true,
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type bookUserGet struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
func GetMyBooksHanderl(ac appcontext.AppContext) {
|
||||
@@ -34,5 +35,6 @@ func userBookDbToWs(b *model.UserBook) bookUserGet {
|
||||
Title: b.Book.Title,
|
||||
Author: b.Book.Author,
|
||||
Rating: b.Rating,
|
||||
Read: b.Read,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ func Setup(config *config.Config) *gin.Engine {
|
||||
r.POST("/book", func(c *gin.Context) {
|
||||
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||
})
|
||||
r.POST("/userbook", func(c *gin.Context) {
|
||||
routes.PostUserBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||
r.POST("/book/read", func(c *gin.Context) {
|
||||
routes.PostBookReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||
})
|
||||
r.POST("/auth/signup", func(c *gin.Context) {
|
||||
routes.PostSignupHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
|
||||
|
||||
@@ -12,28 +12,28 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostUserBookHandler_Ok(t *testing.T) {
|
||||
func TestPostBookReadHandler_Ok(t *testing.T) {
|
||||
userBookJson :=
|
||||
`{
|
||||
"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 :=
|
||||
`{
|
||||
"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()
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
token := testutils.ConnectDemo2User(router)
|
||||
req, _ := http.NewRequest("POST", "/userbook",
|
||||
req, _ := http.NewRequest("POST", "/book/read",
|
||||
strings.NewReader(string(userBookJson)))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
router.ServeHTTP(w, req)
|
||||
Reference in New Issue
Block a user