feat: new home page

This commit is contained in:
juancwu 2026-05-03 23:02:51 +00:00
commit 145eed9eef
7 changed files with 142 additions and 3 deletions

View file

@ -21,7 +21,7 @@ func (h *homeHandler) HomePage(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/auth", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/app/spaces", http.StatusSeeOther)
http.Redirect(w, r, "/app/home", http.StatusSeeOther)
}
func (h *homeHandler) PrivacyPage(w http.ResponseWriter, r *http.Request) {

View file

@ -9,5 +9,5 @@ func NewRedirectHandler() *redirectHandler {
}
func (h *redirectHandler) Spaces(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/app/spaces", http.StatusMovedPermanently)
http.Redirect(w, r, "/app/home", http.StatusMovedPermanently)
}

View file

@ -17,5 +17,5 @@ func TestRedirectHandler_RederictToSpaces(t *testing.T) {
h.Spaces(w, req)
assert.Equal(t, http.StatusMovedPermanently, w.Code)
assert.Equal(t, "/app/spaces", w.Header().Get("Location"))
assert.Equal(t, "/app/home", w.Header().Get("Location"))
}

View file

@ -41,6 +41,42 @@ func NewSpaceHandler(
}
}
func (h *spaceHandler) HomePage(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
if user == nil {
ui.RenderError(w, r, "Unauthorized", http.StatusUnauthorized)
return
}
owned, err := h.spaceService.GetOwnedSpaces(user.ID)
if err != nil {
slog.Error("failed to load owned spaces", "error", err, "user_id", user.ID)
ui.RenderError(w, r, "Failed to load spaces", http.StatusInternalServerError)
return
}
shared, err := h.spaceService.GetSharedSpaces(user.ID)
if err != nil {
slog.Error("failed to load shared spaces", "error", err, "user_id", user.ID)
ui.RenderError(w, r, "Failed to load spaces", http.StatusInternalServerError)
return
}
ownedCards := h.buildSpaceCards(owned)
sharedCards := h.buildSpaceCards(shared)
total := decimal.Zero
for _, c := range ownedCards {
total = total.Add(c.TotalBalance)
}
ui.Render(w, r, pages.Home(pages.HomeProps{
OwnedSpaces: ownedCards,
SharedSpaces: sharedCards,
TotalBalance: total,
}))
}
func (h *spaceHandler) SpacesPage(w http.ResponseWriter, r *http.Request) {
user := ctxkeys.User(r.Context())
if user == nil {

View file

@ -85,6 +85,8 @@ func SetupRoutes(a *app.App) http.Handler {
r.Group("/app", func(g *router.Group) {
g.Use(middleware.RequireAuth)
g.Get("/home", spaceH.HomePage).Name("page.app.home")
g.SubGroup("/spaces", func(g *router.Group) {
g.Get("", spaceH.SpacesPage).Name("page.app.spaces")
g.Get("/create", spaceH.CreateSpacePage).Name("page.app.spaces.create")

View file

@ -0,0 +1,91 @@
package pages
import (
"fmt"
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
"git.juancwu.dev/juancwu/budgit/internal/routeurl"
"git.juancwu.dev/juancwu/budgit/internal/ui/blocks"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
"github.com/shopspring/decimal"
)
type HomeProps struct {
OwnedSpaces []blocks.SpaceCardInfo
SharedSpaces []blocks.SpaceCardInfo
TotalBalance decimal.Decimal
}
templ Home(props HomeProps) {
{{
user := ctxkeys.User(ctx)
displayName := ""
if user != nil && user.Name != nil {
displayName = *user.Name
}
totalSpaces := len(props.OwnedSpaces) + len(props.SharedSpaces)
}}
@layouts.App("Home", spaceOverviewSidebarContent()) {
<div class="container px-6 py-8 mx-auto space-y-10">
<div class="w-full space-y-2 md:space-y-0 md:flex justify-between items-end">
<div class="space-y-2">
<p class="text-muted-foreground text-sm">Home</p>
<h1 class="text-2xl font-bold">Hello, { displayName }</h1>
if totalSpaces == 0 {
<p class="text-muted-foreground">Create a space to start tracking your expenses.</p>
} else {
{{
countPhrase := fmt.Sprintf("Across %d spaces", totalSpaces)
if totalSpaces == 1 {
countPhrase = "Across 1 space"
}
}}
<p class="text-muted-foreground">{ countPhrase } you own you have <span class="font-medium text-foreground">${ utils.FormatDecimalWithThousands(props.TotalBalance.StringFixedBank(2)) }</span> tracked.</p>
}
</div>
<div>
@button.Button(button.Props{
Class: "flex gap-2 items-center",
Href: routeurl.URL("page.app.spaces.create"),
}) {
@icon.Plus()
Create space
}
</div>
</div>
<section class="space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">My spaces</h2>
<span class="text-xs text-muted-foreground">{ fmt.Sprintf("%d", len(props.OwnedSpaces)) }</span>
</div>
if len(props.OwnedSpaces) == 0 {
<p class="text-sm text-muted-foreground">You don't own any spaces yet.</p>
} else {
<div>
for _, space := range props.OwnedSpaces {
@blocks.SpaceCard(space)
}
</div>
}
</section>
<section class="space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Shared with me</h2>
<span class="text-xs text-muted-foreground">{ fmt.Sprintf("%d", len(props.SharedSpaces)) }</span>
</div>
if len(props.SharedSpaces) == 0 {
<p class="text-sm text-muted-foreground">No spaces have been shared with you.</p>
} else {
<div>
for _, space := range props.SharedSpaces {
@blocks.SpaceCard(space)
}
</div>
}
</section>
</div>
}
}

View file

@ -81,6 +81,16 @@ templ spaceOverviewSidebarContent() {
Overview
}
@sidebar.Menu() {
@sidebar.MenuItem() {
@sidebar.MenuButton(sidebar.MenuButtonProps{
Href: routeurl.URL("page.app.home"),
IsActive: ctxkeys.URLPath(ctx) == routeurl.URL("page.app.home"),
Tooltip: "Home",
}) {
@icon.House()
<span>Home</span>
}
}
@sidebar.MenuItem() {
@sidebar.MenuButton(sidebar.MenuButtonProps{
Href: routeurl.URL("page.app.spaces"),