Merge branch 'feat/rename-dashboard'
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m14s
All checks were successful
Deploy / build-and-deploy (push) Successful in 2m14s
This commit is contained in:
commit
85ecd67bc1
9 changed files with 52 additions and 148 deletions
|
|
@ -1,59 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
|
||||
)
|
||||
|
||||
type dashboardHandler struct {
|
||||
spaceService *service.SpaceService
|
||||
expenseService *service.ExpenseService
|
||||
}
|
||||
|
||||
func NewDashboardHandler(ss *service.SpaceService, es *service.ExpenseService) *dashboardHandler {
|
||||
return &dashboardHandler{
|
||||
spaceService: ss,
|
||||
expenseService: es,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *dashboardHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
spaces, err := h.spaceService.GetSpacesForUser(user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get spaces for user", "error", err, "user_id", user.ID)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ui.Render(w, r, pages.Dashboard(spaces))
|
||||
}
|
||||
|
||||
func (h *dashboardHandler) CreateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" {
|
||||
w.Header().Set("HX-Reswap", "none")
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
fmt.Fprint(w, `<p id="create-space-error" hx-swap-oob="true" class="text-sm text-destructive">Space name is required</p>`)
|
||||
return
|
||||
}
|
||||
|
||||
space, err := h.spaceService.CreateSpace(name, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to create space", "error", err, "user_id", user.ID)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Redirect", "/app/spaces/"+space.ID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/repository"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/service"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDashboardHandler_DashboardPage(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
testutil.CreateTestSpace(t, dbi.DB, user.ID, "My Space")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodGet, "/app/dashboard", user, profile, nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.DashboardPage(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardHandler_CreateSpace(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {"New Space"}})
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateSpace(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.True(t, strings.HasPrefix(w.Header().Get("HX-Redirect"), "/app/spaces/"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardHandler_CreateSpace_EmptyName(t *testing.T) {
|
||||
testutil.ForEachDB(t, func(t *testing.T, dbi testutil.DBInfo) {
|
||||
spaceRepo := repository.NewSpaceRepository(dbi.DB)
|
||||
expenseRepo := repository.NewExpenseRepository(dbi.DB)
|
||||
spaceSvc := service.NewSpaceService(spaceRepo)
|
||||
expenseSvc := service.NewExpenseService(expenseRepo)
|
||||
h := NewDashboardHandler(spaceSvc, expenseSvc)
|
||||
|
||||
user, profile := testutil.CreateTestUserWithProfile(t, dbi.DB, "test@example.com", "Test User")
|
||||
|
||||
req := testutil.NewAuthenticatedRequest(t, http.MethodPost, "/app/dashboard/spaces", user, profile, url.Values{"name": {""}})
|
||||
w := httptest.NewRecorder()
|
||||
h.CreateSpace(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
|
||||
})
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
|
|
@ -49,6 +50,40 @@ func NewSpaceHandler(ss *service.SpaceService, ts *service.TagService, sls *serv
|
|||
}
|
||||
}
|
||||
|
||||
func (h *SpaceHandler) DashboardPage(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
spaces, err := h.spaceService.GetSpacesForUser(user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to get spaces for user", "error", err, "user_id", user.ID)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ui.Render(w, r, pages.Dashboard(spaces))
|
||||
}
|
||||
|
||||
func (h *SpaceHandler) CreateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" {
|
||||
w.Header().Set("HX-Reswap", "none")
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
fmt.Fprint(w, `<p id="create-space-error" hx-swap-oob="true" class="text-sm text-destructive">Space name is required</p>`)
|
||||
return
|
||||
}
|
||||
|
||||
space, err := h.spaceService.CreateSpace(name, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to create space", "error", err, "user_id", user.ID)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Redirect", "/app/spaces/"+space.ID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// getExpenseForSpace fetches an expense and verifies it belongs to the given space.
|
||||
func (h *SpaceHandler) getExpenseForSpace(w http.ResponseWriter, spaceID, expenseID string) *model.Expense {
|
||||
exp, err := h.expenseService.GetExpense(expenseID)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package middleware
|
|||
|
||||
import "net/http"
|
||||
|
||||
func Redirect(path string) http.Handler {
|
||||
func Redirect(path string) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Redirect", path)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
func SetupRoutes(a *app.App) http.Handler {
|
||||
auth := handler.NewAuthHandler(a.AuthService, a.InviteService, a.SpaceService)
|
||||
home := handler.NewHomeHandler()
|
||||
dashboard := handler.NewDashboardHandler(a.SpaceService, a.ExpenseService)
|
||||
settings := handler.NewSettingsHandler(a.AuthService, a.UserService)
|
||||
space := handler.NewSpaceHandler(a.SpaceService, a.TagService, a.ShoppingListService, a.ExpenseService, a.InviteService, a.MoneyAccountService, a.PaymentMethodService, a.RecurringExpenseService, a.BudgetService, a.ReportService)
|
||||
|
||||
|
|
@ -55,8 +54,9 @@ func SetupRoutes(a *app.App) http.Handler {
|
|||
mux.HandleFunc("GET /auth/onboarding", middleware.RequireAuth(auth.OnboardingPage))
|
||||
mux.Handle("POST /auth/onboarding", crudLimiter(http.HandlerFunc(middleware.RequireAuth(auth.CompleteOnboarding))))
|
||||
|
||||
mux.HandleFunc("GET /app/dashboard", middleware.RequireAuth(dashboard.DashboardPage))
|
||||
mux.Handle("POST /app/spaces", crudLimiter(http.HandlerFunc(middleware.RequireAuth(dashboard.CreateSpace))))
|
||||
mux.HandleFunc("GET /app/dashboard", middleware.Redirect("/app/spaces"))
|
||||
mux.HandleFunc("GET /app/spaces", middleware.RequireAuth(space.DashboardPage))
|
||||
mux.Handle("POST /app/spaces", crudLimiter(middleware.RequireAuth(space.CreateSpace)))
|
||||
mux.HandleFunc("GET /app/settings", middleware.RequireAuth(settings.SettingsPage))
|
||||
mux.HandleFunc("POST /app/settings/password", authRateLimiter(middleware.RequireAuth(settings.SetPassword)))
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ templ App(title string) {
|
|||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Size: sidebar.MenuButtonSizeLg,
|
||||
Href: "/app/dashboard",
|
||||
Href: "/app/spaces",
|
||||
}) {
|
||||
@icon.LayoutDashboard()
|
||||
<div class="flex flex-col">
|
||||
|
|
@ -45,12 +45,12 @@ templ App(title string) {
|
|||
@sidebar.Menu() {
|
||||
@sidebar.MenuItem() {
|
||||
@sidebar.MenuButton(sidebar.MenuButtonProps{
|
||||
Href: "/app/dashboard",
|
||||
IsActive: ctxkeys.URLPath(ctx) == "/app/dashboard",
|
||||
Tooltip: "Dashboard",
|
||||
Href: "/app/spaces",
|
||||
IsActive: ctxkeys.URLPath(ctx) == "/app/spaces",
|
||||
Tooltip: "Spaces",
|
||||
}) {
|
||||
@icon.House(icon.Props{Class: "size-4"})
|
||||
<span>Dashboard</span>
|
||||
<span>Spaces</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ templ Space(title string, space *model.Space) {
|
|||
@icon.LayoutDashboard()
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-bold">{ cfg.AppName }</span>
|
||||
<span class="text-xs text-muted-foreground">Back to Dashboard</span>
|
||||
<span class="text-xs text-muted-foreground">Back to Spaces</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import (
|
|||
)
|
||||
|
||||
templ Dashboard(spaces []*model.Space) {
|
||||
@layouts.App("Dashboard") {
|
||||
@layouts.App("Spaces") {
|
||||
<div class="container max-w-7xl px-6 py-8">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Dashboard</h1>
|
||||
<h1 class="text-3xl font-bold">Spaces</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Welcome back! Here's an overview of your spaces.
|
||||
Welcome back!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -32,12 +32,10 @@ templ Dashboard(spaces []*model.Space) {
|
|||
{ space.Name }
|
||||
}
|
||||
@card.Description() {
|
||||
Manage expenses and shopping lists in this space.
|
||||
Manage expenses in this space.
|
||||
}
|
||||
}
|
||||
@card.Content() {
|
||||
// You could add some summary stats here later
|
||||
}
|
||||
@card.Content()
|
||||
}
|
||||
</a>
|
||||
}
|
||||
|
|
@ -57,7 +55,7 @@ templ Dashboard(spaces []*model.Space) {
|
|||
Create Space
|
||||
}
|
||||
@dialog.Description() {
|
||||
Create a new space to organize expenses and shopping lists.
|
||||
Create a new space to organize expenses and more.
|
||||
}
|
||||
}
|
||||
<form hx-post="/app/spaces" hx-swap="none" class="space-y-4">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ templ Forbidden() {
|
|||
<h2 class="text-2xl font-semibold mb-2">Access Denied</h2>
|
||||
<p class="text-muted-foreground mb-8">You do not have permission to access this page.</p>
|
||||
<a href="/app/dashboard" class="text-primary hover:underline">
|
||||
← Back to Dashboard
|
||||
← Back to Spaces
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue