Add attendee cap with required login for capped events

Hosts can optionally cap total attendance (RSVPs + plus-ones) when
creating an event. Capped events require guests to log in via the
existing email code flow; RSVPs are tied to accounts and deduped per
user. The event page shows spots left and replaces the form with a
full notice when the cap is reached; the server rejects over-cap
RSVPs and edits with 409 as a race backstop.

Also threads a safeNext-validated ?next= param through the login and
name-setup flow so guests land back on the event after signing in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:36:57 -04:00
parent 2aa3abf035
commit d005080aca
13 changed files with 368 additions and 44 deletions
+20 -2
View File
@@ -19,7 +19,7 @@
{{define "content"}}
<div class="event-header">
<div class="event-tag">Open · {{.TotalGoing}} going</div>
<div class="event-tag">Open · {{.TotalGoing}} going{{if .Capped}} · {{if .CapFull}}FULL{{else}}{{.SpotsLeft}} spot{{if ne .SpotsLeft 1}}s{{end}} left{{end}}{{end}}</div>
<h1 class="event-title">{{.Event.Title}}</h1>
<div class="event-meta">
{{if .Event.Date}}<span>&#128197; {{.Event.Date}}</span>{{end}}
@@ -41,6 +41,23 @@
<div class="section-label" style="margin-top:40px">Sign up</div>
{{if and .Capped .CapFull}}
<div class="claim-form-wrapper">
<div class="form-title">Event is full</div>
<p style="font-family:'DM Mono',monospace;font-size:0.8rem;color:#555;">
All {{.Event.AttendeeCap}} spots are taken.
</p>
</div>
{{else if and .Capped (not .User)}}
<div class="claim-form-wrapper">
<div class="form-title">Log in to RSVP</div>
<p style="font-family:'DM Mono',monospace;font-size:0.8rem;color:#555;margin-bottom:16px;">
This event has limited spots ({{.SpotsLeft}} left), so RSVPs require an account.
</p>
<a class="btn-submit" style="display:inline-block;text-decoration:none;text-align:center;"
href="/login?next=/e/{{.Event.Slug}}">Log in to RSVP &#8599;</a>
</div>
{{else}}
<div class="claim-form-wrapper">
<div class="form-title">I'm coming &#8594;</div>
<form hx-post="/e/{{.Event.Slug}}/rsvp"
@@ -64,7 +81,7 @@
{{end}}
<div class="form-row">
<label>Bringing anyone?</label>
<input type="number" name="plus_one" value="0" min="0" max="10">
<input type="number" name="plus_one" value="0" min="0" max="{{if .Capped}}{{.PlusOneMax}}{{else}}10{{end}}">
</div>
<div class="form-row" id="claim-note">
<label>Note (optional)</label>
@@ -73,6 +90,7 @@
<button class="btn-submit" type="submit">Count me in &#8599;</button>
</form>
</div>
{{end}}
{{if .IsAdmin}}
<div class="section-label" style="margin-top:40px">Admin: Location</div>
+6
View File
@@ -33,6 +33,12 @@
<label>Description (optional, markdown supported)</label>
<textarea name="description" rows="4" placeholder="e.g. Bring your own blanket! We'll have a grill set up near the big oak tree." style="border:var(--border-w) solid var(--border);background:var(--cream);padding:10px 14px;font-family:'Bricolage Grotesque',sans-serif;font-size:0.95rem;resize:vertical;outline:none;"></textarea>
</div>
{{if .AuthEnabled}}
<div class="form-row">
<label>Attendee cap (optional — guests must log in to RSVP)</label>
<input type="number" name="attendee_cap" min="0" max="1000" placeholder="e.g. 30 — leave blank for unlimited">
</div>
{{end}}
<div class="section-label" style="margin:24px 0 16px">Slots</div>
+2
View File
@@ -14,6 +14,7 @@
<div class="claim-form-wrapper">
{{if eq .Step "identify"}}
<form method="POST" action="/login">
{{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}}
<div class="form-row">
<label>Email</label>
<input type="email" name="identifier" placeholder="you@example.com" required autofocus>
@@ -23,6 +24,7 @@
{{else}}
<form method="POST" action="/login/verify">
<input type="hidden" name="identifier" value="{{.Identifier}}">
{{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}}
<p style="font-family:'DM Mono',monospace;font-size:0.78rem;color:#555;margin-bottom:16px;">
Code sent to <strong>{{.Identifier}}</strong>
</p>
+1
View File
@@ -13,6 +13,7 @@
<div class="claim-form-wrapper">
<form method="POST" action="/account/name">
{{if .Next}}<input type="hidden" name="next" value="{{.Next}}">{{end}}
<div class="form-row">
<label>Name</label>
<input type="text" name="name" placeholder="Your name" required autofocus
+1 -1
View File
@@ -32,7 +32,7 @@
{{end}}
</div>
<div class="section-label">Going ({{.TotalGoing}})</div>
<div class="section-label">Going ({{.TotalGoing}}{{if .Capped}} / {{.Event.AttendeeCap}}{{end}})</div>
<div class="rsvp-list">
{{if .GoingList}}
{{range .GoingList}}