feat(ui): add RSVP attendance option and admin features
* Implement attendance selection in RSVP form * Add admin endpoints for managing RSVPs and guests * Update RSVP handling to include attendance status
This commit is contained in:
@@ -38,6 +38,186 @@ func (s *Server) Routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/guests", s.handleGuestList)
|
||||
mux.HandleFunc("GET /api/photos", s.handleListPhotos)
|
||||
mux.HandleFunc("POST /api/photos/upload", s.handlePhotoUpload)
|
||||
|
||||
// Admin RSVP editing. These endpoints are unauthenticated; the UI gates
|
||||
// them behind ?admin=1 only. Put the server behind auth before exposing it
|
||||
// on an untrusted network.
|
||||
mux.HandleFunc("GET /api/admin/rsvps", s.handleAdminListRSVPs)
|
||||
mux.HandleFunc("POST /api/admin/rsvps", s.handleAdminCreateRSVP)
|
||||
mux.HandleFunc("PATCH /api/admin/rsvps/{id}", s.handleAdminPatchRSVP)
|
||||
mux.HandleFunc("DELETE /api/admin/rsvps/{id}", s.handleAdminDeleteRSVP)
|
||||
mux.HandleFunc("POST /api/admin/rsvps/{id}/guests", s.handleAdminAddGuest)
|
||||
mux.HandleFunc("PATCH /api/admin/guests/{id}", s.handleAdminPatchGuest)
|
||||
mux.HandleFunc("DELETE /api/admin/guests/{id}", s.handleAdminDeleteGuest)
|
||||
}
|
||||
|
||||
func pathID(r *http.Request) (int64, bool) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
type adminGuest struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
IsChild bool `json:"isChild"`
|
||||
}
|
||||
|
||||
type adminRSVP struct {
|
||||
ID int64 `json:"id"`
|
||||
Attending bool `json:"attending"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Guests []adminGuest `json:"guests"`
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminListRSVPs(w http.ResponseWriter, r *http.Request) {
|
||||
records, err := s.store.ListRSVPs()
|
||||
if err != nil {
|
||||
log.Printf("admin: list rsvps failed: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not load RSVPs")
|
||||
return
|
||||
}
|
||||
out := make([]adminRSVP, 0, len(records))
|
||||
for _, rec := range records {
|
||||
guests := make([]adminGuest, 0, len(rec.Guests))
|
||||
for _, g := range rec.Guests {
|
||||
guests = append(guests, adminGuest{ID: g.ID, Name: g.Name, IsChild: g.IsChild})
|
||||
}
|
||||
out = append(out, adminRSVP{
|
||||
ID: rec.ID,
|
||||
Attending: rec.Attending,
|
||||
Message: rec.Message,
|
||||
CreatedAt: rec.CreatedAt.Format(time.RFC3339),
|
||||
Guests: guests,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"rsvps": out})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminCreateRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
var req rsvpRequest
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
var guests []store.Guest
|
||||
for _, g := range req.Guests {
|
||||
name := strings.TrimSpace(g.Name)
|
||||
if name != "" {
|
||||
guests = append(guests, store.Guest{Name: name, IsChild: g.Type == "child"})
|
||||
}
|
||||
}
|
||||
if len(guests) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "at least one name is required")
|
||||
return
|
||||
}
|
||||
id, err := s.store.CreateRSVP(store.RSVP{
|
||||
Guests: guests,
|
||||
Message: strings.TrimSpace(req.Message),
|
||||
Attending: req.Attending == nil || *req.Attending,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("admin: create rsvp failed: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not save RSVP")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminPatchRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Attending *bool `json:"attending"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil || req.Attending == nil {
|
||||
writeError(w, http.StatusBadRequest, "attending is required")
|
||||
return
|
||||
}
|
||||
if err := s.store.SetRSVPAttending(id, *req.Attending); err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminDeleteRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteRSVP(id); err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminAddGuest(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var req guestRequest
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
writeError(w, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
guestID, err := s.store.AddGuest(id, store.Guest{Name: name, IsChild: req.Type == "child"})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"id": guestID})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminPatchGuest(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var req guestRequest
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
writeError(w, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
if err := s.store.UpdateGuest(id, name, req.Type == "child"); err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminDeleteGuest(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := pathID(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteGuest(id); err != nil {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
@@ -73,8 +253,9 @@ type guestRequest struct {
|
||||
}
|
||||
|
||||
type rsvpRequest struct {
|
||||
Guests []guestRequest `json:"guests"`
|
||||
Message string `json:"message"`
|
||||
Guests []guestRequest `json:"guests"`
|
||||
Message string `json:"message"`
|
||||
Attending *bool `json:"attending"`
|
||||
}
|
||||
|
||||
func (s *Server) handleRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -96,9 +277,11 @@ func (s *Server) handleRSVP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
attending := req.Attending == nil || *req.Attending
|
||||
rsvp := store.RSVP{
|
||||
Guests: guests,
|
||||
Message: strings.TrimSpace(req.Message),
|
||||
Guests: guests,
|
||||
Message: strings.TrimSpace(req.Message),
|
||||
Attending: attending,
|
||||
}
|
||||
if err := s.store.InsertRSVP(rsvp); err != nil {
|
||||
log.Printf("rsvp: insert failed: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user