feat: shared with me page

This commit is contained in:
juancwu 2026-04-12 16:55:38 +00:00
commit 48cae4957e
5 changed files with 96 additions and 8 deletions

View file

@ -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) {

View file

@ -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())

View file

@ -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) {

View file

@ -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)

View file

@ -11,8 +11,8 @@ templ Spaces(spaces []blocks.SpaceCardInfo) {
<div class="container px-6 py-8 mx-auto">
<div class="mb-8 w-full flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold">Spaces</h1>
<p class="text-muted-foreground mt-2">Manage and monitor your expenses across collaborative spaces.</p>
<h1 class="text-3xl font-bold">My Spaces</h1>
<p class="text-muted-foreground mt-2">Manage and monitor your expenses.</p>
</div>
<div>
@button.Button(button.Props{
@ -32,3 +32,19 @@ templ Spaces(spaces []blocks.SpaceCardInfo) {
</div>
}
}
templ SharedSpaces(spaces []blocks.SpaceCardInfo) {
@layouts.App("Shared with me") {
<div class="container px-6 py-8 mx-auto">
<div class="mb-8">
<h1 class="text-3xl font-bold">Shared with me</h1>
<p class="text-muted-foreground mt-2">Spaces that others have shared with you.</p>
</div>
<div class="">
for _, space := range spaces {
@blocks.SpaceCard(space)
}
</div>
</div>
}
}