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, "no connections match your search.
clear filtersno connections match your search.
clear filters") + 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, "