Blog edit and preview page
This commit is contained in:
parent
5b051391d6
commit
5009065480
|
@ -2,8 +2,10 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/forms"
|
||||||
"github.com/kirsle/blog/core/models/settings"
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
@ -32,5 +34,38 @@ func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get the current settings.
|
// Get the current settings.
|
||||||
settings, _ := settings.Load()
|
settings, _ := settings.Load()
|
||||||
v.Data["s"] = settings
|
v.Data["s"] = settings
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
redisPort, _ := strconv.Atoi(r.FormValue("redis-port"))
|
||||||
|
redisDB, _ := strconv.Atoi(r.FormValue("redis-db"))
|
||||||
|
form := &forms.Settings{
|
||||||
|
Title: r.FormValue("title"),
|
||||||
|
AdminEmail: r.FormValue("admin-email"),
|
||||||
|
RedisEnabled: r.FormValue("redis-enabled") == "true",
|
||||||
|
RedisHost: r.FormValue("redis-host"),
|
||||||
|
RedisPort: redisPort,
|
||||||
|
RedisDB: redisDB,
|
||||||
|
RedisPrefix: r.FormValue("redis-prefix"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy form values into the settings struct for display, in case of
|
||||||
|
// any validation errors.
|
||||||
|
settings.Site.Title = form.Title
|
||||||
|
settings.Site.AdminEmail = form.AdminEmail
|
||||||
|
settings.Redis.Enabled = form.RedisEnabled
|
||||||
|
settings.Redis.Host = form.RedisHost
|
||||||
|
settings.Redis.Port = form.RedisPort
|
||||||
|
settings.Redis.DB = form.RedisDB
|
||||||
|
settings.Redis.Prefix = form.RedisPrefix
|
||||||
|
err := form.Validate()
|
||||||
|
if err != nil {
|
||||||
|
v.Error = err
|
||||||
|
} else {
|
||||||
|
// Save the settings.
|
||||||
|
settings.Save()
|
||||||
|
b.FlashAndReload(w, r, "Settings have been saved!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
b.RenderTemplate(w, r, "admin/settings", v)
|
b.RenderTemplate(w, r, "admin/settings", v)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
r.HandleFunc("/login", blog.LoginHandler)
|
r.HandleFunc("/login", blog.LoginHandler)
|
||||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||||
blog.AdminRoutes(r)
|
blog.AdminRoutes(r)
|
||||||
|
blog.BlogRoutes(r)
|
||||||
|
|
||||||
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
||||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Form: forms.Setup{},
|
Form: forms.Setup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == http.MethodPost {
|
||||||
form := &forms.Login{
|
form := &forms.Login{
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
|
@ -42,7 +42,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars.Error = errors.New("bad username or password")
|
vars.Error = errors.New("bad username or password")
|
||||||
} else {
|
} else {
|
||||||
// Login OK!
|
// Login OK!
|
||||||
vars.Flash = "Login OK!"
|
b.Flash(w, r, "Login OK!")
|
||||||
b.Login(w, r, user)
|
b.Login(w, r, user)
|
||||||
|
|
||||||
// A next URL given? TODO: actually get to work
|
// A next URL given? TODO: actually get to work
|
||||||
|
|
58
core/blog.go
Normal file
58
core/blog.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/models/posts"
|
||||||
|
"github.com/urfave/negroni"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlogRoutes attaches the blog routes to the app.
|
||||||
|
func (b *Blog) BlogRoutes(r *mux.Router) {
|
||||||
|
// Login-required routers.
|
||||||
|
loginRouter := mux.NewRouter()
|
||||||
|
loginRouter.HandleFunc("/blog/edit", b.EditBlog)
|
||||||
|
r.PathPrefix("/blog").Handler(
|
||||||
|
negroni.New(
|
||||||
|
negroni.HandlerFunc(b.LoginRequired),
|
||||||
|
negroni.Wrap(loginRouter),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
|
||||||
|
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
|
||||||
|
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
||||||
|
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||||
|
r.PathPrefix("/admin").Handler(negroni.New(
|
||||||
|
negroni.HandlerFunc(b.LoginRequired),
|
||||||
|
negroni.Wrap(adminRouter),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditBlog is the blog writing and editing page.
|
||||||
|
func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
v := NewVars(map[interface{}]interface{}{
|
||||||
|
"preview": "",
|
||||||
|
})
|
||||||
|
post := posts.New()
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
// Parse from form values.
|
||||||
|
post.LoadForm(r)
|
||||||
|
|
||||||
|
// Previewing, or submitting?
|
||||||
|
switch r.FormValue("submit") {
|
||||||
|
case "preview":
|
||||||
|
v.Data["preview"] = template.HTML(b.RenderMarkdown(post.Body))
|
||||||
|
case "submit":
|
||||||
|
if err := post.Validate(); err != nil {
|
||||||
|
v.Error = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Data["post"] = post
|
||||||
|
b.RenderTemplate(w, r, "blog/edit", v)
|
||||||
|
}
|
31
core/forms/settings.go
Normal file
31
core/forms/settings.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package forms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Settings are the user-facing admin settings.
|
||||||
|
type Settings struct {
|
||||||
|
Title string
|
||||||
|
AdminEmail string
|
||||||
|
RedisEnabled bool
|
||||||
|
RedisHost string
|
||||||
|
RedisPort int
|
||||||
|
RedisDB int
|
||||||
|
RedisPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the form.
|
||||||
|
func (f Settings) Validate() error {
|
||||||
|
if len(f.Title) == 0 {
|
||||||
|
return errors.New("website title is required")
|
||||||
|
}
|
||||||
|
if f.AdminEmail != "" {
|
||||||
|
_, err := mail.ParseAddress(f.AdminEmail)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Form: forms.Setup{},
|
Form: forms.Setup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == http.MethodPost {
|
||||||
form := forms.Setup{
|
form := forms.Setup{
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
|
@ -49,7 +49,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// All set!
|
// All set!
|
||||||
b.Login(w, r, user)
|
b.Login(w, r, user)
|
||||||
b.Redirect(w, "/admin")
|
b.FlashAndRedirect(w, r, "/admin", "Admin user created and logged in.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
core/markdown.go
Normal file
9
core/markdown.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import "github.com/shurcooL/github_flavored_markdown"
|
||||||
|
|
||||||
|
// RenderMarkdown renders markdown to HTML.
|
||||||
|
func (b *Blog) RenderMarkdown(input string) string {
|
||||||
|
output := github_flavored_markdown.Markdown([]byte(input))
|
||||||
|
return string(output)
|
||||||
|
}
|
66
core/models/posts/posts.go
Normal file
66
core/models/posts/posts.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package posts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Post holds information for a blog post.
|
||||||
|
type Post struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Fragment string `json:"fragment"`
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Privacy string `json:"privacy"`
|
||||||
|
Sticky bool `json:"sticky"`
|
||||||
|
EnableComments bool `json:"enableComments"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a blank post with sensible defaults.
|
||||||
|
func New() *Post {
|
||||||
|
return &Post{
|
||||||
|
ContentType: "markdown",
|
||||||
|
Privacy: "public",
|
||||||
|
EnableComments: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadForm populates the post from form values.
|
||||||
|
func (p *Post) LoadForm(r *http.Request) {
|
||||||
|
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||||
|
|
||||||
|
p.ID = id
|
||||||
|
p.Title = r.FormValue("title")
|
||||||
|
p.Fragment = r.FormValue("fragment")
|
||||||
|
p.ContentType = r.FormValue("content-type")
|
||||||
|
p.Body = r.FormValue("body")
|
||||||
|
p.Privacy = r.FormValue("privacy")
|
||||||
|
p.Sticky = r.FormValue("sticky") == "true"
|
||||||
|
p.EnableComments = r.FormValue("enable-comments") == "true"
|
||||||
|
|
||||||
|
// Ingest the tags.
|
||||||
|
tags := strings.Split(r.FormValue("tags"), ",")
|
||||||
|
p.Tags = []string{}
|
||||||
|
for _, tag := range tags {
|
||||||
|
p.Tags = append(p.Tags, strings.TrimSpace(tag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes sure the required fields are all present.
|
||||||
|
func (p *Post) Validate() error {
|
||||||
|
if p.Title == "" {
|
||||||
|
return errors.New("title is required")
|
||||||
|
}
|
||||||
|
if p.ContentType != "markdown" && p.ContentType != "markdown+html" &&
|
||||||
|
p.ContentType != "html" {
|
||||||
|
return errors.New("invalid setting for ContentType")
|
||||||
|
}
|
||||||
|
if p.Privacy != "public" && p.Privacy != "draft" && p.Privacy != "private" {
|
||||||
|
return errors.New("invalid setting for Privacy")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,9 +1,29 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Flash adds a flash message to the user's session.
|
||||||
|
func (b *Blog) Flash(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
|
||||||
|
session := b.Session(r)
|
||||||
|
session.AddFlash(fmt.Sprintf(message, args...))
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashAndRedirect flashes and redirects in one go.
|
||||||
|
func (b *Blog) FlashAndRedirect(w http.ResponseWriter, r *http.Request, location, message string, args ...interface{}) {
|
||||||
|
b.Flash(w, r, message, args...)
|
||||||
|
b.Redirect(w, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlashAndReload flashes and sends a redirect to the same path.
|
||||||
|
func (b *Blog) FlashAndReload(w http.ResponseWriter, r *http.Request, message string, args ...interface{}) {
|
||||||
|
b.Flash(w, r, message, args...)
|
||||||
|
b.Redirect(w, r.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect sends an HTTP redirect response.
|
// Redirect sends an HTTP redirect response.
|
||||||
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
||||||
log.Error("Redirect: %s", location)
|
log.Error("Redirect: %s", location)
|
||||||
|
|
|
@ -23,7 +23,7 @@ type Vars struct {
|
||||||
|
|
||||||
// Common template variables.
|
// Common template variables.
|
||||||
Message string
|
Message string
|
||||||
Flash string
|
Flashes []string
|
||||||
Error error
|
Error error
|
||||||
Data map[interface{}]interface{}
|
Data map[interface{}]interface{}
|
||||||
Form forms.Form
|
Form forms.Form
|
||||||
|
@ -44,7 +44,7 @@ func NewVars(data ...map[interface{}]interface{}) *Vars {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaults combines template variables with default, globally available vars.
|
// LoadDefaults combines template variables with default, globally available vars.
|
||||||
func (v *Vars) LoadDefaults(r *http.Request) {
|
func (v *Vars) LoadDefaults(b *Blog, w http.ResponseWriter, r *http.Request) {
|
||||||
// Get the site settings.
|
// Get the site settings.
|
||||||
s, err := settings.Load()
|
s, err := settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -57,6 +57,16 @@ func (v *Vars) LoadDefaults(r *http.Request) {
|
||||||
v.Title = s.Site.Title
|
v.Title = s.Site.Title
|
||||||
v.Path = r.URL.Path
|
v.Path = r.URL.Path
|
||||||
|
|
||||||
|
// Add any flashed messages from the endpoint controllers.
|
||||||
|
session := b.Session(r)
|
||||||
|
if flashes := session.Flashes(); len(flashes) > 0 {
|
||||||
|
for _, flash := range flashes {
|
||||||
|
_ = flash
|
||||||
|
v.Flashes = append(v.Flashes, flash.(string))
|
||||||
|
}
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
||||||
if user.ID > 0 {
|
if user.ID > 0 {
|
||||||
|
@ -68,7 +78,7 @@ func (v *Vars) LoadDefaults(r *http.Request) {
|
||||||
|
|
||||||
// TemplateVars is an interface that describes the template variable struct.
|
// TemplateVars is an interface that describes the template variable struct.
|
||||||
type TemplateVars interface {
|
type TemplateVars interface {
|
||||||
LoadDefaults(*http.Request)
|
LoadDefaults(*Blog, http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTemplate responds with an HTML template.
|
// RenderTemplate responds with an HTML template.
|
||||||
|
@ -87,9 +97,15 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Useful template functions.
|
||||||
|
log.Error("HERE!!!")
|
||||||
|
t := template.New(filepath.Absolute).Funcs(template.FuncMap{
|
||||||
|
"StringsJoin": strings.Join,
|
||||||
|
})
|
||||||
|
|
||||||
// Parse the template files. The layout comes first because it's the wrapper
|
// Parse the template files. The layout comes first because it's the wrapper
|
||||||
// and allows the filepath template to set the page title.
|
// and allows the filepath template to set the page title.
|
||||||
t, err := template.ParseFiles(layout.Absolute, filepath.Absolute)
|
t, err = t.ParseFiles(layout.Absolute, filepath.Absolute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return err
|
return err
|
||||||
|
@ -99,7 +115,7 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
||||||
if vars == nil {
|
if vars == nil {
|
||||||
vars = &Vars{}
|
vars = &Vars{}
|
||||||
}
|
}
|
||||||
vars.LoadDefaults(r)
|
vars.LoadDefaults(b, w, r)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||||
err = t.ExecuteTemplate(w, "layout", vars)
|
err = t.ExecuteTemplate(w, "layout", vars)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "title" }}Untitled{{ end }}
|
{{ define "title" }}Untitled{{ end }}
|
||||||
{{ define "scripts" }}Default Scripts{{ end }}
|
{{ define "scripts" }}{{ end }}
|
||||||
|
|
||||||
{{ define "layout" }}
|
{{ define "layout" }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -59,9 +59,9 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .Flash }}
|
{{ range .Flashes }}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
{{ .Flash }}
|
{{ . }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="text/javascript" src="/js/vue.min.js"></script>
|
<script type="text/javascript" src="/js/bootstrap.min.js"></script>
|
||||||
{{ template "scripts" or "" }}
|
{{ template "scripts" or "" }}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,45 +1,16 @@
|
||||||
{{ define "title" }}Website Settings{{ end }}
|
{{ define "title" }}Website Settings{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form action="/admin/settings" method="POST">
|
<form action="/admin/settings" method="POST">
|
||||||
<div id="settings-app" class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
|
||||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#site"
|
|
||||||
:class="{ active: currentTab === 'site'}"
|
|
||||||
v-on:click="currentTab = 'site'">
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<!-- <li class="nav-item">
|
|
||||||
<a class="nav-link" href="#db"
|
|
||||||
:class="{ active: currentTab === 'db'}"
|
|
||||||
v-on:click="currentTab = 'db'">
|
|
||||||
Database
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#security"
|
|
||||||
:class="{ active: currentTab === 'security'}"
|
|
||||||
v-on:click="currentTab = 'security'">
|
|
||||||
Security
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">Hello</a>
|
|
||||||
</li> -->
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ with .Data.s }}
|
{{ with .Data.s }}
|
||||||
<div class="card-body" v-if="currentTab === 'site'">
|
<div class="card-body">
|
||||||
<h3>The Basics</h3>
|
<h3>The Basics</h3>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Title</label>
|
<label for="title">Title</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="title" id="title"
|
name="title"
|
||||||
value="{{ .Site.Title }}"
|
value="{{ .Site.Title }}"
|
||||||
placeholder="Website Title">
|
placeholder="Website Title">
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,9 +18,9 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="admin-email">Admin Email</label>
|
<label for="admin-email">Admin Email</label>
|
||||||
<small class="text-muted">For getting notifications about comments, etc.</small>
|
<small class="text-muted">For getting notifications about comments, etc.</small>
|
||||||
<input type="email"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="admin-email" id="admin-email"
|
name="admin-email"
|
||||||
value="{{ .Site.AdminEmail }}"
|
value="{{ .Site.AdminEmail }}"
|
||||||
placeholder="name@domain.com">
|
placeholder="name@domain.com">
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,22 +43,11 @@
|
||||||
Enable Redis
|
Enable Redis
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="redis-prefix">Key Prefix</label>
|
|
||||||
<small class="text-muted">(optional)</small>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="redis-prefix" id="redis-prefix"
|
|
||||||
value="{{ .Redis.Prefix }}"
|
|
||||||
placeholder="blog:">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="redis-host">Redis Host</label>
|
<label for="redis-host">Redis Host</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="redis-host" id="redis-host"
|
name="redis-host"
|
||||||
value="{{ .Redis.Host }}"
|
value="{{ .Redis.Host }}"
|
||||||
placeholder="localhost">
|
placeholder="localhost">
|
||||||
</div>
|
</div>
|
||||||
|
@ -95,7 +55,7 @@
|
||||||
<label for="redis-port">Port</label>
|
<label for="redis-port">Port</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="redis-port" id="redis-port"
|
name="redis-port"
|
||||||
value="{{ .Redis.Port }}"
|
value="{{ .Redis.Port }}"
|
||||||
placeholder="6379">
|
placeholder="6379">
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,7 +64,7 @@
|
||||||
<small class="text-muted">0-15</small>
|
<small class="text-muted">0-15</small>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="redis-db" id="redis-db"
|
name="redis-db"
|
||||||
value="{{ .Redis.DB }}"
|
value="{{ .Redis.DB }}"
|
||||||
placeholder="0">
|
placeholder="0">
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,42 +73,18 @@
|
||||||
<small class="text-muted">(optional)</small>
|
<small class="text-muted">(optional)</small>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
name="redis-prefix" id="redis-prefix"
|
name="redis-prefix"
|
||||||
value="{{ .Redis.Prefix }}"
|
value="{{ .Redis.Prefix }}"
|
||||||
placeholder="blog:">
|
placeholder="blog:">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body" v-if="currentTab === 'db'">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body" v-if="currentTab === 'security'">
|
|
||||||
<div class="form-check">
|
|
||||||
<label class="form-check-label">
|
|
||||||
<input type="checkbox"
|
|
||||||
class="form-check-input"
|
|
||||||
name="redis-enabled"
|
|
||||||
value="true"
|
|
||||||
{{ if .Redis.Enabled }}checked{{ end }}>
|
|
||||||
Enable Redis
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="redis-prefix">Key Prefix</label>
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||||
<small class="text-muted">(optional)</small>
|
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||||
<input type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="redis-prefix" id="redis-prefix"
|
|
||||||
value="{{ .Redis.Prefix }}"
|
|
||||||
placeholder="blog:">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ define "scripts" }}
|
|
||||||
<script type="text/javascript" src="/admin/settings.js"></script>
|
|
||||||
{{ end }}
|
|
||||||
|
|
149
root/blog/edit.gohtml
Normal file
149
root/blog/edit.gohtml
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
{{ define "title" }}Update Blog{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<form action="/blog/edit" method="POST">
|
||||||
|
{{ if .Data.preview }}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">
|
||||||
|
Preview
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{ .Data.preview }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ with .Data.post }}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h3>Update Blog</h3>
|
||||||
|
|
||||||
|
{{ . }}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="title"
|
||||||
|
value="{{ .Title }}"
|
||||||
|
placeholder="Post Title"
|
||||||
|
autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="fragment">URL Fragment</label>
|
||||||
|
<small class="text-muted">
|
||||||
|
You can leave this blank if this is a new post. It will pick a
|
||||||
|
default value based on the title.
|
||||||
|
</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="fragment"
|
||||||
|
value="{{ .Fragment }}"
|
||||||
|
placeholder="url-fragment-for-blog-entry"
|
||||||
|
autocomplete="false">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="body">Body</label>
|
||||||
|
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="content-type"
|
||||||
|
value="markdown"
|
||||||
|
{{ if eq .ContentType "markdown" }}checked{{ end }}
|
||||||
|
> Markdown
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="content-type"
|
||||||
|
value="markdown+html"
|
||||||
|
{{ if eq .ContentType "markdown+html" }}checked{{ end }}
|
||||||
|
> Markdown + HTML
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="radio"
|
||||||
|
class="form-check-input"
|
||||||
|
name="content-type"
|
||||||
|
value="html"
|
||||||
|
{{ if eq .ContentType "html" }}checked{{ end }}
|
||||||
|
> Raw HTML
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea class="form-control"
|
||||||
|
cols="80"
|
||||||
|
rows="12"
|
||||||
|
name="body"
|
||||||
|
placeholder="Post body goes here">{{ .Body }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tags">Tags</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="tags"
|
||||||
|
placeholder="Comma, Separated, List"
|
||||||
|
value="{{ StringsJoin .Tags ", " }}"
|
||||||
|
autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="privacy">Privacy</label>
|
||||||
|
<select name="privacy" class="form-control">
|
||||||
|
<option value="public"{{ if eq .Privacy "public" }} selected{{ end }}>Public: everybody can see this post</option>
|
||||||
|
<option value="private"{{ if eq .Privacy "private" }} selected{{ end }}>Private: only site admins can see this post</option>
|
||||||
|
<option value="draft"{{ if eq .Privacy "draft" }} selected{{ end }}>Draft: don't show this post on the blog anywhere</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Options</label>
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-label"
|
||||||
|
name="sticky"
|
||||||
|
value="true"
|
||||||
|
{{ if .Sticky }}checked{{ end }}
|
||||||
|
> Make this post sticky (always on top)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-label"
|
||||||
|
name="enable-comments"
|
||||||
|
value="true"
|
||||||
|
{{ if .EnableComments }}checked{{ end }}
|
||||||
|
> Enable comments on this post
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-success"
|
||||||
|
name="submit"
|
||||||
|
value="preview">
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
name="submit"
|
||||||
|
value="post">
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
|
@ -30,6 +30,12 @@ h6, .h6 {
|
||||||
.form-group label {
|
.form-group label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
label.form-check-label {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Top nav
|
* Top nav
|
||||||
|
|
Loading…
Reference in New Issue
Block a user