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{ 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) 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/logout", Auth.Logout).Methods("GET")
api.HandleFunc("/admin/me", Auth.Me).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)) 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", "@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1", "axios": "^0.26.1",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vee-validate": "^4.5.10", "vee-validate": "^4.5.10",
"vue": "^3.2.13", "vue": "^3.2.13",
@ -4131,6 +4132,17 @@
"@popperjs/core": "^2.10.2" "@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": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "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==", "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
"requires": {} "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": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "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", "@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1", "axios": "^0.26.1",
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"bootstrap-vue-3": "^0.1.10",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"vee-validate": "^4.5.10", "vee-validate": "^4.5.10",
"vue": "^3.2.13", "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 { .ProseMirror p:last-child {
margin-bottom: 0; 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 Post Details
</button> </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> </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> <h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update Post</h3>
<Form @submit="updatePost" v-slot="{ errors }"> <Form @submit="updatePost" v-slot="{ errors }">
<div class="row"> <div class="row">
<div class="col-md-8 mb-4">
<div class="col-md-8 mb-4">
<div class="form-outline"> <div class="form-outline">
<Field <Field
v-model="post.title" v-model="post.title"
@ -40,7 +48,7 @@
</div> </div>
</div> </div>
<div class="col-md-4 mb-4">
<div class="col-md-3 mb-4">
<div class="form-outline"> <div class="form-outline">
<select <select
v-model="post.front_page" v-model="post.front_page"
@ -50,9 +58,22 @@
<option :value="true">Yes</option> <option :value="true">Yes</option>
<option :value="false">No</option> <option :value="false">No</option>
</select> </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> </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>
<div class="row"> <div class="row">
@ -117,6 +138,64 @@
</Form> </Form>
</div> </div>
</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> </section>
</div> </div>
</template> </template>
@ -131,6 +210,8 @@ export default {
return { return {
tab: 'details', tab: 'details',
post: {}, post: {},
images: [],
imageLabel: 'Choose File',
} }
}, },
@ -203,6 +284,58 @@ export default {
} catch (error) { } catch (error) {
this.$toast.error('An error occured'); 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 AllRules from '@vee-validate/rules';
import Toaster from "@meforma/vue-toaster"; import Toaster from "@meforma/vue-toaster";
import Datepicker from '@vuepic/vue-datepicker'; import Datepicker from '@vuepic/vue-datepicker';
import BootstrapVue from 'bootstrap-vue-3'
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 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/css/bootstrap.min.css'
import 'bootstrap/dist/js/bootstrap.bundle.js' 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 '@vuepic/vue-datepicker/dist/main.css'
import './assets/css/admin.css' import './assets/css/admin.css'
@ -38,6 +41,7 @@ app.use(VueAxios, axios)
app.use(VueCookies) app.use(VueCookies)
app.use(admin) app.use(admin)
app.use(Toaster, { position: 'top-right' }) app.use(Toaster, { position: 'top-right' })
app.use(BootstrapVue)
Object.keys(AllRules).forEach(rule => { Object.keys(AllRules).forEach(rule => {
defineRule(rule, AllRules[rule]); defineRule(rule, AllRules[rule]);


+ 5
- 4
Models/Posts.go View File

@ -40,10 +40,11 @@ type PostLink struct {
type PostImage struct { type PostImage struct {
Base 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 { type PostVideo struct {


+ 8
- 6
Util/Files.go View File

@ -11,9 +11,10 @@ import (
) )
type FileObject struct { 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) { func WriteFile(fileBytes []byte, acceptedMime string) (FileObject, error) {
@ -58,9 +59,10 @@ func WriteFile(fileBytes []byte, acceptedMime string) (FileObject, error) {
} }
fileObject = FileObject{ 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 return fileObject, err


Loading…
Cancel
Save