diff --git a/internal/core/templates/dashboard.html b/internal/core/templates/dashboard.html index e0eb2eb..a6fcd0c 100644 --- a/internal/core/templates/dashboard.html +++ b/internal/core/templates/dashboard.html @@ -66,36 +66,25 @@ - -{{if or (not .OwnedCount) (not .OrgCount)}} -
- {{if not .OwnedCount}} -
-
-
- {{template "icon-antenna" ""}} -

Add your first repeater

-

Connect a repeater to MeshTender and test it with a modem.

- {{template "icon-plus" "me-1"}}Add repeater -
-
+ +{{if .Onboarding}} +
+
+

Get started

+
{{.OnboardingDone}} of {{.OnboardingTotal}} done
- {{end}} - {{if not .OrgCount}} -
-
-
- {{template "icon-world" ""}} -

Join an organization

-

Find a local mesh community to share repeaters and coordinate.

- +
+ {{range .Onboarding}} +
+ +
+
{{.Title}}
+ {{if not .Done}}
{{.Desc}}
{{end}}
+ {{if not .Done}}{{.Action}}{{end}}
+ {{end}}
- {{end}}
{{end}} diff --git a/internal/core/web.go b/internal/core/web.go index 87f2725..9eb4adb 100644 --- a/internal/core/web.go +++ b/internal/core/web.go @@ -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"), }) }