Browse Source

Add file upload and delete capabilities to admin post form

feature/add-admin-posts-frontend
Tovi Jaeschke-Rogers 2 years ago
parent
commit
4c4bfeb340
10 changed files with 211 additions and 22 deletions
  1. +5
    -4
      Api/PostImages.go
  2. +1
    -1
      Api/Routes.go
  3. +6
    -1
      Frontend/Routes.go
  4. +20
    -0
      Frontend/vue/package-lock.json
  5. +1
    -0
      Frontend/vue/package.json
  6. +22
    -0
      Frontend/vue/src/assets/css/admin.css
  7. +138
    -5
      Frontend/vue/src/components/admin/views/posts/AdminPostsForm.vue
  8. +5
    -1
      Frontend/vue/src/main.js
  9. +5
    -4
      Models/Posts.go
  10. +8
    -6
      Util/Files.go

+ 5
- 4
Api/PostImages.go View File

@ -74,10 +74,11 @@ func createPostImage(w http.ResponseWriter, r *http.Request) {
}
postImage = Models.PostImage{
PostID: postUUID,
Filepath: fileObject.Filepath,
Mimetype: fileObject.Mimetype,
Size: fileObject.Size,
PostID: postUUID,
Filepath: fileObject.Filepath,
PublicFilepath: fileObject.PublicFilepath,
Mimetype: fileObject.Mimetype,
Size: fileObject.Size,
}
err = Database.CreatePostImage(&postImage)


+ 1
- 1
Api/Routes.go View File

@ -46,5 +46,5 @@ func InitApiEndpoints(router *mux.Router) {
api.HandleFunc("/admin/logout", Auth.Logout).Methods("GET")
api.HandleFunc("/admin/me", Auth.Me).Methods("GET")
//router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads"))))
// router.PathPrefix("/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("./uploads"))))
}

+ 6
- 1
Frontend/Routes.go View File

@ -46,6 +46,11 @@ func InitFrontendRoutes(router *mux.Router) {
HandlerFunc(indexHandler(indexPath))
}
router.PathPrefix("/").Handler(frontendFS)
router.PathPrefix("/public/").
Handler(http.StripPrefix(
"/public/",
http.FileServer(http.Dir("./Frontend/public/")),
))
router.PathPrefix("/").Handler(frontendFS)
}

+ 20
- 0
Frontend/vue/package-lock.json View File

@ -20,6 +20,7 @@
"@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"core-js": "^3.8.3",
"vee-validate": "^4.5.10",
"vue": "^3.2.13",
@ -4131,6 +4132,17 @@
"@popperjs/core": "^2.10.2"
}
},
"node_modules/bootstrap-vue-3": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/bootstrap-vue-3/-/bootstrap-vue-3-0.1.10.tgz",
"integrity": "sha512-r5zd5DIzclFpR16s6nwFRkZlrLoTANbZ9OWFFGoKLGcHOnL+WFuR8HULUB5QEyKdH5mf9ltTBCEsX/mFRq2S1w==",
"dependencies": {
"core-js": "3.x.x"
},
"peerDependencies": {
"bootstrap": "5.x.x"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -15455,6 +15467,14 @@
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"requires": {}
},
"bootstrap-vue-3": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/bootstrap-vue-3/-/bootstrap-vue-3-0.1.10.tgz",
"integrity": "sha512-r5zd5DIzclFpR16s6nwFRkZlrLoTANbZ9OWFFGoKLGcHOnL+WFuR8HULUB5QEyKdH5mf9ltTBCEsX/mFRq2S1w==",
"requires": {
"core-js": "3.x.x"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",


+ 1
- 0
Frontend/vue/package.json View File

@ -21,6 +21,7 @@
"@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1",
"bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"core-js": "^3.8.3",
"vee-validate": "^4.5.10",
"vue": "^3.2.13",


+ 22
- 0
Frontend/vue/src/assets/css/admin.css View File

@ -133,3 +133,25 @@ label[role=alert] {
.ProseMirror p:last-child {
margin-bottom: 0;
}
.image-button-overlay {
position: relative;
top: 3rem;
z-index: 3;
width: 100%;
text-align: right;
padding-right: 1rem;
}
.image-delete {
color: var(--bs-danger);
z-index: 3;
font-size: 1.6rem;
height: 2rem;
width: 2rem;
border-radius: 50%;
}
.image-delete:hover {
background-color: white;
}

+ 138
- 5
Frontend/vue/src/components/admin/views/posts/AdminPostsForm.vue View File

@ -14,18 +14,26 @@
>
Post Details
</button>
<button
type="button"
class="btn btn-rounded"
:class="tab === 'images' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'images'"
>
Images
</button>
</div>
</div>
</div>
</div>
<div class="card shadow-2-strong card-registration">
<div class="card-body p-4 p-md-5" v-if="tab === 'details'">
<div class="card shadow-2-strong card-registration" v-if="tab === 'details'">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update Post</h3>
<Form @submit="updatePost" v-slot="{ errors }">
<div class="row">
<div class="col-md-8 mb-4">
<div class="col-md-8 mb-4">
<div class="form-outline">
<Field
v-model="post.title"
@ -40,7 +48,7 @@
</div>
</div>
<div class="col-md-4 mb-4">
<div class="col-md-3 mb-4">
<div class="form-outline">
<select
v-model="post.front_page"
@ -50,9 +58,22 @@
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
<label v-if="!errors['Front Page']" class="form-label" for="front_page">Front Page</label>
<label class="form-label" for="front_page">Front Page</label>
</div>
</div>
<div class="col-md-1 mb-4" v-if="post.front_page">
<div class="form-outline">
<Field
v-model="post.order"
type="text"
id="order"
name="Order"
class="form-control form-control-lg"/>
<label class="form-label" for="order">Order</label>
</div>
</div>
</div>
<div class="row">
@ -117,6 +138,64 @@
</Form>
</div>
</div>
<div class="card shadow-2-strong card-registration" v-if="tab === 'images'">
<div class="card-body p-4 p-md-5">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Upload Images</h3>
<form @submit.prevent="onUpload">
<div class="row">
<div class="mb-3">
<input
id="image-input"
class="form-control"
type="file"
accept="image/*"
multiple="multiple"
/>
</div>
<br/>
<div class="form-group mb-3 right-align">
<button
type="button"
class="btn btn-sm btn-outline-danger"
@click="clearFiles"
>
Clear
</button>
<button class="btn btn-sm btn-outline-success">Upload</button>
</div>
</div>
<div class="row row-cols-1 row-cols-md-3" v-if="post.images.length">
<div v-for="image in post.images" :key="image.id">
<div class="image-button-overlay">
<div @click="deleteImage(image.id)">
<span
class="fa-solid fa-xmark image-delete"
></span>
</div>
</div>
<div class="col">
<div class="card">
<img :src="image.filepath" class="card-img-top">
</div>
</div>
</div>
<div class="text-center" v-if="!post.images.length">
<p class="text-muted">Empty</p>
</div>
</div>
</form>
</div>
</div>
</section>
</div>
</template>
@ -131,6 +210,8 @@ export default {
return {
tab: 'details',
post: {},
images: [],
imageLabel: 'Choose File',
}
},
@ -203,6 +284,58 @@ export default {
} catch (error) {
this.$toast.error('An error occured');
}
},
async onUpload(event) {
let fd = new FormData()
let photos = event.target[0].files
if (photos.length === 0) {
alert('No Files')
return
}
for (let i = 0; i < photos.length; i++) {
fd.append('files', photos[i])
}
let response = await this.axios.post(
`/admin/post/${this.$route.params.id}/image`,
fd,
{
headers: {
'Content-Type': 'multipart/form-data',
}
}
)
if (response.status === 200) {
this.post = response.data
this.clearFiles()
}
},
clearFiles () {
document.getElementById("image-input").value=null;
},
async deleteImage(id) {
let response = await this.axios.delete(
`/admin/post/${this.$route.params.id}/image/${id}`,
)
if (response.status === 200) {
this.clearFiles()
const indexOfObject = this.post.images.findIndex(object => {
return object.id === id;
});
this.post.images.splice(indexOfObject, 1);
}
}
}
}


