From ef57f3e196bf2538509d6275052faa37415d495e Mon Sep 17 00:00:00 2001
From: Brittany Ellich
Date: Fri, 15 May 2026 19:32:10 -0700
Subject: [PATCH] Add badges
---
features/common/layouts/bottomnav.templ | 20 +
features/common/layouts/bottomnav_templ.go | 108 ++
features/connect/handlers.go | 21 +
features/connections/handlers.go | 12 +
features/connections/pages/connections.templ | 6 +
.../connections/pages/connections_templ.go | 111 +-
features/connections/pages/profile.templ | 1 +
features/connections/pages/profile_templ.go | 10 +-
features/event/handlers.go | 22 +
features/event/pages/event.templ | 27 +-
features/event/pages/event_templ.go | 99 +-
features/events/handlers.go | 380 ++++++
features/events/pages/detail.templ | 284 ++++
features/events/pages/detail_templ.go | 1156 +++++++++++++++++
features/events/pages/events.templ | 120 ++
features/events/pages/events_templ.go | 491 +++++++
features/events/routes.go | 17 +
features/index/handlers.go | 35 +
features/index/pages/index.templ | 53 +-
features/index/pages/index_templ.go | 274 +++-
features/profile/handlers.go | 17 +
features/profile/pages/profile.templ | 36 +-
features/profile/pages/profile_templ.go | 159 ++-
features/settings/pages/settings.templ | 1 +
features/settings/pages/settings_templ.go | 4 +
internal/badge/award.go | 134 ++
internal/badge/definitions.go | 184 +++
internal/checkin/list.go | 76 ++
.../db/migrations/007_badge_definitions.sql | 50 +
internal/event/list.go | 8 +-
internal/event/stats.go | 57 +
internal/interest/match.go | 95 ++
router/router.go | 2 +
web/resources/static/css/terminal.css | 473 +++++++
web/resources/static/js/geofence.js | 61 +
35 files changed, 4459 insertions(+), 145 deletions(-)
create mode 100644 features/common/layouts/bottomnav.templ
create mode 100644 features/common/layouts/bottomnav_templ.go
create mode 100644 features/events/handlers.go
create mode 100644 features/events/pages/detail.templ
create mode 100644 features/events/pages/detail_templ.go
create mode 100644 features/events/pages/events.templ
create mode 100644 features/events/pages/events_templ.go
create mode 100644 features/events/routes.go
create mode 100644 internal/badge/award.go
create mode 100644 internal/badge/definitions.go
create mode 100644 internal/checkin/list.go
create mode 100644 internal/db/migrations/007_badge_definitions.sql
create mode 100644 internal/event/stats.go
create mode 100644 internal/interest/match.go
create mode 100644 web/resources/static/js/geofence.js
diff --git a/features/common/layouts/bottomnav.templ b/features/common/layouts/bottomnav.templ
new file mode 100644
index 0000000..420a1a0
--- /dev/null
+++ b/features/common/layouts/bottomnav.templ
@@ -0,0 +1,20 @@
+package layouts
+
+// BottomNav renders a fixed bottom navigation bar for authenticated pages.
+// activePage should be "home", "connections", or "event".
+templ BottomNav(activePage string) {
+
+}
diff --git a/features/common/layouts/bottomnav_templ.go b/features/common/layouts/bottomnav_templ.go
new file mode 100644
index 0000000..b19bf20
--- /dev/null
+++ b/features/common/layouts/bottomnav_templ.go
@@ -0,0 +1,108 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.1020
+package layouts
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+// BottomNav renders a fixed bottom navigation bar for authenticated pages.
+// activePage should be "home", "connections", or "event".
+func BottomNav(activePage string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/features/connect/handlers.go b/features/connect/handlers.go
index c07a347..6dbb94c 100644
--- a/features/connect/handlers.go
+++ b/features/connect/handlers.go
@@ -25,6 +25,7 @@ import (
"atmoquest/config"
"atmoquest/features/auth"
"atmoquest/features/connect/pages"
+ "atmoquest/internal/badge"
"atmoquest/internal/checkin"
"atmoquest/internal/connection"
"atmoquest/internal/event"
@@ -192,6 +193,22 @@ func (h *Handlers) ConnectConfirm(w http.ResponseWriter, r *http.Request) {
if _, err := checkin.Put(r.Context(), viewerSess, h.DB, checkinEventURI, time.Time{}); err != nil {
slog.Warn("connect confirm: also-checkin", "event_uri", checkinEventURI, "err", err)
}
+ // Award event-attendee badge for the check-in event.
+ if _, err := badge.Award(r.Context(), viewerSess, h.DB, badge.AwardEventAttendee, checkinEventURI); err != nil {
+ slog.Info("connect confirm: event-attendee badge", "err", err)
+ }
+ }
+
+ // Award first-connect badge (idempotent — skips if already exists).
+ if _, err := badge.Award(r.Context(), viewerSess, h.DB, badge.AwardFirstConnect, checkinEventURI); err != nil {
+ slog.Info("connect confirm: first-connect badge", "err", err)
+ }
+
+ // Update event stats: this viewer made a connection at this event.
+ if checkinEventURI != "" {
+ if err := event.IncrementConnectors(r.Context(), h.DB, checkinEventURI); err != nil {
+ slog.Info("connect confirm: increment connectors", "err", err)
+ }
}
http.Redirect(w, r, "/profile?connected="+target.String(), http.StatusSeeOther)
@@ -249,6 +266,10 @@ func (h *Handlers) ConnectFlushLocal(w http.ResponseWriter, r *http.Request) {
if err := h.Queue.Enqueue(r.Context(), target, viewerDID); err != nil {
slog.Warn("flush-local: enqueue", "target", target.String(), "err", err)
}
+ // Award first-connect badge (idempotent).
+ if _, err := badge.Award(r.Context(), viewerSess, h.DB, badge.AwardFirstConnect, ""); err != nil {
+ slog.Info("flush-local: first-connect badge", "err", err)
+ }
written++
}
diff --git a/features/connections/handlers.go b/features/connections/handlers.go
index b329eaa..6652835 100644
--- a/features/connections/handlers.go
+++ b/features/connections/handlers.go
@@ -86,6 +86,11 @@ func (h *Handlers) List(w http.ResponseWriter, r *http.Request) {
SortTime: entry.ConnectedAt,
}
+ // Load follow-up flag from SQLite (soft-fail).
+ if note, err := notes.Get(r.Context(), h.DB, did.String(), entry.With.String()); err == nil {
+ item.FollowUp = note.FollowUp
+ }
+
targetPDS := h.lookupPDSForDID(r, entry.With)
bsky, err := profile.FetchBluesky(r.Context(), targetPDS, entry.With)
if err == nil && bsky != nil {
@@ -120,6 +125,13 @@ func (h *Handlers) List(w http.ResponseWriter, r *http.Request) {
nj := strings.ToLower(displayOrDID(items[j]))
return ni < nj
})
+ case "followup":
+ sort.SliceStable(items, func(i, j int) bool {
+ if items[i].FollowUp != items[j].FollowUp {
+ return items[i].FollowUp // follow-ups first
+ }
+ return items[i].SortTime.After(items[j].SortTime)
+ })
default: // "recent"
sort.Slice(items, func(i, j int) bool {
return items[i].SortTime.After(items[j].SortTime)
diff --git a/features/connections/pages/connections.templ b/features/connections/pages/connections.templ
index 235fd10..0488c82 100644
--- a/features/connections/pages/connections.templ
+++ b/features/connections/pages/connections.templ
@@ -29,6 +29,7 @@ type ConnectionItem struct {
EventURI string
EventName string
SortTime time.Time // used for sorting, not rendered
+ FollowUp bool // follow-up flag from SQLite
}
// EventOption is an entry in the event filter dropdown.
@@ -71,6 +72,7 @@ templ Connections(v ConnectionsView) {
@@ -115,6 +117,7 @@ templ Connections(v ConnectionsView) {
+ @layouts.BottomNav("connections")
}
}
@@ -142,6 +145,9 @@ templ connectionRow(item ConnectionItem) {
if item.EventName != "" {
{ item.EventName }
}
+ if item.FollowUp {
+ ⚑ follow up
+ }
}
diff --git a/features/connections/pages/connections_templ.go b/features/connections/pages/connections_templ.go
index de31038..ff1fa78 100644
--- a/features/connections/pages/connections_templ.go
+++ b/features/connections/pages/connections_templ.go
@@ -37,6 +37,7 @@ type ConnectionItem struct {
EventURI string
EventName string
SortTime time.Time // used for sorting, not rendered
+ FollowUp bool // follow-up flag from SQLite
}
// EventOption is an entry in the event filter dropdown.
@@ -85,7 +86,7 @@ func Connections(v ConnectionsView) templ.Component {
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("@")
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 51, Col: 56}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 52, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -98,7 +99,7 @@ func Connections(v ConnectionsView) templ.Component {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(v.Query)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 61, Col: 22}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 62, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
@@ -116,7 +117,7 @@ func Connections(v ConnectionsView) templ.Component {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(ev.URI)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 68, Col: 30}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 69, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
@@ -139,7 +140,7 @@ func Connections(v ConnectionsView) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(ev.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 68, Col: 80}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 69, Col: 80}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -170,12 +171,22 @@ func Connections(v ConnectionsView) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">alphabetical ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">alphabetical
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if v.Total == 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "no connections yet — go scan some QR codes!")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "no connections yet — go scan some QR codes!")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -183,59 +194,59 @@ func Connections(v ConnectionsView) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(v.Total))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 81, Col: 22}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 83, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(pluralize(v.Total, "connection", "connections"))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 81, Col: 74}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 83, Col: 74}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "showing ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "showing ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(v.Showing))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 83, Col: 32}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 85, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " of ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " of ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(itoa(v.Total))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 83, Col: 53}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 85, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(v.Items) > 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -245,22 +256,26 @@ func Connections(v ConnectionsView) templ.Component {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if v.Total > 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "you haven't connected with anyone yet.
scan someone's QR code at an event to get started.
← back home ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "you haven't connected with anyone yet.
scan someone's QR code at an event to get started.
← back home ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "● reads from quest.atmo.connection
v0.1
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "● reads from quest.atmo.connection
v0.1
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = layouts.BottomNav("connections").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -295,157 +310,163 @@ func connectionRow(item ConnectionItem) templ.Component {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"connection-row\" role=\"listitem\">
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.AvatarURL != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
)
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" loading=\"lazy\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
?
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
?
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.DisplayName != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DisplayName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 133, Col: 53}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 136, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(shortDID(item.DID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 135, Col: 77}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 138, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.ConnectedAt)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 137, Col: 58}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 140, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if item.Bio != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(truncate(item.Bio, 120))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 140, Col: 55}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 143, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if item.EventName != "" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.EventName)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 143, Col: 56}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `features/connections/pages/connections.templ`, Line: 146, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ if item.FollowUp {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
⚑ follow up")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/features/connections/pages/profile.templ b/features/connections/pages/profile.templ
index 92fbbbb..5c38b93 100644
--- a/features/connections/pages/profile.templ
+++ b/features/connections/pages/profile.templ
@@ -174,6 +174,7 @@ templ ProfileViewPage(v ProfileView) {
+ @layouts.BottomNav("connections")
}
}
diff --git a/features/connections/pages/profile_templ.go b/features/connections/pages/profile_templ.go
index 07bc7c2..37672c1 100644
--- a/features/connections/pages/profile_templ.go
+++ b/features/connections/pages/profile_templ.go
@@ -450,7 +450,15 @@ func ProfileViewPage(v ProfileView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" class=\"btn btn-ghost btn-sm\">connect page →● reads from quest.atmo.profile · notes saved to server
v0.1
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" class=\"btn btn-ghost btn-sm\">connect page →● reads from quest.atmo.profile · notes saved to server
v0.1
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = layouts.BottomNav("connections").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/features/event/handlers.go b/features/event/handlers.go
index ba2a8aa..5ff5654 100644
--- a/features/event/handlers.go
+++ b/features/event/handlers.go
@@ -21,6 +21,7 @@ import (
"atmoquest/config"
"atmoquest/features/auth"
"atmoquest/features/event/pages"
+ "atmoquest/internal/badge"
"atmoquest/internal/checkin"
"atmoquest/internal/event"
"atmoquest/internal/qrcode"
@@ -82,6 +83,11 @@ func (h *Handlers) EventScan(w http.ResponseWriter, r *http.Request) {
IsOngoing: ev.IsOngoing(time.Now()),
ViewerLoggedIn: didStr != "" && sid != "",
}
+ if ev.Geofence != nil {
+ view.GeofenceLat = ev.Geofence.Lat
+ view.GeofenceLng = ev.Geofence.Lng
+ view.GeofenceRadius = ev.Geofence.RadiusMeters
+ }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := pages.EventScan(view).Render(r.Context(), w); err != nil {
slog.Error("render event scan", "err", err)
@@ -106,6 +112,14 @@ func (h *Handlers) EventScanCheckin(w http.ResponseWriter, r *http.Request) {
http.Error(w, "failed to check in: "+err.Error(), http.StatusBadGateway)
return
}
+ // Award event-attendee badge (idempotent).
+ if _, err := badge.Award(r.Context(), viewerSess, h.DB, badge.AwardEventAttendee, ev.URI); err != nil {
+ slog.Info("event scan: event-attendee badge", "err", err)
+ }
+ // Update event stats.
+ if err := event.IncrementCheckins(r.Context(), h.DB, ev.URI); err != nil {
+ slog.Info("event scan: increment checkins", "err", err)
+ }
http.Redirect(w, r, "/?checkedin=1", http.StatusSeeOther)
}
@@ -151,6 +165,14 @@ func (h *Handlers) EventFlushLocal(w http.ResponseWriter, r *http.Request) {
errs = append(errs, "checkin failed for "+truncate(t, 20))
continue
}
+ // Award event-attendee badge (idempotent).
+ if _, err := badge.Award(r.Context(), viewerSess, h.DB, badge.AwardEventAttendee, ev.URI); err != nil {
+ slog.Info("event flush-local: event-attendee badge", "err", err)
+ }
+ // Update event stats.
+ if err := event.IncrementCheckins(r.Context(), h.DB, ev.URI); err != nil {
+ slog.Info("event flush-local: increment checkins", "err", err)
+ }
written++
}
diff --git a/features/event/pages/event.templ b/features/event/pages/event.templ
index 4c4e877..547a776 100644
--- a/features/event/pages/event.templ
+++ b/features/event/pages/event.templ
@@ -1,6 +1,11 @@
package pages
-import "atmoquest/features/common/layouts"
+import (
+ "fmt"
+ "strconv"
+
+ "atmoquest/features/common/layouts"
+)
// EventScanView feeds the /e/{token} landing page.
type EventScanView struct {
@@ -11,6 +16,11 @@ type EventScanView struct {
EventEndTime string
IsOngoing bool
ViewerLoggedIn bool
+
+ // Geofence fields (zero values = no geofence configured).
+ GeofenceLat float64
+ GeofenceLng float64
+ GeofenceRadius int // meters
}
templ EventScan(v EventScanView) {
@@ -43,6 +53,20 @@ templ EventScan(v EventScanView) {
}
if v.ViewerLoggedIn {
+ if v.GeofenceRadius > 0 {
+
+ 📍 heads up:
+ checking your location…
+
+ }