Skip to content

Commit

Permalink
refactor admin edit with htmx
Browse files Browse the repository at this point in the history
  • Loading branch information
mpoegel committed May 8, 2024
1 parent ef7491f commit 8fbcb85
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 92 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -19,7 +19,7 @@ rsvp.pizza patch -init
```

### Setup OAuth2 Server
Configure an OAuth2 server such as Keycloak. Create a client application to get the Client ID and Client Server.
Configure a Keycloak OAuth2 server. Create a client application to get the Client ID and Client Server.

### Install the package
1. Download the latest version
Expand Down
12 changes: 7 additions & 5 deletions pkg/pizza/server.go
Expand Up @@ -118,7 +118,7 @@ func NewServer(config Config, metricsReg MetricsRegistry) (*Server, error) {
r.HandleFunc("/login/callback", s.HandleLoginCallback)
r.HandleFunc("/logout", s.HandleLogout)
r.HandleFunc("/admin", s.HandleAdmin)
r.HandleFunc("/admin/submit", s.HandleAdminSubmit)
r.HandleFunc("/admin/edit", s.HandleAdminEdit)

return &s, nil
}
Expand Down Expand Up @@ -154,10 +154,12 @@ func (s *Server) WatchCalendar(period time.Duration) {
}

type IndexFridayData struct {
Date string
ID int64
Guests []string
Active bool
Date string
ID int64
Guests []string
Active bool
Group string
Details string
}

type PageData struct {
Expand Down
134 changes: 66 additions & 68 deletions pkg/pizza/server_admin.go
@@ -1,6 +1,7 @@
package pizza

import (
"fmt"
"net/http"
"path"
"strconv"
Expand All @@ -10,7 +11,7 @@ import (
zap "go.uber.org/zap"
)

const futureFridayLimit = 90
const futureFridayLimit = 30

func getFutureFridays() []time.Time {
dates := make([]time.Time, 0)
Expand All @@ -29,12 +30,6 @@ func getFutureFridays() []time.Time {
}

func (s *Server) HandleAdmin(w http.ResponseWriter, r *http.Request) {
plate, err := template.ParseFiles(path.Join(s.config.StaticDir, "html/admin.html"))
if err != nil {
Log.Error("template submit failure", zap.Error(err))
s.Handle500(w, r)
return
}

claims, ok := s.authenticateRequest(r)
if !ok {
Expand All @@ -47,6 +42,18 @@ func (s *Server) HandleAdmin(w http.ResponseWriter, r *http.Request) {
return
}

plate, err := template.ParseFiles(path.Join(s.config.StaticDir, "html/admin.html"))
if err != nil {
Log.Error("template submit failure", zap.Error(err))
s.Handle500(w, r)
return
}
if _, err = plate.ParseGlob(path.Join(s.config.StaticDir, "html/snippets/*.html")); err != nil {
Log.Error("template snippets submit failure", zap.Error(err))
s.Handle500(w, r)
return
}

data := PageData{
Name: claims.GivenName,
}
Expand All @@ -62,10 +69,12 @@ func (s *Server) HandleAdmin(w http.ResponseWriter, r *http.Request) {
data.FridayTimes = make([]IndexFridayData, 0)
for _, friday := range allFridays {
f := IndexFridayData{
Date: friday.Format(time.RFC822),
ID: friday.Unix(),
Guests: nil,
Active: false,
Date: friday.Format(time.RFC822),
ID: friday.Unix(),
Guests: nil,
Active: false,
Group: "",
Details: "",
}
if fridayIndex < len(setFridays) && friday.Equal(setFridays[fridayIndex].Date) {
f.Active = true
Expand All @@ -92,75 +101,64 @@ func (s *Server) HandleAdmin(w http.ResponseWriter, r *http.Request) {
data.FridayTimes = append(data.FridayTimes, f)
}

if err = plate.Execute(w, data); err != nil {
if err = plate.ExecuteTemplate(w, "Admin", data); err != nil {
Log.Error("template execution failure", zap.Error(err))
s.Handle500(w, r)
return
}
}

func (s *Server) HandleAdminSubmit(w http.ResponseWriter, r *http.Request) {
claims, ok := s.authenticateRequest(r)
if !ok {
http.Redirect(w, r, "/login", http.StatusFound)
func getToast(msg string) []byte {
return []byte(fmt.Sprintf(`<span class="toast">%s</span>`, msg))
}

func (s *Server) HandleAdminEdit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
Log.Error("form parse failure on admin edit", zap.Error(err))
w.Write(getToast("bad request"))
return
}
if !claims.HasRole("pizza_host") {
s.Handle4xx(w, r)
form := r.URL.Query()
isActive := form["active"]
group := r.Form["group"]
details := r.Form["details"]
dates := r.Form["date"]
needsActivation := len(r.Form["activate"]) > 0

Log.Info("admin edit", zap.Strings("dates", dates),
zap.Strings("group", group),
zap.Strings("details", details),
zap.Strings("isActive", isActive),
zap.Bool("needsActivation", needsActivation))

loc, _ := time.LoadLocation("America/New_York")
num, err := strconv.ParseInt(dates[0], 10, 64)
if err != nil {
Log.Error("failed parsing date int from rsvp form", zap.String("date", dates[0]))
w.Write(getToast("parse error"))
return
}
f := time.Unix(num, 0).In(loc)

form := r.URL.Query()
dates := form["date"]
loc, _ := time.LoadLocation("America/New_York")
dateIndex := 0
allFridays := getFutureFridays()
for _, d := range allFridays {
f := time.Now().AddDate(1, 0, 0).In(loc)
if dateIndex < len(dates) {
num, err := strconv.ParseInt(dates[dateIndex], 10, 64)
if err != nil {
Log.Error("failed parsing date int from rsvp form", zap.String("date", dates[dateIndex]))
s.Handle500(w, r)
return
}
f = time.Unix(num, 0).In(loc)
if exists, err := s.store.DoesFridayExist(f); err != nil {
Log.Error("failed check friday", zap.Error(err))
} else if needsActivation && !exists {
err := s.store.AddFriday(f)
if err != nil {
Log.Error("failed to add friday", zap.Error(err))
} else {
Log.Info("added friday", zap.Time("date", f))
w.Write(getToast("added friday"))
}
if d.Equal(f) {
// friday selected, so add it
if exists, err := s.store.DoesFridayExist(f); err != nil {
Log.Error("failed check friday", zap.Error(err))
continue
} else if !exists {
err := s.store.AddFriday(f)
if err != nil {
Log.Error("failed to add friday", zap.Error(err))
} else {
Log.Info("added friday", zap.Time("date", f))
}
}
dateIndex++
} else if f.After(d) {
// friday is not selected, so remove it
// TODO warn if users have already RSVP'ed
if exists, err := s.store.DoesFridayExist(d); err != nil {
Log.Error("failed to check friday", zap.Error(err))
continue
} else if exists {
err := s.store.RemoveFriday(d)
if err != nil {
Log.Error("failed to remove friday", zap.Error(err))
} else {
Log.Info("removed friday", zap.Time("date", d))
}
// NOTE the calendar event must be deleted manually
}
} else if !needsActivation && exists {
err := s.store.RemoveFriday(f)
if err != nil {
Log.Error("failed to remove friday", zap.Error(err))
} else {
Log.Info("removed friday", zap.Time("date", f))
w.Write(getToast("removed friday"))
}
// else {
// f.Before(d) == true
// do nothing
// }
} else {
w.Write(getToast("no changes"))
}

http.Redirect(w, r, "/admin", http.StatusFound)
}
1 change: 1 addition & 0 deletions pkg/pizza/store.go
Expand Up @@ -17,6 +17,7 @@ type Accessor interface {
ListFridays() ([]Friday, error)
RemoveFriend(email string) error
RemoveFriday(date time.Time) error
// UpdateFriday(friday Friday) error
}

type Friend struct {
Expand Down
14 changes: 13 additions & 1 deletion static/css/index.css
Expand Up @@ -53,6 +53,18 @@ a {
justify-content: flex-end;
}

.toast.htmx-settling {
opacity: 100;
}

.toast {
opacity: 0;
transition: opacity 3s ease-out;
padding: 2px;
flex-grow: 2;
color: brown;
}

@media (max-width: 600px) {
body {
font-size: 1.5em;
Expand Down Expand Up @@ -83,4 +95,4 @@ a {
#friday-table {
width: 50%;
}
}
}
23 changes: 9 additions & 14 deletions static/html/admin.html
@@ -1,8 +1,10 @@
{{define "Admin"}}
<!DOCTYPE html>
<html>

<head>
<link rel="stylesheet" href="/static/css/index.css">
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>

Expand All @@ -11,25 +13,18 @@ <h2>Pizza Admin</h2>

<p>Hi {{.Name}}</p>

<form method="get" action="/admin/submit">
<table id="friday-table">
{{range .FridayTimes}}
{{if .Active}}
<input type="checkbox" id="{{.Date}}" name="date" value="{{.ID}}" checked>
{{else}}
<input type="checkbox" id="{{.Date}}" name="date" value="{{.ID}}">
<tr>
{{template "AdminFridayView" .}}
</tr>
{{end}}
<label for="{{.Date}}">{{.Date}}</label><br>
<div class="guestLevel">{{range .Guests}}<span class="guest" title="{{.}}">&nbsp;</span>{{end}}<br></div>
{{end}}
<br>
<div id="submit">
<input type="submit" value="Update">
</div>
</form>
</table>

<br><br>
<a href="/">pizza</a> |
<a href="/logout">logout</a>
</body>

</html>
</html>
{{end}}
19 changes: 19 additions & 0 deletions static/html/snippets/admin_friday_view.html
@@ -0,0 +1,19 @@
{{define "AdminFridayView"}}
<td class="friday-time">
{{if .Active}}
<input type="checkbox" id="{{.Date}}" name="activate" value="activate" checked>
{{else}}
<input type="checkbox" id="{{.Date}}" name="activate" value="activate">
{{end}}
<label for="{{.Date}}">{{.Date}}</label><br>
<div class="guestLevel">{{range .Guests}}<span class="guest" title="{{.}}">&nbsp;</span>{{end}}<br>
</div>
<label for="group">Group</label><input name="group" value="{{.Group}}"><br>
<label for="details">Details</label><input name="details" value="{{.Details}}">
<div class="btn-rsvp">
<span class="toast"></span>
<button class="btn" hx-post="/admin/edit?date={{.ID}}&active={{.Active}}" hx-swap="outerHTML settle:3s"
hx-target="previous .toast" hx-include="closest td">Update</button>
</div>
</td>
{{end}}
2 changes: 1 addition & 1 deletion static/html/snippets/rsvp_error.html
@@ -1,3 +1,3 @@
<div>
<p>Pizza goblins are trying to steal the secret recipe.</p>
</div>
</div>
2 changes: 1 addition & 1 deletion static/html/snippets/rsvp_fail.html
@@ -1,3 +1,3 @@
<div>
<p>Sorry, no pizza for you.</p>
</div>
</div>
2 changes: 1 addition & 1 deletion static/html/snippets/rsvp_success.html
@@ -1,3 +1,3 @@
<div>
<p>You've been invited for pizza!</p>
</div>
</div>

0 comments on commit 8fbcb85

Please sign in to comment.