Initial events UI - Creation, editing, viewing
This commit is contained in:
parent
a1c84fa1e9
commit
765e80b64d
4
blog.go
4
blog.go
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/kirsle/blog/internal/controllers/authctl"
|
||||
commentctl "github.com/kirsle/blog/internal/controllers/comments"
|
||||
"github.com/kirsle/blog/internal/controllers/contact"
|
||||
eventctl "github.com/kirsle/blog/internal/controllers/events"
|
||||
postctl "github.com/kirsle/blog/internal/controllers/posts"
|
||||
"github.com/kirsle/blog/internal/controllers/setup"
|
||||
"github.com/kirsle/blog/internal/log"
|
||||
|
@ -25,6 +26,7 @@ import (
|
|||
"github.com/kirsle/blog/jsondb/caches/null"
|
||||
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||
"github.com/kirsle/blog/models/comments"
|
||||
"github.com/kirsle/blog/models/events"
|
||||
"github.com/kirsle/blog/models/posts"
|
||||
"github.com/kirsle/blog/models/settings"
|
||||
"github.com/kirsle/blog/models/users"
|
||||
|
@ -88,6 +90,7 @@ func (b *Blog) Configure() {
|
|||
posts.DB = b.DB
|
||||
users.DB = b.DB
|
||||
comments.DB = b.DB
|
||||
events.DB = b.DB
|
||||
|
||||
// Redis cache?
|
||||
if config.Redis.Enabled {
|
||||
|
@ -120,6 +123,7 @@ func (b *Blog) SetupHTTP() {
|
|||
contact.Register(r)
|
||||
postctl.Register(r, b.MustLogin)
|
||||
commentctl.Register(r)
|
||||
eventctl.Register(r, b.MustLogin)
|
||||
|
||||
// GitHub Flavored Markdown CSS.
|
||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||
|
|
60
internal/controllers/events/edit.go
Normal file
60
internal/controllers/events/edit.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/kirsle/blog/internal/markdown"
|
||||
"github.com/kirsle/blog/internal/render"
|
||||
"github.com/kirsle/blog/internal/responses"
|
||||
"github.com/kirsle/blog/models/events"
|
||||
)
|
||||
|
||||
func editHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v := map[string]interface{}{
|
||||
"preview": "",
|
||||
}
|
||||
var ev *events.Event
|
||||
|
||||
// Are we editing an existing event?
|
||||
if idStr := r.FormValue("id"); idStr != "" {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err == nil {
|
||||
ev, err = events.Load(id)
|
||||
if err != nil {
|
||||
responses.Flash(w, r, "That event ID was not found")
|
||||
ev = events.New()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ev = events.New()
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
// Parse from form values.
|
||||
ev.ParseForm(r)
|
||||
|
||||
// Previewing, or submitting?
|
||||
switch r.FormValue("submit") {
|
||||
case "preview":
|
||||
v["preview"] = template.HTML(markdown.RenderTrustedMarkdown(ev.Description))
|
||||
case "save":
|
||||
if err := ev.Validate(); err != nil {
|
||||
responses.Flash(w, r, "Error: %s", err.Error())
|
||||
} else {
|
||||
err = ev.Save()
|
||||
|
||||
if err != nil {
|
||||
responses.Flash(w, r, "Error: %s", err.Error())
|
||||
} else {
|
||||
responses.Flash(w, r, "Event created!")
|
||||
responses.Redirect(w, "/e/"+ev.Fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v["event"] = ev
|
||||
render.Template(w, r, "events/edit", v)
|
||||
}
|
75
internal/controllers/events/events.go
Normal file
75
internal/controllers/events/events.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/internal/log"
|
||||
"github.com/kirsle/blog/internal/middleware/auth"
|
||||
"github.com/kirsle/blog/internal/render"
|
||||
"github.com/kirsle/blog/internal/responses"
|
||||
"github.com/kirsle/blog/models/events"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
// Register the blog routes to the app.
|
||||
func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||
// Login-required routers.
|
||||
loginRouter := mux.NewRouter()
|
||||
loginRouter.HandleFunc("/e/admin/edit", editHandler)
|
||||
loginRouter.HandleFunc("/e/admin/invite", inviteHandler)
|
||||
loginRouter.HandleFunc("/e/admin/", indexHandler)
|
||||
r.PathPrefix("/e/admin").Handler(
|
||||
negroni.New(
|
||||
negroni.HandlerFunc(auth.LoginRequired(loginError)),
|
||||
negroni.Wrap(loginRouter),
|
||||
),
|
||||
)
|
||||
|
||||
// Public routes
|
||||
r.HandleFunc("/e/{fragment}", viewHandler)
|
||||
}
|
||||
|
||||
// Admin index to view all events.
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
result := []*events.Event{}
|
||||
docs, _ := events.DB.List("events/by-id")
|
||||
for _, doc := range docs {
|
||||
ev := &events.Event{}
|
||||
err := events.DB.Get(doc, &ev)
|
||||
if err != nil {
|
||||
log.Error("error reading %s: %s", doc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, ev)
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(events.ByDate(result)))
|
||||
|
||||
render.Template(w, r, "events/index", map[string]interface{}{
|
||||
"events": result,
|
||||
})
|
||||
}
|
||||
|
||||
// User handler to view a single event page.
|
||||
func viewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
fragment, ok := params["fragment"]
|
||||
if !ok {
|
||||
responses.NotFound(w, r, "Not Found")
|
||||
return
|
||||
}
|
||||
|
||||
event, err := events.LoadFragment(fragment)
|
||||
if err != nil {
|
||||
responses.FlashAndRedirect(w, r, "/", "Event Not Found")
|
||||
return
|
||||
}
|
||||
|
||||
v := map[string]interface{}{
|
||||
"event": event,
|
||||
}
|
||||
render.Template(w, r, "events/view", v)
|
||||
}
|
30
internal/controllers/events/invite.go
Normal file
30
internal/controllers/events/invite.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/kirsle/blog/internal/render"
|
||||
"github.com/kirsle/blog/internal/responses"
|
||||
"github.com/kirsle/blog/models/events"
|
||||
)
|
||||
|
||||
func inviteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v := map[string]interface{}{
|
||||
"preview": "",
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(r.FormValue("id"))
|
||||
if err != nil {
|
||||
responses.FlashAndRedirect(w, r, "/e/admin/", "Invalid ID")
|
||||
return
|
||||
}
|
||||
event, err := events.Load(id)
|
||||
if err != nil {
|
||||
responses.FlashAndRedirect(w, r, "/e/admin/", "Can't load event: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
v["event"] = event
|
||||
render.Template(w, r, "events/invite", v)
|
||||
}
|
|
@ -4,11 +4,22 @@ import (
|
|||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kirsle/blog/internal/markdown"
|
||||
)
|
||||
|
||||
// Funcs is a global funcmap that the blog can hook its internal
|
||||
// methods onto.
|
||||
var Funcs = template.FuncMap{
|
||||
"StringsJoin": strings.Join,
|
||||
"NewlinesToSpace": func(text string) string {
|
||||
return strings.Replace(
|
||||
strings.Replace(text, "\n", " ", -1),
|
||||
"\r", "", -1,
|
||||
)
|
||||
},
|
||||
"Now": time.Now,
|
||||
"TrustedMarkdown": func(text string) template.HTML {
|
||||
return template.HTML(markdown.RenderTrustedMarkdown(text))
|
||||
},
|
||||
}
|
||||
|
|
243
models/events/events.go
Normal file
243
models/events/events.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kirsle/blog/jsondb"
|
||||
"github.com/kirsle/golog"
|
||||
)
|
||||
|
||||
// DB is a reference to the parent app's JsonDB object.
|
||||
var DB *jsondb.DB
|
||||
|
||||
var log *golog.Logger
|
||||
|
||||
func init() {
|
||||
log = golog.GetLogger("blog")
|
||||
}
|
||||
|
||||
// Event holds information about events.
|
||||
type Event struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Fragment string `json:"fragment"`
|
||||
Description string `json:"description"`
|
||||
Location string `json:"location"`
|
||||
CoverPhoto string `json:"coverPhoto"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
AllDay bool `json:"allDay"`
|
||||
OpenSignup bool `json:"openSignup"`
|
||||
RSVP []RSVP `json:"rsvp"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// RSVP tracks invitations and confirmations to events.
|
||||
type RSVP struct {
|
||||
// If the user was invited by an admin, they will have a ContactID and
|
||||
// not much else. Users who signed up themselves from an OpenSignup event
|
||||
// will have the metadata filled in instead.
|
||||
ContactID int `json:"contactId"`
|
||||
Notified bool `json:"notified"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Status string `json:"status,omitempty"` // invited, going, maybe, not going
|
||||
Email string `json:"email,omitempty"`
|
||||
SMS string `json:"sms,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// New creates a blank event with sensible defaults.
|
||||
func New() *Event {
|
||||
return &Event{
|
||||
StartTime: time.Now().UTC(),
|
||||
EndTime: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ParseForm populates the event from form values.
|
||||
func (ev *Event) ParseForm(r *http.Request) {
|
||||
id, _ := strconv.Atoi(r.FormValue("id"))
|
||||
|
||||
ev.ID = id
|
||||
ev.Title = r.FormValue("title")
|
||||
ev.Fragment = r.FormValue("fragment")
|
||||
ev.Description = r.FormValue("description")
|
||||
ev.Location = r.FormValue("location")
|
||||
ev.AllDay = r.FormValue("all_day") == "true"
|
||||
ev.OpenSignup = r.FormValue("open_signup") == "true"
|
||||
|
||||
startTime, err := parseDateTime(r, "start_date", "start_time")
|
||||
ev.StartTime = startTime
|
||||
if err != nil {
|
||||
log.Error("startTime parse error: %s", err)
|
||||
}
|
||||
|
||||
endTime, err := parseDateTime(r, "end_date", "end_time")
|
||||
ev.EndTime = endTime
|
||||
if err != nil {
|
||||
log.Error("endTime parse error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseDateTime parses separate date + time fields into a single time.Time.
|
||||
func parseDateTime(r *http.Request, dateField, timeField string) (time.Time, error) {
|
||||
dateValue := r.FormValue(dateField)
|
||||
timeValue := r.FormValue(timeField)
|
||||
|
||||
if dateValue != "" && timeValue != "" {
|
||||
datetime, err := time.Parse("2006-01-02 15:04", dateValue+" "+timeValue)
|
||||
return datetime, err
|
||||
} else if dateValue != "" {
|
||||
datetime, err := time.Parse("2006-01-02", dateValue)
|
||||
return datetime, err
|
||||
} else {
|
||||
return time.Time{}, errors.New("no date/times given")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate makes sure the required fields are all present.
|
||||
func (ev *Event) Validate() error {
|
||||
if ev.Title == "" {
|
||||
return errors.New("title is required")
|
||||
} else if ev.Description == "" {
|
||||
return errors.New("description is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load an event by its ID.
|
||||
func Load(id int) (*Event, error) {
|
||||
ev := &Event{}
|
||||
err := DB.Get(fmt.Sprintf("events/by-id/%d", id), &ev)
|
||||
return ev, err
|
||||
}
|
||||
|
||||
// LoadFragment loads an event by its URL fragment.
|
||||
func LoadFragment(fragment string) (*Event, error) {
|
||||
idx, err := GetIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id, ok := idx.Fragments[fragment]; ok {
|
||||
ev, err := Load(id)
|
||||
return ev, err
|
||||
}
|
||||
|
||||
return nil, errors.New("fragment not found")
|
||||
}
|
||||
|
||||
// Save the event.
|
||||
func (ev *Event) Save() error {
|
||||
// Editing an existing event?
|
||||
if ev.ID == 0 {
|
||||
ev.ID = nextID()
|
||||
}
|
||||
|
||||
// Generate a URL fragment if needed.
|
||||
if ev.Fragment == "" {
|
||||
fragment := strings.ToLower(ev.Title)
|
||||
fragment = regexp.MustCompile(`[^A-Za-z0-9]+`).ReplaceAllString(fragment, "-")
|
||||
if strings.Contains(fragment, "--") {
|
||||
log.Error("Generated event fragment '%s' contains double dashes still!", fragment)
|
||||
}
|
||||
ev.Fragment = strings.Trim(fragment, "-")
|
||||
|
||||
// If still no fragment, make one based on the post ID.
|
||||
if ev.Fragment == "" {
|
||||
ev.Fragment = fmt.Sprintf("event-%d", ev.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the URL fragment is unique!
|
||||
if len(ev.Fragment) > 0 {
|
||||
if exist, err := LoadFragment(ev.Fragment); err == nil && exist.ID != ev.ID {
|
||||
var resolved bool
|
||||
for i := 1; i <= 100; i++ {
|
||||
fragment := fmt.Sprintf("%s-%d", ev.Fragment, i)
|
||||
_, err := LoadFragment(fragment)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ev.Fragment = fragment
|
||||
resolved = true
|
||||
break
|
||||
}
|
||||
|
||||
if !resolved {
|
||||
return fmt.Errorf("failed to generate a unique URL fragment for '%s' after 100 attempts", ev.Fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dates & times.
|
||||
if ev.Created.IsZero() {
|
||||
ev.Created = time.Now().UTC()
|
||||
}
|
||||
if ev.Updated.IsZero() {
|
||||
ev.Updated = ev.Created
|
||||
}
|
||||
|
||||
// Write the event.
|
||||
DB.Commit(fmt.Sprintf("events/by-id/%d", ev.ID), ev)
|
||||
|
||||
// Update the index cache.
|
||||
err := UpdateIndex(ev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateIndex() error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete an event.
|
||||
func (ev *Event) Delete() error {
|
||||
if ev.ID == 0 {
|
||||
return errors.New("event has no ID")
|
||||
}
|
||||
|
||||
// Delete the DB files.
|
||||
DB.Delete(fmt.Sprintf("events/by-id/%d", ev.ID))
|
||||
|
||||
// Remove it from the index.
|
||||
idx, err := GetIndex()
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetIndex error: %v", err)
|
||||
}
|
||||
return idx.Delete(ev)
|
||||
}
|
||||
|
||||
// getNextID gets the next blog post ID.
|
||||
func nextID() int {
|
||||
// Highest ID seen so far.
|
||||
var highest int
|
||||
|
||||
events, err := DB.List("events/by-id")
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
for _, doc := range events {
|
||||
fields := strings.Split(doc, "/")
|
||||
id, err := strconv.Atoi(fields[len(fields)-1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if id > highest {
|
||||
highest = id
|
||||
}
|
||||
}
|
||||
|
||||
// Return the highest +1
|
||||
return highest + 1
|
||||
}
|
60
models/events/index.go
Normal file
60
models/events/index.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package events
|
||||
|
||||
// Index maps URL fragments to event IDs.
|
||||
type Index struct {
|
||||
Fragments map[string]int `json:"fragments"`
|
||||
}
|
||||
|
||||
// GetIndex loads the index DB, or rebuilds it if not found.
|
||||
func GetIndex() (*Index, error) {
|
||||
if !DB.Exists("events/index") {
|
||||
index, err := RebuildIndex()
|
||||
return index, err
|
||||
}
|
||||
|
||||
idx := &Index{}
|
||||
err := DB.Get("events/index", &idx)
|
||||
return idx, err
|
||||
}
|
||||
|
||||
// RebuildIndex builds the event index from scratch.
|
||||
func RebuildIndex() (*Index, error) {
|
||||
idx := &Index{
|
||||
Fragments: map[string]int{},
|
||||
}
|
||||
|
||||
events, _ := DB.List("events/by-id")
|
||||
for _, doc := range events {
|
||||
ev := &Event{}
|
||||
err := DB.Get(doc, &ev)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx.Update(ev)
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
// UpdateIndex updates the index with an event.
|
||||
func UpdateIndex(event *Event) error {
|
||||
idx, err := GetIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return idx.Update(event)
|
||||
}
|
||||
|
||||
// Update an event in the index.
|
||||
func (idx *Index) Update(event *Event) error {
|
||||
idx.Fragments[event.Fragment] = event.ID
|
||||
return DB.Commit("events/index", idx)
|
||||
}
|
||||
|
||||
// Delete an event from the index.
|
||||
func (idx *Index) Delete(event *Event) error {
|
||||
delete(idx.Fragments, event.Fragment)
|
||||
return DB.Commit("events/index", idx)
|
||||
}
|
10
models/events/sorting.go
Normal file
10
models/events/sorting.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package events
|
||||
|
||||
// ByDate sorts events by their start time.
|
||||
type ByDate []*Event
|
||||
|
||||
func (a ByDate) Len() int { return len(a) }
|
||||
func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByDate) Less(i, j int) bool {
|
||||
return a[i].StartTime.Before(a[i].StartTime) || a[i].ID < a[j].ID
|
||||
}
|
|
@ -70,6 +70,7 @@ func (idx *Index) Update(p *Post) error {
|
|||
// Delete a blog's entry from the index.
|
||||
func (idx *Index) Delete(p *Post) error {
|
||||
delete(idx.Posts, p.ID)
|
||||
delete(idx.Fragments, p.Fragment)
|
||||
return DB.Commit("blog/index", idx)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<ul>
|
||||
<li><a href="/admin/settings">App Settings</a></li>
|
||||
<li><a href="/e/admin/">Events</a></li>
|
||||
<li><a href="/blog/edit">Post Blog Entry</a></li>
|
||||
<li><a href="/admin/editor">Page Editor</a></li>
|
||||
<li><a href="/admin/users">User Management</a></li>
|
||||
|
|
|
@ -29,3 +29,9 @@ a.blog-title {
|
|||
font-size: smaller;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Address formatting */
|
||||
address {
|
||||
white-space: pre-line;
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
138
root/events/edit.gohtml
Normal file
138
root/events/edit.gohtml
Normal file
|
@ -0,0 +1,138 @@
|
|||
{{ define "title" }}Edit Event{{ end }}
|
||||
{{ define "content" }}
|
||||
<form action="/e/admin/edit" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||
{{ if .Data.preview }}
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
Preview
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1>{{ .Data.event.Title }}</h1>
|
||||
|
||||
{{ if .Data.event.Location }}
|
||||
<address>{{ .Data.event.Location }}</address>
|
||||
{{ end }}
|
||||
|
||||
{{ .Data.preview }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ with .Data.event }}
|
||||
<input type="hidden" name="id" value="{{ or .ID "" }}">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1>Edit Event</h1>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-12">
|
||||
<label for="title">Event Title:</label>
|
||||
<input type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
class="form-control"
|
||||
value="{{ .Title }}"
|
||||
placeholder="Event Title Goes Here">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-6">
|
||||
<label for="start_date">Start Time:</label>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<input type="date"
|
||||
name="start_date"
|
||||
id="start_date"
|
||||
class="form-control"
|
||||
value="{{ .StartTime.Format "2006-01-02" }}"
|
||||
placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="time"
|
||||
name="start_time"
|
||||
id="start_time"
|
||||
class="form-control"
|
||||
value="{{ .StartTime.Format "15:04" }}"
|
||||
placeholder="HH:MM">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="end_date">End Time:</label>
|
||||
<label class="ml-4">
|
||||
<input type="checkbox"
|
||||
name="all_day"
|
||||
value="true">
|
||||
All day
|
||||
</label>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<input type="date"
|
||||
name="end_date"
|
||||
id="end_date"
|
||||
class="form-control"
|
||||
value="{{ .EndTime.Format "2006-01-02" }}"
|
||||
placeholder="YYYY-MM-DD">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="time"
|
||||
name="end_time"
|
||||
id="end_time"
|
||||
class="form-control"
|
||||
value="{{ .EndTime.Format "15:04" }}"
|
||||
placeholder="HH:MM">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12">
|
||||
<label for="location">Location:</label>
|
||||
<textarea
|
||||
name="location"
|
||||
id="location"
|
||||
class="form-control"
|
||||
cols="80"
|
||||
rows="3"
|
||||
placeholder="123 Nowhere Drive">{{ .Location }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12">
|
||||
<label for="description">Description (<a href="/markdown" target="_blank">Markdown</a> supported):</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
class="form-control text-monospace"
|
||||
cols="80"
|
||||
rows="12"
|
||||
placeholder="Come to my awesome event!">{{ .Description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-12">
|
||||
<label for="fragment">Custom URL fragment (<abbr title="a-z 0-9 - . _">URL-safe characters only</abbr>):</label>
|
||||
<input type="text"
|
||||
name="fragment"
|
||||
id="fragment"
|
||||
class="form-control"
|
||||
pattern="[A-Za-z0-9\-_.]*"
|
||||
value="{{ .Fragment }}"
|
||||
placeholder="example: spring-break-2032">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<button type="submit"
|
||||
name="submit"
|
||||
value="preview"
|
||||
class="btn btn-primary">Preview</button>
|
||||
<button type="submit"
|
||||
name="submit"
|
||||
value="save"
|
||||
class="btn btn-success">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</form>
|
||||
|
||||
{{ end }}
|
18
root/events/index.gohtml
Normal file
18
root/events/index.gohtml
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{ define "title" }}Events{{ end }}
|
||||
{{ define "content" }}
|
||||
|
||||
<h1>Events</h1>
|
||||
|
||||
<p>
|
||||
<a href="/e/admin/edit" class="btn btn-success">New Event</a>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
{{ range .Data.events }}
|
||||
<li>
|
||||
<a href="/e/{{ .Fragment }}">{{ .Title }}</a> {{ .StartTime.Format "Jan 1 2006 @ 3:04:05 PM" }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
{{ end }}
|
108
root/events/invite.gohtml
Normal file
108
root/events/invite.gohtml
Normal file
|
@ -0,0 +1,108 @@
|
|||
{{ define "title" }}Invite People to {{ .Data.event.Title }}{{ end }}
|
||||
{{ define "content" }}
|
||||
<form action="/e/admin/edit" method="POST">
|
||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||
|
||||
{{ $e := .Data.event }}
|
||||
<h1>Invite <em>{{ $e.Title }}</em></h1>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">Contact List</div>
|
||||
<div class="card-body">
|
||||
<div class="row" style="max-height: 500px; overflow: auto">
|
||||
<div class="col-6">
|
||||
<h4>Invited</h4>
|
||||
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<strong>John Doe</strong><br>
|
||||
<span class="text-muted">name@example.com</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>John Doe</strong><br>
|
||||
<span class="text-muted">name@example.com</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h4>Available</h4>
|
||||
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<label class="d-block alert alert-info">
|
||||
<input type="checkbox" name="invite" value="1">
|
||||
<strong>John Doe</strong><br>
|
||||
<span class="text-muted">name@example.com</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label class="d-block alert alert-info">
|
||||
<input type="checkbox" name="invite" value="1">
|
||||
<strong>John Doe</strong><br>
|
||||
<span class="text-muted">name@example.com</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button type="submit"
|
||||
name="action" value="send-invite"
|
||||
class="btn btn-primary">Send Invites</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Invite New People</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="first_name">First name:</label>
|
||||
<input type="text"
|
||||
name="first_name"
|
||||
id="first_name"
|
||||
class="form-control"
|
||||
placeholder="First name">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="last_name">Last name:</label>
|
||||
<input type="text"
|
||||
name="last_name"
|
||||
id="last_name"
|
||||
class="form-control"
|
||||
placeholder="Last name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="email">E-mail:</label>
|
||||
<input type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
class="form-control"
|
||||
placeholder="name@example.com">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="last_name">SMS Number:</label>
|
||||
<input type="text"
|
||||
name="sms"
|
||||
id="sms"
|
||||
class="form-control"
|
||||
placeholder="800-555-1234">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<button type="button"
|
||||
class="btn btn-primary">Send Invite</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Invited</h2>
|
||||
|
||||
To Do
|
||||
</form>
|
||||
|
||||
{{ end }}
|
51
root/events/view.gohtml
Normal file
51
root/events/view.gohtml
Normal file
|
@ -0,0 +1,51 @@
|
|||
{{ define "title" }}{{ .Data.event.Title }}{{ end }}
|
||||
{{ define "content" }}
|
||||
|
||||
{{ with .Data.event }}
|
||||
<h1>{{ .Title }}</h1>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12 col-md-8">
|
||||
{{ TrustedMarkdown .Description }}
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
{{ if .Location }}
|
||||
<h4>Location</h4>
|
||||
<address class="mb-4"><a href="https://maps.google.com/?q={{ NewlinesToSpace .Location }}" target="_blank">{{ .Location }}</a></address>
|
||||
{{ end }}
|
||||
|
||||
<h4>Time</h4>
|
||||
<abbr title="{{ .StartTime.Format "Mon Jan 2 15:04:05 2006" }}">
|
||||
{{ .StartTime.Format "January 2 @ 3:04 PM" }}
|
||||
</abbr>
|
||||
{{ if not .EndTime.IsZero }}
|
||||
to<br>
|
||||
<abbr title="{{ .EndTime.Format "Mon Jan 2 15:04:05 2006" }}">
|
||||
{{ if .AllDay }}
|
||||
{{ .EndTime.Format "January 2" }}
|
||||
{{ else }}
|
||||
{{ .EndTime.Format "January 2 @ 3:04 PM" }}
|
||||
{{ end }}
|
||||
</abbr>
|
||||
{{ end }}
|
||||
|
||||
<h4 class="mt-4">Invited</h4>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if and .LoggedIn .CurrentUser.Admin }}
|
||||
<div class="alert alert-danger">
|
||||
<a href="/e/admin/edit?id={{ .Data.event.ID }}"
|
||||
class="btn btn-primary">edit event</a>
|
||||
<a href="/e/admin/invite?id={{ .Data.event.ID }}"
|
||||
class="btn btn-success">invite people</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<h2 id="comments" class="mt-4">Comments</h2>
|
||||
|
||||
{{ $idStr := printf "%d" .Data.event.ID }}
|
||||
{{ RenderComments .Request .Data.event.Title "event" $idStr }}
|
||||
|
||||
{{ end }}
|
Loading…
Reference in New Issue
Block a user