From 48cae4957e92fcb22d43aefa8d8ec78466e1a6f6 Mon Sep 17 00:00:00 2001 From: juancwu Date: Sun, 12 Apr 2026 16:55:38 +0000 Subject: [PATCH] feat: shared with me page --- internal/handler/space.go | 31 ++++++++++++++++++++++++++----- internal/repository/space.go | 33 +++++++++++++++++++++++++++++++++ internal/routes/routes.go | 2 +- internal/service/space.go | 18 ++++++++++++++++++ internal/ui/pages/spaces.templ | 20 ++++++++++++++++++-- 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/internal/handler/space.go b/internal/handler/space.go index 865a691..bed2393 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -6,6 +6,7 @@ import ( "strings" "git.juancwu.dev/juancwu/budgit/internal/ctxkeys" + "git.juancwu.dev/juancwu/budgit/internal/model" "git.juancwu.dev/juancwu/budgit/internal/service" "git.juancwu.dev/juancwu/budgit/internal/ui" "git.juancwu.dev/juancwu/budgit/internal/ui/blocks" @@ -30,27 +31,48 @@ func (h *spaceHandler) SpacesPage(w http.ResponseWriter, r *http.Request) { return } - spaces, err := h.spaceService.GetSpacesForUser(user.ID) + spaces, err := h.spaceService.GetOwnedSpaces(user.ID) if err != nil { slog.Error("failed to load spaces", "error", err, "user_id", user.ID) ui.RenderError(w, r, "Failed to load spaces", http.StatusInternalServerError) return } + cards := h.buildSpaceCards(spaces) + ui.Render(w, r, pages.Spaces(cards)) +} + +func (h *spaceHandler) SharedSpacesPage(w http.ResponseWriter, r *http.Request) { + user := ctxkeys.User(r.Context()) + if user == nil { + ui.RenderError(w, r, "Unauthorized", http.StatusUnauthorized) + return + } + + spaces, 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 shared spaces", http.StatusInternalServerError) + return + } + + cards := h.buildSpaceCards(spaces) + ui.Render(w, r, pages.SharedSpaces(cards)) +} + +func (h *spaceHandler) buildSpaceCards(spaces []*model.Space) []blocks.SpaceCardInfo { cards := make([]blocks.SpaceCardInfo, 0, len(spaces)) for _, sp := range spaces { memberCount, err := h.spaceService.GetMemberCount(sp.ID) if err != nil { slog.Error("failed to get space member count", "error", err, "space_id", sp.ID) memberCount = 0 - err = nil } accounts, err := h.accountService.GetAccountsForSpace(sp.ID) if err != nil { slog.Error("failed to get space accounts", "error", err, "space_id", sp.ID) accounts = nil - err = nil } totalBalance := decimal.Zero @@ -65,8 +87,7 @@ func (h *spaceHandler) SpacesPage(w http.ResponseWriter, r *http.Request) { TotalBalance: totalBalance, }) } - - ui.Render(w, r, pages.Spaces(cards)) + return cards } func (h *spaceHandler) CreateSpacePage(w http.ResponseWriter, r *http.Request) { diff --git a/internal/repository/space.go b/internal/repository/space.go index 94a0294..b533437 100644 --- a/internal/repository/space.go +++ b/internal/repository/space.go @@ -17,6 +17,8 @@ type SpaceRepository interface { Create(space *model.Space) error ByID(id string) (*model.Space, error) ByUserID(userID string) ([]*model.Space, error) + ByOwnerID(userID string) ([]*model.Space, error) + SharedWithUser(userID string) ([]*model.Space, error) AddMember(spaceID, userID string, role model.Role) error RemoveMember(spaceID, userID string) error IsMember(spaceID, userID string) (bool, error) @@ -91,6 +93,37 @@ func (r *spaceRepository) ByUserID(userID string) ([]*model.Space, error) { return spaces, nil } +func (r *spaceRepository) ByOwnerID(userID string) ([]*model.Space, error) { + var spaces []*model.Space + query := ` + SELECT s.* + FROM spaces s + WHERE s.owner_id = $1 + ORDER BY s.created_at DESC; + ` + err := r.db.Select(&spaces, query, userID) + if err != nil { + return nil, err + } + return spaces, nil +} + +func (r *spaceRepository) SharedWithUser(userID string) ([]*model.Space, error) { + var spaces []*model.Space + query := ` + SELECT s.* + FROM spaces s + JOIN space_members sm ON s.id = sm.space_id + WHERE sm.user_id = $1 AND s.owner_id != $1 + ORDER BY s.created_at DESC; + ` + err := r.db.Select(&spaces, query, userID) + if err != nil { + return nil, err + } + return spaces, nil +} + func (r *spaceRepository) AddMember(spaceID, userID string, role model.Role) error { query := `INSERT INTO space_members (space_id, user_id, role, joined_at) VALUES ($1, $2, $3, $4);` _, err := r.db.Exec(query, spaceID, userID, role, time.Now()) diff --git a/internal/routes/routes.go b/internal/routes/routes.go index c30bff5..b119788 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -96,7 +96,7 @@ func SetupRoutes(a *app.App) http.Handler { }) g.SubGroup("/shared-with-me", func(g *router.Group) { - g.Get("", spaceH.SpacesPage).Name("page.app.shared-with-me") + g.Get("", spaceH.SharedSpacesPage).Name("page.app.shared-with-me") }) g.SubGroup("/settings", func(g *router.Group) { diff --git a/internal/service/space.go b/internal/service/space.go index 76013c1..ee36a11 100644 --- a/internal/service/space.go +++ b/internal/service/space.go @@ -50,6 +50,24 @@ func (s *SpaceService) GetSpacesForUser(userID string) ([]*model.Space, error) { return spaces, nil } +// GetOwnedSpaces returns spaces owned by the user. +func (s *SpaceService) GetOwnedSpaces(userID string) ([]*model.Space, error) { + spaces, err := s.spaceRepo.ByOwnerID(userID) + if err != nil { + return nil, fmt.Errorf("failed to get owned spaces: %w", err) + } + return spaces, nil +} + +// GetSharedSpaces returns spaces shared with the user (not owned by them). +func (s *SpaceService) GetSharedSpaces(userID string) ([]*model.Space, error) { + spaces, err := s.spaceRepo.SharedWithUser(userID) + if err != nil { + return nil, fmt.Errorf("failed to get shared spaces: %w", err) + } + return spaces, nil +} + // GetSpace retrieves a single space by its ID. func (s *SpaceService) GetSpace(spaceID string) (*model.Space, error) { space, err := s.spaceRepo.ByID(spaceID) diff --git a/internal/ui/pages/spaces.templ b/internal/ui/pages/spaces.templ index a4d5f48..1acca63 100644 --- a/internal/ui/pages/spaces.templ +++ b/internal/ui/pages/spaces.templ @@ -11,8 +11,8 @@ templ Spaces(spaces []blocks.SpaceCardInfo) {
-

Spaces

-

Manage and monitor your expenses across collaborative spaces.

+

My Spaces

+

Manage and monitor your expenses.

@button.Button(button.Props{ @@ -32,3 +32,19 @@ templ Spaces(spaces []blocks.SpaceCardInfo) {
} } + +templ SharedSpaces(spaces []blocks.SpaceCardInfo) { + @layouts.App("Shared with me") { +
+
+

Shared with me

+

Spaces that others have shared with you.

+
+
+ for _, space := range spaces { + @blocks.SpaceCard(space) + } +
+
+ } +}