Add component to upload book cover images

This commit is contained in:
2025-10-27 22:51:28 +01:00
parent d407b41c9f
commit 8b8eee8210
11 changed files with 152 additions and 12 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
PersonalLibraryManager
plm.db
plm.toml
img

View File

@@ -2,6 +2,7 @@
import { ref, reactive, computed } from 'vue'
import { postBook, extractFormErrorFromField } from './api.js'
import { useRouter } from 'vue-router'
import CoverUpload from './CoverUpload.vue'
const router = useRouter();
@@ -28,6 +29,7 @@
}
})
}
</script>
<template>
@@ -48,6 +50,7 @@
</div>
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
</div>
<CoverUpload name="cover"/>
<div class="field">
<div class="control">
<button class="button is-link">{{$t('addbook.submit')}}</button>

80
front/src/CoverUpload.vue Normal file
View File

@@ -0,0 +1,80 @@
<script setup>
import { ref, computed } from 'vue'
import { postImage } from './api.js'
const props = defineProps({
name: String
});
const imagePath = ref(null);
const error = ref(null);
function onFileChanged(e) {
postImage(e.target.files[0])
.then((res) => res.json())
.then((json) => (imagePath.value = json["filepath"]))
.catch((err) => (error.value = err["error"]));
}
function unsetImage() {
imagePath.value = null;
}
const imageSrc = computed(() => {
return "http://localhost:8080" + imagePath.value
})
</script>
<template>
<div v-if="imagePath">
<div class="relative">
<figure class="image mb-3">
<img v-bind:src="imageSrc" v-bind:alt="props.name">
</figure>
<span class="icon is-large" @click="unsetImage">
<b-icon-x-circle-fill/>
</span>
</div>
</div>
<div v-else class="file">
<label class="file-label">
<input class="file-input" @change="onFileChanged" type="file" :name="props.name" accept="image/*"/>
<span class="file-cta">
<span class="file-icon">
<b-icon-upload />
</span>
<span class="file-label">{{$t('addbook.coverupload')}}</span>
</span>
</label>
</div>
</template>
<style scoped>
img {
max-height:500px;
max-width:500px;
height:auto;
width:auto;
}
.relative {
position: relative;
}
.relative img {
display: block;
}
.relative .icon {
position: absolute;
bottom:5px;
left:5px;
background-color: rgba(0,0,0,0.7);
font-size: 24px;
border-radius: 5px;
}
.relative .icon:hover {
background-color: rgba(0,0,0,0.8);
cursor: pointer;
}
</style>

View File

@@ -52,6 +52,23 @@ export function postSignUp(user) {
return genericPostCallNoAuth('/auth/signup', user.value)
}
export function postImage(file) {
const { user } = useAuthStore();
const formData = new FormData();
formData.append('file', file);
if (user != null) {
return fetch(baseUrl + "/upload/cover", {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + user.token
},
body: formData
})
} else {
return Promise.resolve();
}
}
export function genericPostCallNoAuth(apiRoute, object) {
return fetch(baseUrl + apiRoute, {
method: 'POST',

View File

@@ -13,7 +13,8 @@
"addbook": {
"title":"Title",
"author":"Author",
"submit":"Submit"
"submit":"Submit",
"coverupload":"Upload cover"
},
"signup": {
"username":"Username",

View File

@@ -13,7 +13,8 @@
"addbook": {
"title":"Titre",
"author":"Auteur",
"submit":"Confirmer"
"submit":"Confirmer",
"coverupload":"Téléverser la couverture"
},
"signup": {
"username":"Nom d'utilisateur",

View File

@@ -3,6 +3,7 @@ package appcontext
import (
"errors"
"git.artlef.fr/PersonalLibraryManager/internal/config"
"git.artlef.fr/PersonalLibraryManager/internal/model"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
@@ -13,6 +14,7 @@ type AppContext struct {
C *gin.Context
Db *gorm.DB
I18n *i18n.Bundle
Config *config.Config
}
func (ac AppContext) GetAuthenticatedUser() (model.User, error) {

View File

@@ -13,6 +13,7 @@ type Config struct {
DatabaseFilePath string `toml:"database_file_path" comment:"Path to sqlite database file."`
DemoDataPath string `toml:"demo_data_path" comment:"The path to the sql file to load for demo data."`
JWTKey string `toml:"jwt_key" comment:"The key used to encrypt the generated JWT. Encoded in base64. If empty a random one will be generated on every restart."`
ImageFolderPath string `toml:"image_folder_path" comment:"Folder where uploaded files will be stored."`
}
func defaultConfig() Config {
@@ -21,6 +22,7 @@ func defaultConfig() Config {
DatabaseFilePath: "plm.db",
DemoDataPath: "",
JWTKey: "",
ImageFolderPath: "img",
}
}

View File

@@ -17,6 +17,11 @@ func Auth() gin.HandlerFunc {
return
}
//do not check static files
if strings.HasPrefix(c.FullPath(), "/bookcover") {
return
}
username, err := parseUserFromJwt(c)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized,

View File

@@ -0,0 +1,24 @@
package routes
import (
"net/http"
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
"github.com/gin-gonic/gin"
)
func PostUploadBookCoverHandler(ac appcontext.AppContext) {
file, err := ac.C.FormFile("file")
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
filepath := file.Filename
err = ac.C.SaveUploadedFile(file, ac.Config.ImageFolderPath+"/"+filepath)
if err != nil {
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
return
}
ac.C.JSON(http.StatusOK, gin.H{"filepath": "/bookcover/" + filepath})
}

View File

@@ -22,27 +22,31 @@ func Setup(config *config.Config) *gin.Engine {
r := gin.Default()
r.Use(cors.New(configureCors())) // All origins allowed by default
r.Use(middleware.Auth())
r.Static("/bookcover", config.ImageFolderPath)
bundle := i18nresource.InitializeI18n()
r.GET("/mybooks", func(c *gin.Context) {
routes.GetMyBooksHanderl(appcontext.AppContext{C: c, Db: db, I18n: bundle})
routes.GetMyBooksHanderl(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.GET("/search/:searchterm", func(c *gin.Context) {
routes.GetSearchBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
routes.GetSearchBooksHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.GET("/book/:id", func(c *gin.Context) {
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
routes.GetBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
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, Config: config})
})
r.POST("/book/read", func(c *gin.Context) {
routes.PostBookReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
routes.PostBookReadHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
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, Config: config})
})
r.POST("/auth/login", func(c *gin.Context) {
routes.PostLoginHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle})
routes.PostLoginHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
r.POST("/upload/cover", func(c *gin.Context) {
routes.PostUploadBookCoverHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
})
return r
}