feat: mark book as read
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ type UserBook struct {
|
|||||||
BookID uint
|
BookID uint
|
||||||
Book Book
|
Book Book
|
||||||
Rating int
|
Rating int
|
||||||
|
Read bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
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
|
err = ac.Db.Save(&userbookDb).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user