Add component to upload book cover images
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
PersonalLibraryManager
|
PersonalLibraryManager
|
||||||
plm.db
|
plm.db
|
||||||
plm.toml
|
plm.toml
|
||||||
|
img
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed } from 'vue'
|
||||||
import { postBook, extractFormErrorFromField } from './api.js'
|
import { postBook, extractFormErrorFromField } from './api.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import CoverUpload from './CoverUpload.vue'
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -48,6 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
|
<p v-if="authorError" class="help is-danger">{{authorError}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<CoverUpload name="cover"/>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<button class="button is-link">{{$t('addbook.submit')}}</button>
|
<button class="button is-link">{{$t('addbook.submit')}}</button>
|
||||||
|
|||||||
80
front/src/CoverUpload.vue
Normal file
80
front/src/CoverUpload.vue
Normal 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>
|
||||||
@@ -52,6 +52,23 @@ export function postSignUp(user) {
|
|||||||
return genericPostCallNoAuth('/auth/signup', user.value)
|
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) {
|
export function genericPostCallNoAuth(apiRoute, object) {
|
||||||
return fetch(baseUrl + apiRoute, {
|
return fetch(baseUrl + apiRoute, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"addbook": {
|
"addbook": {
|
||||||
"title":"Title",
|
"title":"Title",
|
||||||
"author":"Author",
|
"author":"Author",
|
||||||
"submit":"Submit"
|
"submit":"Submit",
|
||||||
|
"coverupload":"Upload cover"
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"username":"Username",
|
"username":"Username",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
"addbook": {
|
"addbook": {
|
||||||
"title":"Titre",
|
"title":"Titre",
|
||||||
"author":"Auteur",
|
"author":"Auteur",
|
||||||
"submit":"Confirmer"
|
"submit":"Confirmer",
|
||||||
|
"coverupload":"Téléverser la couverture"
|
||||||
},
|
},
|
||||||
"signup": {
|
"signup": {
|
||||||
"username":"Nom d'utilisateur",
|
"username":"Nom d'utilisateur",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package appcontext
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/config"
|
||||||
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
@@ -13,6 +14,7 @@ type AppContext struct {
|
|||||||
C *gin.Context
|
C *gin.Context
|
||||||
Db *gorm.DB
|
Db *gorm.DB
|
||||||
I18n *i18n.Bundle
|
I18n *i18n.Bundle
|
||||||
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac AppContext) GetAuthenticatedUser() (model.User, error) {
|
func (ac AppContext) GetAuthenticatedUser() (model.User, error) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Config struct {
|
|||||||
DatabaseFilePath string `toml:"database_file_path" comment:"Path to sqlite database file."`
|
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."`
|
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."`
|
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 {
|
func defaultConfig() Config {
|
||||||
@@ -21,6 +22,7 @@ func defaultConfig() Config {
|
|||||||
DatabaseFilePath: "plm.db",
|
DatabaseFilePath: "plm.db",
|
||||||
DemoDataPath: "",
|
DemoDataPath: "",
|
||||||
JWTKey: "",
|
JWTKey: "",
|
||||||
|
ImageFolderPath: "img",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ func Auth() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//do not check static files
|
||||||
|
if strings.HasPrefix(c.FullPath(), "/bookcover") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
username, err := parseUserFromJwt(c)
|
username, err := parseUserFromJwt(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
c.AbortWithStatusJSON(http.StatusUnauthorized,
|
||||||
|
|||||||
24
internal/routes/uploadbookcover.go
Normal file
24
internal/routes/uploadbookcover.go
Normal 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})
|
||||||
|
}
|
||||||
@@ -22,27 +22,31 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
r.Use(cors.New(configureCors())) // All origins allowed by default
|
r.Use(cors.New(configureCors())) // All origins allowed by default
|
||||||
r.Use(middleware.Auth())
|
r.Use(middleware.Auth())
|
||||||
|
r.Static("/bookcover", config.ImageFolderPath)
|
||||||
bundle := i18nresource.InitializeI18n()
|
bundle := i18nresource.InitializeI18n()
|
||||||
r.GET("/mybooks", func(c *gin.Context) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user