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"
|
"github.com/kirsle/blog/internal/controllers/authctl"
|
||||||
commentctl "github.com/kirsle/blog/internal/controllers/comments"
|
commentctl "github.com/kirsle/blog/internal/controllers/comments"
|
||||||
"github.com/kirsle/blog/internal/controllers/contact"
|
"github.com/kirsle/blog/internal/controllers/contact"
|
||||||
|
eventctl "github.com/kirsle/blog/internal/controllers/events"
|
||||||
postctl "github.com/kirsle/blog/internal/controllers/posts"
|
postctl "github.com/kirsle/blog/internal/controllers/posts"
|
||||||
"github.com/kirsle/blog/internal/controllers/setup"
|
"github.com/kirsle/blog/internal/controllers/setup"
|
||||||
"github.com/kirsle/blog/internal/log"
|
"github.com/kirsle/blog/internal/log"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"github.com/kirsle/blog/jsondb/caches/null"
|
"github.com/kirsle/blog/jsondb/caches/null"
|
||||||
"github.com/kirsle/blog/jsondb/caches/redis"
|
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
|
"github.com/kirsle/blog/models/events"
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
"github.com/kirsle/blog/models/users"
|
"github.com/kirsle/blog/models/users"
|
||||||
|
@ -88,6 +90,7 @@ func (b *Blog) Configure() {
|
||||||
posts.DB = b.DB
|
posts.DB = b.DB
|
||||||
users.DB = b.DB
|
users.DB = b.DB
|
||||||
comments.DB = b.DB
|
comments.DB = b.DB
|
||||||
|
events.DB = b.DB
|
||||||
|
|
||||||
// Redis cache?
|
// Redis cache?
|
||||||
if config.Redis.Enabled {
|
if config.Redis.Enabled {
|
||||||
|
@ -120,6 +123,7 @@ func (b *Blog) SetupHTTP() {
|
||||||
contact.Register(r)
|
contact.Register(r)
|
||||||
postctl.Register(r, b.MustLogin)
|
postctl.Register(r, b.MustLogin)
|
||||||
commentctl.Register(r)
|
commentctl.Register(r)
|
||||||
|
eventctl.Register(r, b.MustLogin)
|
||||||
|
|
||||||
// GitHub Flavored Markdown CSS.
|
// GitHub Flavored Markdown CSS.
|
||||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
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"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/internal/markdown"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Funcs is a global funcmap that the blog can hook its internal
|
// Funcs is a global funcmap that the blog can hook its internal
|
||||||
// methods onto.
|
// methods onto.
|
||||||
var Funcs = template.FuncMap{
|
var Funcs = template.FuncMap{
|
||||||
"StringsJoin": strings.Join,
|
"StringsJoin": strings.Join,
|
||||||
|
"NewlinesToSpace": func(text string) string {
|
||||||
|
return strings.Replace(
|
||||||
|
strings.Replace(text, "\n", " ", -1),
|
||||||
|
"\r", "", -1,
|
||||||
|
)
|
||||||
|
},
|
||||||
"Now": time.Now,
|
"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.
|
// Delete a blog's entry from the index.
|
||||||
func (idx *Index) Delete(p *Post) error {
|
func (idx *Index) Delete(p *Post) error {
|
||||||
delete(idx.Posts, p.ID)
|
delete(idx.Posts, p.ID)
|
||||||
|
delete(idx.Fragments, p.Fragment)
|
||||||
return DB.Commit("blog/index", idx)
|
return DB.Commit("blog/index", idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/admin/settings">App Settings</a></li>
|
<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="/blog/edit">Post Blog Entry</a></li>
|
||||||
<li><a href="/admin/editor">Page Editor</a></li>
|
<li><a href="/admin/editor">Page Editor</a></li>
|
||||||
<li><a href="/admin/users">User Management</a></li>
|
<li><a href="/admin/users">User Management</a></li>
|
||||||
|
|
|
@ -29,3 +29,9 @@ a.blog-title {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
margin-bottom: 1rem;
|
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