+ 5
- 1
Frontend/vue/src/main.js View File

@ -6,6 +6,7 @@ import { defineRule } from 'vee-validate';
import AllRules from '@vee-validate/rules';
import Toaster from "@meforma/vue-toaster";
import Datepicker from '@vuepic/vue-datepicker';
import BootstrapVue from 'bootstrap-vue-3'
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@ -25,7 +26,9 @@ import admin from './store/admin/index.js'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.js'
// Import the CSS or use your own!
import "bootstrap-vue-3/dist/bootstrap-vue-3.css"
import "bootstrap-vue-3/dist/bootstrap-vue-3.es.js"
import "bootstrap-vue-3/dist/bootstrap-vue-3.umd.js"
import '@vuepic/vue-datepicker/dist/main.css'
import './assets/css/admin.css'
@ -38,6 +41,7 @@ app.use(VueAxios, axios)
app.use(VueCookies)
app.use(admin)
app.use(Toaster, { position: 'top-right' })
app.use(BootstrapVue)
Object.keys(AllRules).forEach(rule => {
defineRule(rule, AllRules[rule]);


+ 5
- 4
Models/Posts.go View File

@ -40,10 +40,11 @@ type PostLink struct {
type PostImage struct {
Base
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Filepath string `gorm:"not null" json:"filepath"`
Mimetype string `gorm:"not null" json:"mimetype"`
Size int64 `gorm:"not null"`
PostID uuid.UUID `gorm:"type:uuid;column:post_id;not null;" json:"post_id"`
Filepath string `gorm:"not null" json:"-"`
PublicFilepath string `gorm:"not null" json:"filepath"`
Mimetype string `gorm:"not null" json:"mimetype"`
Size int64 `gorm:"not null"`
}
type PostVideo struct {


+ 8
- 6
Util/Files.go View File

@ -11,9 +11,10 @@ import (
)
type FileObject struct {
Filepath string
Mimetype string
Size int64
Filepath string
PublicFilepath string
Mimetype string
Size int64
}
func WriteFile(fileBytes []byte, acceptedMime string) (FileObject, error) {
@ -58,9 +59,10 @@ func WriteFile(fileBytes []byte, acceptedMime string) (FileObject, error) {
}
fileObject = FileObject{
Filepath: file.Name(),
Mimetype: mime.String(),
Size: fi.Size(),
Filepath: file.Name(),
PublicFilepath: strings.ReplaceAll(file.Name(), "./Frontend", ""),
Mimetype: mime.String(),
Size: fi.Size(),
}
return fileObject, err


Loading…
Cancel
Save