Merge pull request #33 from Floriansylvain/feature/postCreation

Feature/post creation
This commit is contained in:
Florian Sylvain
2024-05-18 12:48:08 +02:00
committed by GitHub
16 changed files with 376 additions and 26 deletions
+51
View File
@@ -0,0 +1,51 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "tmp\\main.exe"
cmd = "go build -o ./tmp/main.exe ./main"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "api\\static\\tinymce", "api\\static\\bootstrap-icons"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true
+2
View File
@@ -10,3 +10,5 @@ bin
# Environment # Environment
.env .env
tmp/
@@ -29,6 +29,16 @@ func (a *PostRepository) Get(id uint32) (domain.Post, error) {
return mapPostToDomain(post), nil return mapPostToDomain(post), nil
} }
func (a *PostRepository) GetByName(name string) (domain.Post, error) {
var post entity.Post
err := a.db.Model(&entity.Post{}).Where("title = ?", name).First(&post).Error
if err != nil {
return domain.Post{}, err
}
return mapPostToDomain(post), nil
}
func (a *PostRepository) Create(post domain.Post) (domain.Post, error) { func (a *PostRepository) Create(post domain.Post) (domain.Post, error) {
creationResult := a.db.Create(&entity.Post{ creationResult := a.db.Create(&entity.Post{
Title: post.Title, Title: post.Title,
@@ -59,4 +69,25 @@ func (a *PostRepository) GetAll() []domain.Post {
return domainPosts return domainPosts
} }
func (a *PostRepository) UpdateBody(id uint32, body string) error {
var localPost entity.Post
err := a.db.Model(&entity.Post{}).First(&localPost, id).Error
if err != nil {
return err
}
localPost.Body = body
err = a.db.Save(&localPost).Error
if err != nil {
return err
}
return nil
}
func (a *PostRepository) Delete(id uint32) error {
//TODO implement me
panic("implement me")
}
var _ gateways.IPostRepository = &PostRepository{} var _ gateways.IPostRepository = &PostRepository{}
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>GoCMS | Create post</title>
{{.Head}}
<style>
form {
max-width: 24rem;
}
</style>
</head>
<body class="text-dark">
{{.Navbar}}
<div class="container mt-3 text-black-50">
<h1>Post - creation</h1>
<p>Create a new post</p>
<form action="create" method="post" class="d-flex align-items-center flex-column gap-3" id="createPostForm">
<div class="form-floating w-100">
<input type="text" class="form-control {{ if .PageError.IsError }} is-invalid {{ end }}" id="name"
name="name" placeholder="Chose the post name" value="{{ .Name }}">
<label for="name">Name</label>
<div class="invalid-feedback">{{ .PageError.Message }}</div>
</div>
<button id="createPostButton" class="btn btn-primary w-100" disabled type="submit">
<span class="createPostFormButtonLoading visually-hidden spinner-border spinner-border-sm"
role="status"></span>
<span class="createPostFormButtonDefault">Create</span>
</button>
</form>
</div>
<script>
const button = document.querySelector("#createPostButton")
const inputs = document.querySelectorAll('input')
const form = document.querySelector("#createPostForm")
function formFieldsEmpty() {
return Array.from(inputs).some((input) => input.value === "")
}
function setButtonDisabled() {
button.disabled = formFieldsEmpty() ? "disabled" : ""
}
function setButtonLoading() {
button.classList.add("disabled")
button.querySelector(".createPostFormButtonDefault").classList.add("visually-hidden")
button.querySelector(".createPostFormButtonLoading").classList.remove("visually-hidden")
}
function onCreatePostFormSubmit(event) {
setButtonLoading()
event.target.submit()
}
function onInput(event) {
if (event.target.tagName === "INPUT") setButtonDisabled()
}
form.addEventListener('submit', onCreatePostFormSubmit)
window.addEventListener('input', onInput)
setButtonDisabled()
</script>
</body>
</html>
@@ -6,28 +6,36 @@
</head> </head>
<body class="text-dark vh-100 d-flex flex-column"> <body class="text-dark vh-100 d-flex flex-column">
{{.Navbar}} {{.Navbar}}
<div class="container mt-3 text-black-50 d-flex flex-column h-100"> <form class="container mt-3 text-black-50 d-flex flex-column h-100" action="edit" method="post">
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<div> <div>
<h1>Post - edition</h1> <h1>Post - edition</h1>
<label for="postContent">Edition</label> <label for="postBody">Edition</label>
</div> </div>
{{ if .Alert.Message }}
<div class="alert {{ if .Alert.IsError }} alert-danger {{ else }} alert-success {{ end }} alert-dismissible" role="alert">
{{ .Alert.Message }}
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="alert"></button>
</div>
{{ end }}
<div class="d-flex justify-content-center align-items-center"> <div class="d-flex justify-content-center align-items-center">
<button class="btn btn-success d-flex justify-content-center align-items-center gap-1" style="fill: white;"> <button class="btn btn-success d-flex justify-content-center align-items-center gap-1" style="fill: white;" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="M840-680v480q0 33-23.5 56.5T760-120H200q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h480l160 160Zm-80 34L646-760H200v560h560v-446ZM480-240q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35ZM240-560h360v-160H240v160Zm-40-86v446-560 114Z"/>
</svg>
Save Save
</button> </button>
</div> </div>
</div> </div>
<div class="text-black-50 d-flex align-items-center justify-content-center h-100 py-3"> <div class="text-black-50 d-flex align-items-center justify-content-center h-100 py-3">
<textarea id="postContent" name="postContent"></textarea> <textarea id="postBody" name="postBody">{{.Body}}</textarea>
</div> </div>
</div> </form>
<script src="/static/tinymce/js/tinymce/tinymce.min.js"></script> <script src="/static/tinymce/js/tinymce/tinymce.min.js"></script>
<script> <script>
tinymce.init({ tinymce.init({
selector: '#postContent', selector: '#postBody',
promotion: false, promotion: false,
plugins: 'image', plugins: 'image',
width: '100%', width: '100%',
@@ -1,21 +1,73 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>GoCMS | Posts</title> <title>GoCMS | Posts</title>
{{.Head}} {{.Head}}
<style>
.btn-outline-danger:hover > * {
fill: #fff !important;
transition: fill ease-in-out 50ms;
}
</style>
</head> </head>
<body class="text-dark"> <body class="text-dark">
{{.Navbar}} {{.Navbar}}
<div class="container mt-3 text-black-50"> <div class="container mt-3 text-black-50">
<h1>Posts</h1> <h1>Posts</h1>
<p>List of your posts</p> <p>List of your posts</p>
<table class="table table-striped"> <table class="table table-hover">
<thead> <thead>
</thead> <tr>
<tbody> <th scope="col">Name</th>
</tbody> <th scope="col">Creation Date</th>
</table> <th scope="col">Edition Date</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="4" class="text-center">
<form action="post/create" method="get">
<button class="btn btn-sm btn-link w-100 h-100" type="submit">Create a new post...</button>
</form>
</td>
</tr>
{{ range $post := .Posts }}
<tr>
<td>{{ $post.Title }}</td>
<td class="date">{{ $post.CreatedAt }}</td>
<td class="date">{{ $post.UpdatedAt }}</td>
<td>
<a href="/post/{{ $post.Title }}/edit" class="btn btn-secondary btn-sm">
<svg width="12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#pen"/>
</svg>
</a>
<a href="/post/{{ $post.Title }}/delete" class="btn btn-outline-danger btn-sm">
<svg width="12px" height="12px" fill="currentColor">
<use xlink:href="/static/bootstrap-icons.svg#trash"/>
</svg>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div> </div>
<script>
const datesElements = document.querySelectorAll("td.date")
datesElements.forEach(date => {
date.innerText = new Date(date.innerText).toLocaleDateString(undefined, {
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
})
})
</script>
</body> </body>
</html> </html>
+2
View File
@@ -14,6 +14,7 @@ type UseCases struct {
CreatePostUseCase *useCases.CreatePostUseCase CreatePostUseCase *useCases.CreatePostUseCase
GetPostUseCase *useCases.GetPostUseCase GetPostUseCase *useCases.GetPostUseCase
ListPostsUseCase *useCases.ListPostsUseCase ListPostsUseCase *useCases.ListPostsUseCase
UpdatePostUseCase *useCases.UpdatePostUseCase
GetUserUseCase *useCases.GetUserUseCase GetUserUseCase *useCases.GetUserUseCase
CreateUserUseCase *useCases.CreateUserUseCase CreateUserUseCase *useCases.CreateUserUseCase
UpdateUserUseCase *useCases.UpdateUserUseCase UpdateUserUseCase *useCases.UpdateUserUseCase
@@ -49,6 +50,7 @@ func InitContainer() {
CreatePostUseCase: useCases.NewCreatePostUseCase(db), CreatePostUseCase: useCases.NewCreatePostUseCase(db),
GetPostUseCase: useCases.NewGetPostUseCase(db), GetPostUseCase: useCases.NewGetPostUseCase(db),
ListPostsUseCase: useCases.NewListPostsUseCase(db), ListPostsUseCase: useCases.NewListPostsUseCase(db),
UpdatePostUseCase: useCases.NewUpdatePostUseCase(db),
GetUserUseCase: useCases.NewGetUserUseCase(db), GetUserUseCase: useCases.NewGetUserUseCase(db),
CreateUserUseCase: useCases.NewCreateUserUseCase(db), CreateUserUseCase: useCases.NewCreateUserUseCase(db),
UpdateUserUseCase: useCases.NewUpdateUserUseCase(db), UpdateUserUseCase: useCases.NewUpdateUserUseCase(db),
+4 -1
View File
@@ -122,7 +122,10 @@ func NewPageRouter() http.Handler {
r.Use(IsVerifiedMiddleware) r.Use(IsVerifiedMiddleware)
r.Get("/home", GetHomePage) r.Get("/home", GetHomePage)
r.Get("/post", GetPostsPage) r.Get("/post", GetPostsPage)
r.Get("/post/edit", GetPostEditPage) r.Get("/post/{name}/edit", GetPostEditPage)
r.Post("/post/{name}/edit", PostPostEditPage)
r.Get("/post/create", GetPostCreatePage)
r.Post("/post/create", PostPostCreatePage)
}) })
return r return r
+56
View File
@@ -0,0 +1,56 @@
package api
import (
"GoCMS/useCases"
"html/template"
"net/http"
"regexp"
)
type PostCreatePageError struct {
IsError bool
Message string
}
func GetPostCreatePageTemplate(postName string, errorMessage string) ([]byte, error) {
navbarTmpl, _ := Container.GetPageUseCase.GetPage("componentNavbar", nil)
return Container.GetPageUseCase.GetPage("postCreate", map[string]interface{}{
"Navbar": template.HTML(navbarTmpl),
"Head": headTmpl,
"PageError": PostCreatePageError{
IsError: errorMessage != "",
Message: errorMessage,
},
"Name": postName,
})
}
func PostPostCreatePage(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
postName := r.FormValue("name")
pattern := regexp.MustCompile("^[a-zA-Z0-9À-ÖØ-öø-ÿĀ-ſḀ-ỿ ]{3,50}$")
if !pattern.MatchString(postName) {
postsTmpl, _ := GetPostCreatePageTemplate(postName, "Name should be alphanumeric, and between 3 and 50 characters.")
_, _ = w.Write(postsTmpl)
return
}
post, err := Container.CreatePostUseCase.CreatePost(useCases.CreatePostCommand{
Title: postName,
Body: "",
})
if err != nil {
postsTmpl, _ := GetPostCreatePageTemplate(postName, "Something went wrong when creating the post, please contact admin.")
_, _ = w.Write(postsTmpl)
return
}
http.Redirect(w, r, "/post/"+post.Title+"/edit", http.StatusSeeOther)
}
func GetPostCreatePage(w http.ResponseWriter, _ *http.Request) {
postsTmpl, _ := GetPostCreatePageTemplate("", "")
_, _ = w.Write(postsTmpl)
}
+56 -3
View File
@@ -1,15 +1,68 @@
package api package api
import ( import (
"github.com/go-chi/chi/v5"
"html/template" "html/template"
"net/http" "net/http"
"net/url"
) )
func GetPostEditPage(w http.ResponseWriter, _ *http.Request) { type PostEditPageAlert struct {
IsError bool
Message string
}
func getPostEditPageTemplate(body string, alert PostEditPageAlert) []byte {
navbarTmpl, _ := Container.GetPageUseCase.GetPage("componentNavbar", nil) navbarTmpl, _ := Container.GetPageUseCase.GetPage("componentNavbar", nil)
postsTmpl, _ := Container.GetPageUseCase.GetPage("postEdit", map[string]interface{}{ postTmpl, _ := Container.GetPageUseCase.GetPage("postEdit", map[string]interface{}{
"Navbar": template.HTML(navbarTmpl), "Navbar": template.HTML(navbarTmpl),
"Head": headTmpl, "Head": headTmpl,
"Body": body,
"Alert": alert,
}) })
_, _ = w.Write(postsTmpl) return postTmpl
}
func PostPostEditPage(w http.ResponseWriter, r *http.Request) {
postName := chi.URLParam(r, "name")
if len(postName) == 0 {
_, _ = w.Write(getPostEditPageTemplate("", PostEditPageAlert{
IsError: true,
Message: "Could not find the requested post.",
}))
return
}
_ = r.ParseForm()
postBody := r.FormValue("postBody")
parsedName, _ := url.PathUnescape(postName)
post, _ := Container.GetPostUseCase.GetPostByName(parsedName)
err := Container.UpdatePostUseCase.UpdateBody(post.ID, postBody)
if err != nil {
_, _ = w.Write(getPostEditPageTemplate(postBody, PostEditPageAlert{
IsError: true,
Message: "Could not save the post: " + err.Error(),
}))
return
}
_, _ = w.Write(getPostEditPageTemplate(postBody, PostEditPageAlert{
IsError: false,
Message: "Post successfully edited!",
}))
}
func GetPostEditPage(w http.ResponseWriter, r *http.Request) {
postName := chi.URLParam(r, "name")
if len(postName) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
parsedName, _ := url.PathUnescape(postName)
post, _ := Container.GetPostUseCase.GetPostByName(parsedName)
_, _ = w.Write(getPostEditPageTemplate(post.Body, PostEditPageAlert{
IsError: false,
Message: "",
}))
} }
+2
View File
@@ -6,10 +6,12 @@ import (
) )
func GetPostsPage(w http.ResponseWriter, _ *http.Request) { func GetPostsPage(w http.ResponseWriter, _ *http.Request) {
posts := Container.ListPostsUseCase.ListPosts()
navbarTmpl, _ := Container.GetPageUseCase.GetPage("componentNavbar", nil) navbarTmpl, _ := Container.GetPageUseCase.GetPage("componentNavbar", nil)
postsTmpl, _ := Container.GetPageUseCase.GetPage("posts", map[string]interface{}{ postsTmpl, _ := Container.GetPageUseCase.GetPage("posts", map[string]interface{}{
"Navbar": template.HTML(navbarTmpl), "Navbar": template.HTML(navbarTmpl),
"Head": headTmpl, "Head": headTmpl,
"Posts": posts,
}) })
_, _ = w.Write(postsTmpl) _, _ = w.Write(postsTmpl)
} }
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.0 MiB

+3
View File
@@ -6,6 +6,9 @@ import (
type IPostRepository interface { type IPostRepository interface {
Get(id uint32) (post.Post, error) Get(id uint32) (post.Post, error)
GetByName(name string) (post.Post, error)
GetAll() []post.Post GetAll() []post.Post
Create(post post.Post) (post.Post, error) Create(post post.Post) (post.Post, error)
UpdateBody(id uint32, body string) error
Delete(id uint32) error
} }
+4
View File
@@ -19,3 +19,7 @@ func NewGetPostUseCase(db *gorm.DB) *GetPostUseCase {
func (g *GetPostUseCase) GetPost(id uint32) (post.Post, error) { func (g *GetPostUseCase) GetPost(id uint32) (post.Post, error) {
return g.postRepository.Get(id) return g.postRepository.Get(id)
} }
func (g *GetPostUseCase) GetPostByName(name string) (post.Post, error) {
return g.postRepository.GetByName(name)
}
+20
View File
@@ -0,0 +1,20 @@
package useCases
import (
"GoCMS/adapters/secondary/gateways"
"gorm.io/gorm"
)
type UpdatePostUseCase struct {
postRepository gateways.PostRepository
}
func NewUpdatePostUseCase(db *gorm.DB) *UpdatePostUseCase {
return &UpdatePostUseCase{
postRepository: *gateways.NewPostRepository(db),
}
}
func (g *UpdatePostUseCase) UpdateBody(id uint32, body string) error {
return g.postRepository.UpdateBody(id, body)
}
-4
View File
@@ -10,10 +10,6 @@ type UpdateUserUseCase struct {
userRepository gateways.UserRepository userRepository gateways.UserRepository
} }
type UpdateVerificationStatusCommand struct {
isVerified bool
}
func NewUpdateUserUseCase(db *gorm.DB) *UpdateUserUseCase { func NewUpdateUserUseCase(db *gorm.DB) *UpdateUserUseCase {
return &UpdateUserUseCase{ return &UpdateUserUseCase{
userRepository: *gateways.NewUserRepository(db), userRepository: *gateways.NewUserRepository(db),