feat: create space
This commit is contained in:
parent
8e952455cd
commit
775177cba1
11 changed files with 200 additions and 3 deletions
|
|
@ -3,11 +3,13 @@ package handler
|
|||
import (
|
||||
"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/blocks"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/forms"
|
||||
"git.juancwu.dev/juancwu/budgit/internal/ui/pages"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
|
@ -66,3 +68,52 @@ func (h *spaceHandler) SpacesPage(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
ui.Render(w, r, pages.Spaces(cards))
|
||||
}
|
||||
|
||||
func (h *spaceHandler) CreateSpacePage(w http.ResponseWriter, r *http.Request) {
|
||||
ui.Render(w, r, pages.CreateSpace())
|
||||
}
|
||||
|
||||
func (h *spaceHandler) HandleCreateSpace(w http.ResponseWriter, r *http.Request) {
|
||||
spaceName := strings.TrimSpace(r.FormValue("name"))
|
||||
|
||||
if spaceName == "" {
|
||||
ui.Render(w, r, forms.CreateSpace("Space name can't be empty.", spaceName))
|
||||
return
|
||||
}
|
||||
|
||||
user := ctxkeys.User(r.Context())
|
||||
|
||||
isNameAvailable, err := h.spaceService.IsNameAvailable(spaceName, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to create new space", "error", err, "user_id", user.ID)
|
||||
ui.Render(w, r, forms.CreateSpace("Something went wrong. Please try again later.", spaceName))
|
||||
return
|
||||
}
|
||||
|
||||
if !isNameAvailable {
|
||||
ui.Render(w, r, forms.CreateSpace("Space name is not available. Please use another name.", spaceName))
|
||||
return
|
||||
}
|
||||
|
||||
sp, err := h.spaceService.CreateSpace(spaceName, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("failed to create new space", "error", err, "user_id", user.ID)
|
||||
ui.Render(w, r, forms.CreateSpace("Something went wrong. Please try again later.", spaceName))
|
||||
return
|
||||
}
|
||||
|
||||
ui.Render(w, r, forms.CreateSpaceSuccess(sp.ID))
|
||||
}
|
||||
|
||||
func (h *spaceHandler) SpaceOverviewPage(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 data", "error", err, "spaceID", spaceID)
|
||||
ui.Render(w, r, pages.NotFound())
|
||||
return
|
||||
}
|
||||
|
||||
ui.Render(w, r, pages.SpaceOverview(space.Name))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
|
||||
// RequireSpaceAccess validates that a user is a member of the space they are trying to access.
|
||||
// It expects a URL parameter named "spaceID".
|
||||
func RequireSpaceAccess(spaceService *service.SpaceService) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
func RequireSpaceAccess(spaceService *service.SpaceService) func(http.Handler) http.HandlerFunc {
|
||||
return func(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := ctxkeys.User(r.Context())
|
||||
if user == nil {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type SpaceRepository interface {
|
|||
RemoveMember(spaceID, userID string) error
|
||||
IsMember(spaceID, userID string) (bool, error)
|
||||
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
||||
GetMember(spaceID string, userID string) (*model.SpaceMember, error)
|
||||
UpdateName(spaceID, name string) error
|
||||
GetMemberCount(spaceID string) (int, error)
|
||||
|
||||
|
|
@ -125,6 +126,18 @@ func (r *spaceRepository) GetMembers(spaceID string) ([]*model.SpaceMemberWithPr
|
|||
return members, err
|
||||
}
|
||||
|
||||
func (r *spaceRepository) GetMember(spaceID, userID string) (*model.SpaceMember, error) {
|
||||
query := `SELECT * FROM space_members WHERE space_id = $1 AND user_id = $2;`
|
||||
|
||||
var member model.SpaceMember
|
||||
err := r.db.Get(&member, query, spaceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &member, nil
|
||||
}
|
||||
|
||||
func (r *spaceRepository) UpdateName(spaceID, name string) error {
|
||||
query := `UPDATE spaces SET name = $1, updated_at = $2 WHERE id = $3;`
|
||||
_, err := r.db.Exec(query, name, time.Now(), spaceID)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ func SetupRoutes(a *app.App) http.Handler {
|
|||
|
||||
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")
|
||||
g.Post("/create", spaceH.HandleCreateSpace).Name("action.app.spaces.create")
|
||||
g.SubGroup("/{spaceID}", func(g *router.Group) {
|
||||
spaceAccessMw := middleware.RequireSpaceAccess(a.SpaceService)
|
||||
g.Use(spaceAccessMw)
|
||||
g.Get("/overview", spaceH.SpaceOverviewPage).Name("page.app.spaces.space.overview")
|
||||
})
|
||||
})
|
||||
|
||||
g.SubGroup("/settings", func(g *router.Group) {
|
||||
|
|
|
|||
|
|
@ -102,3 +102,18 @@ func (s *SpaceService) GetMemberCount(spaceID string) (int, error) {
|
|||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *SpaceService) IsNameAvailable(name string, userID string) (bool, error) {
|
||||
spaces, err := s.GetSpacesForUser(userID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get spaces to check name availability: %w", err)
|
||||
}
|
||||
|
||||
for _, sp := range spaces {
|
||||
if sp.Name == name {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
48
internal/ui/forms/create_space.templ
Normal file
48
internal/ui/forms/create_space.templ
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package forms
|
||||
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/routeurl"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/form"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||
|
||||
templ CreateSpace(errMsg string, spaceName string) {
|
||||
<form hx-post={ routeurl.URL("action.app.spaces.create") }>
|
||||
@card.Card(card.Props{Class: "rounded-sm"}) {
|
||||
@card.Content(card.ContentProps{Class: "p-4"}) {
|
||||
@form.Item() {
|
||||
@form.Label(form.LabelProps{
|
||||
For: "space-name",
|
||||
}) {
|
||||
Space Name
|
||||
}
|
||||
@input.Input(input.Props{
|
||||
ID: "space-name",
|
||||
Type: input.TypeText,
|
||||
Placeholder: "My Expenses",
|
||||
Class: "rounded-sm",
|
||||
Name: "name",
|
||||
Value: spaceName,
|
||||
HasError: errMsg != "",
|
||||
Attributes: templ.Attributes{
|
||||
"autocomplete": "off",
|
||||
},
|
||||
})
|
||||
if errMsg != "" {
|
||||
@form.Message(form.MessageProps{Variant: form.MessageVariantError}) {
|
||||
{ errMsg }
|
||||
}
|
||||
}
|
||||
@form.Description() {
|
||||
You can always rename your space later in space settings.
|
||||
}
|
||||
}
|
||||
}
|
||||
@card.Footer() {
|
||||
@button.Button(button.Props{Class: "w-full", Type: button.TypeSubmit}) {
|
||||
Create
|
||||
}
|
||||
}
|
||||
}
|
||||
</form>
|
||||
}
|
||||
20
internal/ui/forms/create_space_success.templ
Normal file
20
internal/ui/forms/create_space_success.templ
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package forms
|
||||
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/routeurl"
|
||||
|
||||
templ CreateSpaceSuccess(spaceID string) {
|
||||
<div>
|
||||
@card.Card(card.Props{Class: "rounded-sm"}) {
|
||||
@card.Content(card.ContentProps{Class: "p-4"}) {
|
||||
<p class="text-xl text-success">Space successfully created!</p>
|
||||
}
|
||||
@card.Footer() {
|
||||
@button.Button(button.Props{Class: "w-full", Href: routeurl.URL("page.app.spaces.space.overview", "spaceID", spaceID)}) {
|
||||
Start tracking expenses
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
31
internal/ui/pages/create_space.templ
Normal file
31
internal/ui/pages/create_space.templ
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package pages
|
||||
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/blocks"
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/forms"
|
||||
|
||||
templ CreateSpace() {
|
||||
@layouts.Base(layouts.SEOProps{
|
||||
Title: "Create Space",
|
||||
Description: "Create space to manage and track expenses in a collaborative manner.",
|
||||
Path: ctxkeys.URLPath(ctx),
|
||||
}) {
|
||||
<div class="min-h-screen flex items-center justify-center p-4 relative">
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
@blocks.ThemeSwitcher()
|
||||
</div>
|
||||
<div class="w-full max-w-2xl md:grid md:grid-cols-2 md:gap-4">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-2xl">
|
||||
Create Space
|
||||
</h1>
|
||||
<p class="text-sm text-muted-foreground">This is where you or your group can track expenses.</p>
|
||||
</div>
|
||||
<div class="mt-4 md:mt-0">
|
||||
@forms.CreateSpace("", "")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
9
internal/ui/pages/page_app_spaces_space_overview.templ
Normal file
9
internal/ui/pages/page_app_spaces_space_overview.templ
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package pages
|
||||
|
||||
import "git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||
|
||||
templ SpaceOverview(spaceName string) {
|
||||
@layouts.App("Space Overview") {
|
||||
<div>space overview: { spaceName }</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ templ Spaces(spaces []blocks.SpaceCardInfo) {
|
|||
<div>
|
||||
@button.Button(button.Props{
|
||||
Class: "flex gap-2 items-center",
|
||||
Href: routeurl.URL("page.app.create-space"),
|
||||
Href: routeurl.URL("page.app.spaces.create"),
|
||||
}) {
|
||||
@icon.Plus()
|
||||
Create space
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue