Browse Source

Add AdminPostsList and restructure vue project

feature/add-admin-posts-frontend
Tovi Jaeschke-Rogers 3 years ago
parent
commit
67f0b9cea4
24 changed files with 1685 additions and 90 deletions
  1. +10
    -2
      Api/Posts.go
  2. +14
    -7
      Api/Users.go
  3. +23
    -8
      Database/Posts.go
  4. +55
    -0
      Database/Seeder/PostSeeder.go
  5. +2
    -0
      Database/Seeder/Seed.go
  6. +1
    -1
      Database/Seeder/UserSeeder.go
  7. +9
    -0
      Database/Users.go
  8. +1
    -1
      Frontend/Routes.go
  9. +1088
    -3
      Frontend/vue/package-lock.json
  10. +7
    -0
      Frontend/vue/package.json
  11. +4
    -0
      Frontend/vue/src/assets/css/admin.css
  12. +61
    -0
      Frontend/vue/src/components/admin/components/list/AdminListHeader.vue
  13. +17
    -3
      Frontend/vue/src/components/admin/components/navbar/AdminNavbar.vue
  14. +1
    -1
      Frontend/vue/src/components/admin/views/auth/AdminLogin.vue
  15. +0
    -0
      Frontend/vue/src/components/admin/views/auth/AdminSignup.vue
  16. +156
    -0
      Frontend/vue/src/components/admin/views/posts/AdminPostsForm.vue
  17. +139
    -0
      Frontend/vue/src/components/admin/views/posts/AdminPostsList.vue
  18. +1
    -1
      Frontend/vue/src/components/admin/views/users/AdminUsersCreate.vue
  19. +31
    -3
      Frontend/vue/src/components/admin/views/users/AdminUsersForm.vue
  20. +14
    -46
      Frontend/vue/src/components/admin/views/users/AdminUsersList.vue
  21. +15
    -2
      Frontend/vue/src/main.js
  22. +26
    -5
      Frontend/vue/src/router/index.js
  23. +2
    -2
      Models/Base.go
  24. +8
    -5
      Models/Posts.go

+ 10
- 2
Api/Posts.go View File

