Get started checklist

This commit is contained in:
Jonathon Leight committed 2026-06-28 18:16:44 -04:00
1 parent cfc236e365
commit f2a34cf03c
2 files changed
+79 -36

No files matched your search

+15 -26
View File
@@ -66,36 +66,25 @@
</div>
</div>
<!-- Getting started: shown only while a step is still pending -->
{{if or (not .OwnedCount) (not .OrgCount)}}
<div class="row row-cards mt-1">
{{if not .OwnedCount}}
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<span class="avatar avatar-lg bg-primary-lt mb-3">{{template "icon-antenna" ""}}</span>
<h3 class="card-title">Add your first repeater</h3>
<p class="text-secondary">Connect a repeater to MeshTender and test it with a modem.</p>
<a href="/repeaters/add" class="btn btn-primary">{{template "icon-plus" "me-1"}}Add repeater</a>
</div>
</div>
<!-- Getting-started checklist: shown only while a step is still pending -->
{{if .Onboarding}}
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title">Get started</h3>
<div class="card-actions text-secondary small">{{.OnboardingDone}} of {{.OnboardingTotal}} done</div>
</div>
{{end}}
{{if not .OrgCount}}
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<span class="avatar avatar-lg bg-azure-lt mb-3">{{template "icon-world" ""}}</span>
<h3 class="card-title">Join an organization</h3>
<p class="text-secondary">Find a local mesh community to share repeaters and coordinate.</p>
<div class="btn-list justify-content-center">
<a href="{{.RootURL}}/orgs" class="btn btn-primary">Browse organizations</a>
<a href="/orgs/new" class="btn">Create one</a>
</div>
<div class="list-group list-group-flush">
{{range .Onboarding}}
<div class="list-group-item d-flex align-items-center gap-3">
<input class="form-check-input m-0" type="checkbox" {{if .Done}}checked{{end}} disabled aria-label="{{.Title}}">
<div class="flex-fill" style="min-width:0">
<div class="fw-bold{{if .Done}} text-secondary text-decoration-line-through{{end}}">{{.Title}}</div>
{{if not .Done}}<div class="text-secondary small">{{.Desc}}</div>{{end}}
</div>
{{if not .Done}}<a href="{{.Href}}" class="btn btn-sm btn-primary">{{.Action}}</a>{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
+64 -10
View File
@@ -223,8 +223,19 @@ func (s *Handlers) pageRepeaters(w http.ResponseWriter, r *http.Request) {
})
}
// pageDashboard renders the signed-in overview: summary stats, short lists of
// repeaters and organizations, a map of owned repeaters, and recent activity.
// onboardingStep is one item in the dashboard getting-started checklist. Steps
// are data-driven so adding a new one (e.g. future profile fields) is a single
// entry in buildOnboarding rather than template surgery.
type onboardingStep struct {
Title string
Desc string
Action string // CTA label, shown only while the step is pending
Href string // where the CTA leads (absolute when it crosses hosts)
Done bool
}
// pageDashboard renders the signed-in overview: summary stats, a getting-started
// checklist, a map of owned repeaters, and recent activity.
func (s *Handlers) pageDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
uid := s.Auth.CurrentUserID(ctx)
@@ -246,6 +257,11 @@ func (s *Handlers) pageDashboard(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not load activity", http.StatusInternalServerError)
return
}
user, err := s.Store.GetUserByID(ctx, uid)
if err != nil {
http.Error(w, "could not load account", http.StatusInternalServerError)
return
}
confirmed, unconfirmed := 0, 0
var mapped []*store.Repeater
@@ -260,15 +276,53 @@ func (s *Handlers) pageDashboard(w http.ResponseWriter, r *http.Request) {
}
}
steps := []onboardingStep{
{
Title: "Set up your profile",
Desc: "Add a display name so teammates recognize you.",
Action: "Edit profile",
Href: s.Origin(r, s.Cfg.AuthHost) + "/account",
Done: user.DisplayName != nil && *user.DisplayName != "",
},
{
Title: "Add a repeater",
Desc: "Connect a repeater to MeshTender and test it with a modem.",
Action: "Add repeater",
Href: "/repeaters/add",
Done: len(owned) > 0,
},
{
Title: "Join an organization",
Desc: "Find a local mesh community to share repeaters and coordinate.",
Action: "Browse organizations",
Href: s.Origin(r, s.Cfg.RootHost) + "/orgs",
Done: len(orgs) > 0,
},
}
total := len(steps)
doneCount := 0
for _, st := range steps {
if st.Done {
doneCount++
}
}
// Hide the checklist entirely once everything's done.
if doneCount == total {
steps = nil
}
s.Render(w, r, "dashboard.html", map[string]any{
"OwnedCount": len(owned),
"SharedCount": len(shared),
"OrgCount": len(orgs),
"Confirmed": confirmed,
"Unconfirmed": unconfirmed,
"Mapped": mapped,
"Recent": recent,
"Error": r.URL.Query().Get("error"),
"OwnedCount": len(owned),
"SharedCount": len(shared),
"OrgCount": len(orgs),
"Confirmed": confirmed,
"Unconfirmed": unconfirmed,
"Mapped": mapped,
"Recent": recent,
"Onboarding": steps,
"OnboardingDone": doneCount,
"OnboardingTotal": total,
"Error": r.URL.Query().Get("error"),
})
}