diff --git a/internal/handler/space.go b/internal/handler/space.go index 91ddfd1..dfc2b33 100644 --- a/internal/handler/space.go +++ b/internal/handler/space.go @@ -333,6 +333,108 @@ func (h *spaceHandler) SpaceAccountTransactionsPage(w http.ResponseWriter, r *ht })) } +func (h *spaceHandler) SpaceSettingsPage(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + + space, err := h.spaceService.GetSpace(spaceID) + if err != nil { + slog.Error("failed to fetch space", "error", err, "space_id", spaceID) + ui.Render(w, r, pages.NotFound()) + return + } + + user := ctxkeys.User(r.Context()) + canDelete := user != nil && user.ID == space.OwnerID + + ui.Render(w, r, pages.SpaceSettingsPage(pages.SpaceSettingsPageProps{ + SpaceID: space.ID, + SpaceName: space.Name, + CanDelete: canDelete, + UpdateForm: forms.UpdateSpaceProps{ + SpaceID: space.ID, + Name: space.Name, + }, + })) +} + +func (h *spaceHandler) HandleRenameSpace(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + + space, err := h.spaceService.GetSpace(spaceID) + if err != nil { + ui.RenderError(w, r, "Space not found", http.StatusNotFound) + return + } + + user := ctxkeys.User(r.Context()) + if user == nil || user.ID != space.OwnerID { + ui.RenderError(w, r, "Forbidden", http.StatusForbidden) + return + } + + nameInput := strings.TrimSpace(r.FormValue("name")) + formProps := forms.UpdateSpaceProps{ + SpaceID: spaceID, + Name: nameInput, + } + + if nameInput == "" { + formProps.NameErr = "Space name is required." + ui.Render(w, r, forms.UpdateSpace(formProps)) + return + } + + if !strings.EqualFold(nameInput, space.Name) { + available, err := h.spaceService.IsNameAvailable(nameInput, user.ID) + if err != nil { + slog.Error("failed to check name availability", "error", err, "user_id", user.ID) + formProps.GeneralErr = "Something went wrong. Please try again." + ui.Render(w, r, forms.UpdateSpace(formProps)) + return + } + if !available { + formProps.NameErr = "You already have a space with this name." + ui.Render(w, r, forms.UpdateSpace(formProps)) + return + } + } + + if err := h.spaceService.UpdateSpaceName(spaceID, nameInput); err != nil { + slog.Error("failed to rename space", "error", err, "space_id", spaceID) + formProps.GeneralErr = "Something went wrong. Please try again." + ui.Render(w, r, forms.UpdateSpace(formProps)) + return + } + + formProps.SuccessMsg = "Space name updated." + ui.Render(w, r, forms.UpdateSpace(formProps)) +} + +func (h *spaceHandler) HandleDeleteSpace(w http.ResponseWriter, r *http.Request) { + spaceID := r.PathValue("spaceID") + + space, err := h.spaceService.GetSpace(spaceID) + if err != nil { + ui.RenderError(w, r, "Space not found", http.StatusNotFound) + return + } + + user := ctxkeys.User(r.Context()) + if user == nil || user.ID != space.OwnerID { + ui.RenderError(w, r, "Forbidden", http.StatusForbidden) + return + } + + if err := h.spaceService.DeleteSpace(spaceID); err != nil { + slog.Error("failed to delete space", "error", err, "space_id", spaceID) + ui.RenderError(w, r, "Failed to delete space", http.StatusInternalServerError) + return + } + + w.Header().Set("HX-Redirect", routeurl.URL("page.app.spaces")) + w.WriteHeader(http.StatusOK) +} + func (h *spaceHandler) SpaceAccountSettingsPage(w http.ResponseWriter, r *http.Request) { spaceID := r.PathValue("spaceID") accountID := r.PathValue("accountID") diff --git a/internal/routes/routes.go b/internal/routes/routes.go index 5dbbf27..52d63ea 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -92,6 +92,9 @@ func SetupRoutes(a *app.App) http.Handler { spaceAccessMw := middleware.RequireSpaceAccess(a.SpaceService) g.Use(spaceAccessMw) g.Get("/overview", spaceH.SpaceOverviewPage).Name("page.app.spaces.space.overview") + g.Get("/settings", spaceH.SpaceSettingsPage).Name("page.app.spaces.space.settings") + g.Post("/settings/rename", spaceH.HandleRenameSpace).Name("action.app.spaces.space.settings.rename") + g.Post("/settings/delete", spaceH.HandleDeleteSpace).Name("action.app.spaces.space.settings.delete") g.Get("/accounts/create", spaceH.SpaceCreateAccountPage).Name("page.app.spaces.space.accounts.create") g.Post("/accounts/create", spaceH.HandleCreateAccount).Name("action.app.spaces.space.accounts.create") diff --git a/internal/ui/forms/update_space.templ b/internal/ui/forms/update_space.templ new file mode 100644 index 0000000..c8e8271 --- /dev/null +++ b/internal/ui/forms/update_space.templ @@ -0,0 +1,75 @@ +package forms + +import "git.juancwu.dev/juancwu/budgit/internal/routeurl" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/card" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/form" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/input" + +type UpdateSpaceProps struct { + SpaceID string + + Name string + + NameErr string + GeneralErr string + SuccessMsg string +} + +templ UpdateSpace(props UpdateSpaceProps) { +
+} diff --git a/internal/ui/pages/space_overview.templ b/internal/ui/pages/space_overview.templ index f83a224..7c5b5a8 100644 --- a/internal/ui/pages/space_overview.templ +++ b/internal/ui/pages/space_overview.templ @@ -64,6 +64,16 @@ templ spaceSpecificSidebarContent(spaceID string) { Space Overview } } + @sidebar.MenuItem() { + @sidebar.MenuButton(sidebar.MenuButtonProps{ + Href: routeurl.URL("page.app.spaces.space.settings", "spaceID", spaceID), + IsActive: ctxkeys.URLPath(ctx) == routeurl.URL("page.app.spaces.space.settings", "spaceID", spaceID), + Tooltip: "Space Settings", + }) { + @icon.Settings() + Settings + } + } } } } diff --git a/internal/ui/pages/space_settings.templ b/internal/ui/pages/space_settings.templ new file mode 100644 index 0000000..1b5515d --- /dev/null +++ b/internal/ui/pages/space_settings.templ @@ -0,0 +1,82 @@ +package pages + +import "git.juancwu.dev/juancwu/budgit/internal/routeurl" +import "git.juancwu.dev/juancwu/budgit/internal/ui/forms" +import "git.juancwu.dev/juancwu/budgit/internal/ui/layouts" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/button" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/card" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog" +import "git.juancwu.dev/juancwu/budgit/internal/ui/components/icon" + +type SpaceSettingsPageProps struct { + SpaceID string + SpaceName string + CanDelete bool + UpdateForm forms.UpdateSpaceProps +} + +templ SpaceSettingsPage(props SpaceSettingsPageProps) { + @layouts.App("Space Settings", spaceOverviewSidebarContent(), spaceSpecificSidebarContent(props.SpaceID)) { ++ Manage settings for { props.SpaceName }. +
+