@ -21,6 +21,7 @@ func getPosts(w http.ResponseWriter, r *http.Request) {
returnJson []byte returnJson []byte
values url.Values values url.Values
page, pageSize int page, pageSize int
search string
err error err error
) )
@ -33,20 +34,27 @@ func getPosts(w http.ResponseWriter, r *http.Request) {
return return
} }
page, err = strconv.Atoi(values.Get("pageSize"))
pageSize, err = strconv.Atoi(values.Get("pageSize"))
if err != nil { if err != nil {
log.Println("Could not parse pageSize url argument") log.Println("Could not parse pageSize url argument")
Util.JsonReturn(w, 500, "An error occured") Util.JsonReturn(w, 500, "An error occured")
return return
} }
posts, err = Database.GetPosts(page, pageSize)
search = values.Get("search")
posts, err = Database.GetPosts(page, pageSize, search)
if err != nil { if err != nil {
log.Printf("An error occured: %s\n", err.Error()) log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured") Util.JsonReturn(w, 500, "An error occured")
return return
} }
if len(posts) == 0 {
Util.JsonReturn(w, 404, "No more data")
return
}
returnJson, err = json.MarshalIndent(posts, "", " ") returnJson, err = json.MarshalIndent(posts, "", " ")
if err != nil { if err != nil {
Util.JsonReturn(w, 500, "An error occured") Util.JsonReturn(w, 500, "An error occured")


+ 14
- 7
Api/Users.go View File

@ -171,19 +171,26 @@ func createUser(w http.ResponseWriter, r *http.Request) {
func updateUser(w http.ResponseWriter, r *http.Request) { func updateUser(w http.ResponseWriter, r *http.Request) {
var ( var (
currentUserData Models.User
userData Models.User
requestBody []byte
returnJson []byte
err error
userData Models.User
requestBody []byte
returnJson []byte
id string
err error
) )
currentUserData, err = Auth.CheckCookieCurrentUser(w, r)
_, err = Auth.CheckCookie(r)
if err != nil { if err != nil {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
return return
} }
id, err = Util.GetUserId(r)
if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured")
return
}
requestBody, err = ioutil.ReadAll(r.Body) requestBody, err = ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
log.Printf("Error encountered reading POST body: %s\n", err.Error()) log.Printf("Error encountered reading POST body: %s\n", err.Error())
@ -198,7 +205,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
err = Database.UpdateUser(currentUserData.ID.String(), &userData)
err = Database.UpdateUser(id, &userData)
if err != nil { if err != nil {
log.Printf("An error occured: %s\n", err.Error()) log.Printf("An error occured: %s\n", err.Error())
Util.JsonReturn(w, 500, "An error occured") Util.JsonReturn(w, 500, "An error occured")


+ 23
- 8
Database/Posts.go View File

@ -1,22 +1,22 @@
package Database package Database
import ( import (
"fmt"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models" "git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
func GetPosts(page, pageSize int) ([]Models.Post, error) {
func GetPosts(page, pageSize int, search string) ([]Models.Post, error) {
var ( var (
posts []Models.Post
err error
posts []Models.Post
query *gorm.DB
offset int
err error
) )
if page == 0 {
page = 1
}
switch { switch {
case pageSize > 100: case pageSize > 100:
pageSize = 100 pageSize = 100
@ -24,8 +24,23 @@ func GetPosts(page, pageSize int) ([]Models.Post, error) {
pageSize = 10 pageSize = 10
} }
err = DB.Offset(page).
offset = page * pageSize
search = fmt.Sprintf("%%%s%%", search)
query = DB.Model(Models.Post{}).
Preload(clause.Associations).
Offset(offset).
Limit(pageSize). Limit(pageSize).
Order("created_at desc")
if search != "%%" {
query = query.
Where("title LIKE ?", search).
Or("content LIKE ?", search)
}
err = query.
Find(&posts). Find(&posts).
Error Error


+ 55
- 0
Database/Seeder/PostSeeder.go View File

@ -0,0 +1,55 @@
package Seeder
import (
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Database"
"git.tovijaeschke.xyz/tovi/SuddenImpactRecords/Models"
)
func createPost(userData Models.User) (Models.Post, error) {
var (
postData Models.Post
err error
)
postData = Models.Post{
UserID: userData.ID,
Title: "Test post",
Content: "Test content",
FrontPage: true,
Order: 1,
PostLinks: []Models.PostLink{
{
Type: "Facebook",
Link: "http://facebook.com/",
},
},
}
err = Database.CreatePost(&postData)
return postData, err
}
func SeedPosts() {
var (
userData Models.User
i int
err error
)
err = Database.DB.
Model(Models.User{}).
First(&userData).
Error
if err != nil {
panic(err)
}
for i = 0; i <= 20; i++ {
_, err = createPost(userData)
if err != nil {
panic(err)
}
}
}

+ 2
- 0
Database/Seeder/Seed.go View File

@ -5,4 +5,6 @@ import "log"
func Seed() { func Seed() {
log.Println("Seeding users...") log.Println("Seeding users...")
SeedUsers() SeedUsers()
log.Println("Seeding posts...")
SeedPosts()
} }

+ 1
- 1
Database/Seeder/UserSeeder.go View File

@ -63,7 +63,7 @@ func createUser() (Models.User, error) {
firstName = randName(false) firstName = randName(false)
lastName = randName(true) lastName = randName(true)
email = fmt.Sprintf("%s%s+%s@email.com", firstName, lastName, Util.RandomString(4))
email = fmt.Sprintf("%s%s+%s@email.com", firstName, lastName, Util.RandomString(10))
password, err = Auth.HashPassword("password") password, err = Auth.HashPassword("password")
if err != nil { if err != nil {


+ 9
- 0
Database/Users.go View File

@ -127,6 +127,15 @@ func UpdateUser(id string, userData *Models.User) error {
Updates(userData). Updates(userData).
Error Error
if err != nil {
return err
}
err = DB.Model(Models.User{}).
Where("id = ?", id).
First(userData).
Error
userData.Password = "" userData.Password = ""
return err return err


+ 1
- 1
Frontend/Routes.go View File

@ -17,7 +17,7 @@ var (
"/admin/signup", "/admin/signup",
"/admin/users", "/admin/users",
"/admin/users/new", "/admin/users/new",
"/admin/users/{id}",
"/admin/posts",
} }
) )


+ 1088
- 3
Frontend/vue/package-lock.json
File diff suppressed because it is too large
View File


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

@ -9,7 +9,14 @@
"watch": "vue-cli-service build --watch" "watch": "vue-cli-service build --watch"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"@meforma/vue-toaster": "^1.3.0", "@meforma/vue-toaster": "^1.3.0",
"@tiptap/starter-kit": "^2.0.0-beta.183",
"@tiptap/vue-3": "^2.0.0-beta.90",
"@vee-validate/rules": "^4.5.10", "@vee-validate/rules": "^4.5.10",
"@vuepic/vue-datepicker": "^3.0.0", "@vuepic/vue-datepicker": "^3.0.0",
"axios": "^0.26.1", "axios": "^0.26.1",


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

@ -2,6 +2,10 @@ body {
min-height: 100vh; min-height: 100vh;
} }
.nav-link svg {
padding-right: 0.2rem;
}
#app, #admin-page-container { #app, #admin-page-container {
min-height: 100vh; min-height: 100vh;
height: 100%; height: 100%;


+ 61
- 0
Frontend/vue/src/components/admin/components/list/AdminListHeader.vue View File

@ -0,0 +1,61 @@
<template>
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="row">
<div class="col-md-6 col-10">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Search..."
ref="search"
>
<div class="input-group-append">
<button
class="btn btn-dark"
type="button"
@click="searchFunction"
>
Search
</button>
</div>
</div>
</div>
<div class="col-md-6 float-right col-2">
<div class="btn-group float-right" role="group">
<router-link :to="{ name: addNewTo }">
<button
type="button"
class="btn btn-rounded btn-dark d-none d-md-inline-block"
>
<i class="fa-solid fa-plus"></i>
{{ addNewLabel }}
</button>
<button
type="button"
class="btn btn-rounded btn-dark d-inline-block d-md-none"
>
<i class="fa-solid fa-plus"></i>
</button>
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
addNewTo: { type: String },
addNewLabel: { type: String },
searchFunction: { type: Function },
}
}
</script>

Frontend/vue/src/components/admin/AdminNavbar.vue → Frontend/vue/src/components/admin/components/navbar/AdminNavbar.vue View File


Frontend/vue/src/components/admin/AdminLogin.vue → Frontend/vue/src/components/admin/views/auth/AdminLogin.vue View File


Frontend/vue/src/components/admin/AdminSignup.vue → Frontend/vue/src/components/admin/views/auth/AdminSignup.vue View File


+ 156
- 0
Frontend/vue/src/components/admin/views/posts/AdminPostsForm.vue View File

@ -0,0 +1,156 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<section class="container mt-5">
<div class="row mb-3">
<div class="col-12">
<div class="page-nav-container">
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-rounded"
:class="tab === 'details' ? 'btn-dark' : 'btn-outline-dark'"
@click="tab = 'details'"
>
Post Details
</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'">
<h3 class="mb-4 pb-2 pb-md-0 mb-md-5">Update Post</h3>
<Form @submit="updatePost" v-slot="{ meta, errors }">
<div class="row">
<div class="col-md-8 mb-4">
<div class="form-outline">
<Field
v-model="post.title"
type="text"
id="title"
name="Title"
class="form-control form-control-lg"
:class="errors['Title'] ? 'invalid' : ''"
rules="required"/>
<label v-if="!errors['Title']" class="form-label" for="title">Title</label>
<ErrorMessage name="Title" as="label" class="form-label" for="title"/>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="form-outline">
<select
v-model="post.front_page"
id="front_page"
name="Front Page"
class="form-control form-control-lg form-select">
<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>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-4 pb-2">
<div class="form-outline">
<date-picker
v-model="post.created_at"
format="dd/MM/yyyy, HH:mm"
disabled="disabled"
id="created_at"/>
<label class="form-label" for="created_at">Created At</label>
</div>
</div>
<div class="col-md-4 mb-4 pb-2">
<div class="form-outline">
<date-picker
v-model="post.updated_at"
format="dd/MM/yyyy, HH:mm"
disabled="disabled"
id="updated_at"/>
<label class="form-label" for="updated_at">Updated At</label>
</div>
</div>
</div>
<div class="mt-2 pt-2 right-align">
<button class="btn btn-danger btn-md" type="button">
Delete
</button>
<button :disabled="!meta.touched || !meta.valid" class="btn btn-primary btn-md" type="submit">
Update
</button>
</div>
</Form>
</div>
</div>
</section>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/components/navbar/AdminNavbar'
import { Form, Field, ErrorMessage } from 'vee-validate'
export default {
data() {
return {
tab: 'details',
post: {},
}
},
components: {
AdminNavbar,
Form,
Field,
ErrorMessage,
},
mounted () {
this.getPost()
},
methods: {
async getPost () {
try {
const response = await this.axios.get(`/post/${this.$route.params.id}`)
if (response.status === 200) {
this.post = response.data
}
} catch (error) {
this.$toast.error('An error occurred.')
}
},
async updatePost () {
try {
let response = await this.axios.put(
`/user/${this.$route.params.id}`,
{
first_name: this.user.first_name,
last_name: this.user.last_name,
email: this.user.email,
},
)
if (response.status === 200) {
this.$toast.success('Successfully updated user details.');
this.setPostFromResponse(response)
}
} catch (error) {
this.$toast.error('An error occured');
}
},
}
}
</script>

+ 139
- 0
Frontend/vue/src/components/admin/views/posts/AdminPostsList.vue View File

@ -0,0 +1,139 @@
<template>
<div id="admin-page-container">
<admin-navbar/>
<div class="container table-responsive mt-5 pb-5">
<admin-list-header
addNewTo="AdminUsersCreate"
addNewLabel="Add Post"
:searchFunction="searchPosts"
ref="listHeader"
/>
<div class="card shadow-2-strong card-registration">
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Title</th>
<th scope="col">Front Page</th>
<th scope="col" class="d-none d-sm-table-cell">Created At</th>
<th scope="col" class="d-none d-sm-table-cell">Published At</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.id">
<td class="align-middle">{{ post.title }}</td>
<td class="align-middle">{{ post.front_page }}</td>
<td class="align-middle d-none d-sm-table-cell">{{ formatDate(post.created_at) }}</td>
<td v-if="post.published_at" class="align-middle d-none d-sm-table-cell">{{ formatDate(post.published_at) }}</td>
<td v-if="!post.published_at" class="align-middle d-none d-sm-table-cell">-</td>
<td class="align-middle">
<router-link
:to="{ name: 'AdminPostsForm', params: { id: post.id } }"
>
<button
class="btn btn-outline-dark"
>
Open
</button>
</router-link>
</td>
</tr>
</tbody>
</table>
<p v-if="dataEnd" class="py-2 center-align text-muted">No more data</p>
</div>
</div>
</div>
</template>
<script>
import AdminNavbar from '@/components/admin/components/navbar/AdminNavbar'
import AdminListHeader from '@/components/admin/components/list/AdminListHeader.vue'
export default {
data() {
return {
posts: [],
page: 0,
pageSize: 15,
search: '',
dataEnd: false,
}
},
components: {
AdminNavbar,
AdminListHeader,
},
beforeMount () {
this.getInitialPosts()
},
mounted () {
this.axios.get('/admin/me')
this.getNextPosts()
},
methods: {
formatDate (dateString) {
const d = new Date(dateString)
let hours = d.getHours();
let minutes = d.getMinutes();
const ampm = hours >= 12 ? 'pm' : 'am';
hours = hours % 12;
hours = hours ? hours : 12;
minutes = minutes < 10 ? '0'+minutes : minutes;
const strTime = hours + ':' + minutes + ' ' + ampm;
return d.getDate() + "/" + (d.getMonth()+1) + "/" + d.getFullYear() + " " + strTime;
},
async getInitialPosts () {
try {
const response = await this.axios.get(
`/post?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.posts = response.data
}
} catch (error) {
if (error.response.status === 404) {
this.posts = {}
this.dataEnd = true
}
}
},
async getNextPosts () {
window.onscroll = async () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow) {
try {
this.page += 1
const response = await this.axios.get(
`/post?page=${this.page}&pageSize=${this.pageSize}&search=${this.search}`
)
if (response.status === 200) {
this.posts.push(...response.data)
}
} catch (error) {
console.log(error)
if (error.response.status === 404) {
this.dataEnd = true
}
}
}
}
},
searchPosts () {
this.search = this.$refs.listHeader.$refs.search.value
this.getInitialPosts()
}
}
}
</script>

Frontend/vue/src/components/admin/users/AdminUsersCreate.vue → Frontend/vue/src/components/admin/views/users/AdminUsersCreate.vue View File


Frontend/vue/src/components/admin/users/AdminUsersForm.vue → Frontend/vue/src/components/admin/views/users/AdminUsersForm.vue View File


Frontend/vue/src/components/admin/users/AdminUsersList.vue → Frontend/vue/src/components/admin/views/users/AdminUsersList.vue View File


+ 15
- 2
Frontend/vue/src/main.js View File

@ -6,7 +6,18 @@ 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 '@vuepic/vue-datepicker/dist/main.css'
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { fas } from '@fortawesome/free-solid-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons';
import { dom } from "@fortawesome/fontawesome-svg-core";
library.add(fas);
library.add(fab);
library.add(far);
dom.watch();
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
@ -15,7 +26,7 @@ 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 the CSS or use your own!
import "vue-toastification/dist/index.css";
import '@vuepic/vue-datepicker/dist/main.css'
import './assets/css/admin.css' import './assets/css/admin.css'
@ -34,4 +45,6 @@ Object.keys(AllRules).forEach(rule => {
app.component('date-picker', Datepicker); app.component('date-picker', Datepicker);
app.component("font-awesome-icon", FontAwesomeIcon)
app.mount('#app') app.mount('#app')

+ 26
- 5
Frontend/vue/src/router/index.js View File

@ -1,10 +1,15 @@
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue"; import HelloWorld from "@/components/HelloWorld.vue";
import AdminLogin from "@/components/admin/AdminLogin.vue";
import AdminSignup from "@/components/admin/AdminSignup.vue";
import AdminUsersList from "@/components/admin/users/AdminUsersList.vue";
import AdminUsersCreate from "@/components/admin/users/AdminUsersCreate.vue";
import AdminUsersForm from "@/components/admin/users/AdminUsersForm.vue";
import AdminLogin from "@/components/admin/views/auth/AdminLogin.vue";
import AdminSignup from "@/components/admin/views/auth/AdminSignup.vue";
import AdminPostsList from "@/components/admin/views/posts/AdminPostsList.vue";
import AdminPostsForm from "@/components/admin/views/posts/AdminPostsForm.vue";
import AdminUsersList from "@/components/admin/views/users/AdminUsersList.vue";
import AdminUsersCreate from "@/components/admin/views/users/AdminUsersCreate.vue";
import AdminUsersForm from "@/components/admin/views/users/AdminUsersForm.vue";
import admin from '@/store/admin/index.js' import admin from '@/store/admin/index.js'
@ -24,6 +29,22 @@ const routes = [
name: "AdminSignup", name: "AdminSignup",
component: AdminSignup, component: AdminSignup,
}, },
{
path: "/admin/posts",
name: "AdminPostsList",
component: AdminPostsList,
meta: {
requiresAuth: true,
},
},
{
path: "/admin/posts/:id",
name: "AdminPostsForm",
component: AdminPostsForm,
meta: {
requiresAuth: true,
},
},
{ {
path: "/admin/users", path: "/admin/users",
name: "AdminUsersList", name: "AdminUsersList",


+ 2
- 2
Models/Base.go View File

@ -10,8 +10,8 @@ import (
// Base contains common columns for all tables. // Base contains common columns for all tables.
type Base struct { type Base struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"` ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `sql:"index" json:"-"` DeletedAt *time.Time `sql:"index" json:"-"`
} }


+ 8
- 5
Models/Posts.go View File

@ -1,16 +1,19 @@
package Models package Models
import ( import (
"time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
type Post struct { type Post struct {
Base Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
Title string `gorm:"not null" json:"title"`
Content string `gorm:"not null" json:"content"`
FrontPage bool `gorm:"not null;type:boolean" json:"front_page"`
Order int `gorm:"not null" json:"order"`
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
Title string `gorm:"not null" json:"title"`
Content string `gorm:"not null" json:"content"`
FrontPage bool `gorm:"not null;type:boolean" json:"front_page"`
Order int `gorm:"not null" json:"order"`
PublishedAt *time.Time `json:"published_at"`
PostLinks []PostLink `json:"links"` PostLinks []PostLink `json:"links"`
PostImages []PostImage `json:"images"` PostImages []PostImage `json:"images"`


Loading…
Cancel
Save