feat: set timezone space level
Some checks failed
Deploy / build-and-deploy (push) Failing after 1m38s
Some checks failed
Deploy / build-and-deploy (push) Failing after 1m38s
This commit is contained in:
parent
945069052f
commit
64891638d1
10 changed files with 191 additions and 20 deletions
|
|
@ -87,8 +87,8 @@ func New(cfg *config.Config) (*App, error) {
|
||||||
inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService)
|
inviteService := service.NewInviteService(invitationRepository, spaceRepository, userRepository, emailService)
|
||||||
moneyAccountService := service.NewMoneyAccountService(moneyAccountRepository)
|
moneyAccountService := service.NewMoneyAccountService(moneyAccountRepository)
|
||||||
paymentMethodService := service.NewPaymentMethodService(paymentMethodRepository)
|
paymentMethodService := service.NewPaymentMethodService(paymentMethodRepository)
|
||||||
recurringExpenseService := service.NewRecurringExpenseService(recurringExpenseRepository, expenseRepository, profileRepository)
|
recurringExpenseService := service.NewRecurringExpenseService(recurringExpenseRepository, expenseRepository, profileRepository, spaceRepository)
|
||||||
recurringDepositService := service.NewRecurringDepositService(recurringDepositRepository, moneyAccountRepository, expenseService, profileRepository)
|
recurringDepositService := service.NewRecurringDepositService(recurringDepositRepository, moneyAccountRepository, expenseService, profileRepository, spaceRepository)
|
||||||
budgetService := service.NewBudgetService(budgetRepository)
|
budgetService := service.NewBudgetService(budgetRepository)
|
||||||
reportService := service.NewReportService(expenseRepository)
|
reportService := service.NewReportService(expenseRepository)
|
||||||
|
|
||||||
|
|
|
||||||
5
internal/db/migrations/00016_add_timezone_to_spaces.sql
Normal file
5
internal/db/migrations/00016_add_timezone_to_spaces.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE spaces ADD COLUMN timezone TEXT;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE spaces DROP COLUMN IF EXISTS timezone;
|
||||||
|
|
@ -1177,6 +1177,46 @@ func (h *SpaceHandler) UpdateSpaceName(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SpaceHandler) UpdateSpaceTimezone(w http.ResponseWriter, r *http.Request) {
|
||||||
|
spaceID := r.PathValue("spaceID")
|
||||||
|
user := ctxkeys.User(r.Context())
|
||||||
|
|
||||||
|
space, err := h.spaceService.GetSpace(spaceID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Space not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if space.OwnerID != user.ID {
|
||||||
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
ui.RenderError(w, r, "Bad Request", http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tz := r.FormValue("timezone")
|
||||||
|
if tz == "" {
|
||||||
|
ui.RenderError(w, r, "Timezone is required", http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.spaceService.UpdateSpaceTimezone(spaceID, tz); err != nil {
|
||||||
|
if err == service.ErrInvalidTimezone {
|
||||||
|
ui.RenderError(w, r, "Invalid timezone", http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("failed to update space timezone", "error", err, "space_id", spaceID)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("HX-Refresh", "true")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SpaceHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
|
func (h *SpaceHandler) RemoveMember(w http.ResponseWriter, r *http.Request) {
|
||||||
spaceID := r.PathValue("spaceID")
|
spaceID := r.PathValue("spaceID")
|
||||||
userID := r.PathValue("userID")
|
userID := r.PathValue("userID")
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,24 @@ type Space struct {
|
||||||
ID string `db:"id"`
|
ID string `db:"id"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
OwnerID string `db:"owner_id"`
|
OwnerID string `db:"owner_id"`
|
||||||
|
Timezone *string `db:"timezone"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Location returns the *time.Location for this space's timezone.
|
||||||
|
// Returns nil if timezone is not set, so callers can distinguish "not set" from "UTC".
|
||||||
|
func (s *Space) Location() *time.Location {
|
||||||
|
if s.Timezone == nil || *s.Timezone == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
loc, err := time.LoadLocation(*s.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
type SpaceMember struct {
|
type SpaceMember struct {
|
||||||
SpaceID string `db:"space_id"`
|
SpaceID string `db:"space_id"`
|
||||||
UserID string `db:"user_id"`
|
UserID string `db:"user_id"`
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ type SpaceRepository interface {
|
||||||
IsMember(spaceID, userID string) (bool, error)
|
IsMember(spaceID, userID string) (bool, error)
|
||||||
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
||||||
UpdateName(spaceID, name string) error
|
UpdateName(spaceID, name string) error
|
||||||
|
UpdateTimezone(spaceID, timezone string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type spaceRepository struct {
|
type spaceRepository struct {
|
||||||
|
|
@ -128,3 +129,9 @@ func (r *spaceRepository) UpdateName(spaceID, name string) error {
|
||||||
_, err := r.db.Exec(query, name, time.Now(), spaceID)
|
_, err := r.db.Exec(query, name, time.Now(), spaceID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *spaceRepository) UpdateTimezone(spaceID, timezone string) error {
|
||||||
|
query := `UPDATE spaces SET timezone = $1, updated_at = $2 WHERE id = $3;`
|
||||||
|
_, err := r.db.Exec(query, timezone, time.Now(), spaceID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,10 @@ func SetupRoutes(a *app.App) http.Handler {
|
||||||
updateSpaceNameWithAuth := middleware.RequireAuth(updateSpaceNameHandler)
|
updateSpaceNameWithAuth := middleware.RequireAuth(updateSpaceNameHandler)
|
||||||
mux.Handle("PATCH /app/spaces/{spaceID}/settings/name", crudLimiter(updateSpaceNameWithAuth))
|
mux.Handle("PATCH /app/spaces/{spaceID}/settings/name", crudLimiter(updateSpaceNameWithAuth))
|
||||||
|
|
||||||
|
updateSpaceTimezoneHandler := middleware.RequireSpaceAccess(a.SpaceService)(space.UpdateSpaceTimezone)
|
||||||
|
updateSpaceTimezoneWithAuth := middleware.RequireAuth(updateSpaceTimezoneHandler)
|
||||||
|
mux.Handle("PATCH /app/spaces/{spaceID}/settings/timezone", crudLimiter(updateSpaceTimezoneWithAuth))
|
||||||
|
|
||||||
removeMemberHandler := middleware.RequireSpaceAccess(a.SpaceService)(space.RemoveMember)
|
removeMemberHandler := middleware.RequireSpaceAccess(a.SpaceService)(space.RemoveMember)
|
||||||
removeMemberWithAuth := middleware.RequireAuth(removeMemberHandler)
|
removeMemberWithAuth := middleware.RequireAuth(removeMemberHandler)
|
||||||
mux.Handle("DELETE /app/spaces/{spaceID}/members/{userID}", crudLimiter(removeMemberWithAuth))
|
mux.Handle("DELETE /app/spaces/{spaceID}/members/{userID}", crudLimiter(removeMemberWithAuth))
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,16 @@ type RecurringDepositService struct {
|
||||||
accountRepo repository.MoneyAccountRepository
|
accountRepo repository.MoneyAccountRepository
|
||||||
expenseService *ExpenseService
|
expenseService *ExpenseService
|
||||||
profileRepo repository.ProfileRepository
|
profileRepo repository.ProfileRepository
|
||||||
|
spaceRepo repository.SpaceRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecurringDepositService(recurringRepo repository.RecurringDepositRepository, accountRepo repository.MoneyAccountRepository, expenseService *ExpenseService, profileRepo repository.ProfileRepository) *RecurringDepositService {
|
func NewRecurringDepositService(recurringRepo repository.RecurringDepositRepository, accountRepo repository.MoneyAccountRepository, expenseService *ExpenseService, profileRepo repository.ProfileRepository, spaceRepo repository.SpaceRepository) *RecurringDepositService {
|
||||||
return &RecurringDepositService{
|
return &RecurringDepositService{
|
||||||
recurringRepo: recurringRepo,
|
recurringRepo: recurringRepo,
|
||||||
accountRepo: accountRepo,
|
accountRepo: accountRepo,
|
||||||
expenseService: expenseService,
|
expenseService: expenseService,
|
||||||
profileRepo: profileRepo,
|
profileRepo: profileRepo,
|
||||||
|
spaceRepo: spaceRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,8 +167,8 @@ func (s *RecurringDepositService) ProcessDueRecurrences(now time.Time) error {
|
||||||
|
|
||||||
tzCache := make(map[string]*time.Location)
|
tzCache := make(map[string]*time.Location)
|
||||||
for _, rd := range dues {
|
for _, rd := range dues {
|
||||||
userNow := s.getUserNow(rd.CreatedBy, now, tzCache)
|
localNow := s.getLocalNow(rd.SpaceID, rd.CreatedBy, now, tzCache)
|
||||||
if err := s.processRecurrence(rd, userNow); err != nil {
|
if err := s.processRecurrence(rd, localNow); err != nil {
|
||||||
slog.Error("failed to process recurring deposit", "id", rd.ID, "error", err)
|
slog.Error("failed to process recurring deposit", "id", rd.ID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -181,16 +183,32 @@ func (s *RecurringDepositService) ProcessDueRecurrencesForSpace(spaceID string,
|
||||||
|
|
||||||
tzCache := make(map[string]*time.Location)
|
tzCache := make(map[string]*time.Location)
|
||||||
for _, rd := range dues {
|
for _, rd := range dues {
|
||||||
userNow := s.getUserNow(rd.CreatedBy, now, tzCache)
|
localNow := s.getLocalNow(rd.SpaceID, rd.CreatedBy, now, tzCache)
|
||||||
if err := s.processRecurrence(rd, userNow); err != nil {
|
if err := s.processRecurrence(rd, localNow); err != nil {
|
||||||
slog.Error("failed to process recurring deposit", "id", rd.ID, "error", err)
|
slog.Error("failed to process recurring deposit", "id", rd.ID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *RecurringDepositService) getUserNow(userID string, now time.Time, cache map[string]*time.Location) time.Time {
|
// getLocalNow resolves the effective timezone for a recurring deposit.
|
||||||
if loc, ok := cache[userID]; ok {
|
// Resolution order: space timezone → user profile timezone → UTC.
|
||||||
|
func (s *RecurringDepositService) getLocalNow(spaceID, userID string, now time.Time, cache map[string]*time.Location) time.Time {
|
||||||
|
spaceKey := "space:" + spaceID
|
||||||
|
if loc, ok := cache[spaceKey]; ok {
|
||||||
|
return now.In(loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
space, err := s.spaceRepo.ByID(spaceID)
|
||||||
|
if err == nil && space != nil {
|
||||||
|
if loc := space.Location(); loc != nil {
|
||||||
|
cache[spaceKey] = loc
|
||||||
|
return now.In(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey := "user:" + userID
|
||||||
|
if loc, ok := cache[userKey]; ok {
|
||||||
return now.In(loc)
|
return now.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +217,7 @@ func (s *RecurringDepositService) getUserNow(userID string, now time.Time, cache
|
||||||
if err == nil && profile != nil {
|
if err == nil && profile != nil {
|
||||||
loc = profile.Location()
|
loc = profile.Location()
|
||||||
}
|
}
|
||||||
cache[userID] = loc
|
cache[userKey] = loc
|
||||||
return now.In(loc)
|
return now.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,15 @@ type RecurringExpenseService struct {
|
||||||
recurringRepo repository.RecurringExpenseRepository
|
recurringRepo repository.RecurringExpenseRepository
|
||||||
expenseRepo repository.ExpenseRepository
|
expenseRepo repository.ExpenseRepository
|
||||||
profileRepo repository.ProfileRepository
|
profileRepo repository.ProfileRepository
|
||||||
|
spaceRepo repository.SpaceRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecurringExpenseService(recurringRepo repository.RecurringExpenseRepository, expenseRepo repository.ExpenseRepository, profileRepo repository.ProfileRepository) *RecurringExpenseService {
|
func NewRecurringExpenseService(recurringRepo repository.RecurringExpenseRepository, expenseRepo repository.ExpenseRepository, profileRepo repository.ProfileRepository, spaceRepo repository.SpaceRepository) *RecurringExpenseService {
|
||||||
return &RecurringExpenseService{
|
return &RecurringExpenseService{
|
||||||
recurringRepo: recurringRepo,
|
recurringRepo: recurringRepo,
|
||||||
expenseRepo: expenseRepo,
|
expenseRepo: expenseRepo,
|
||||||
profileRepo: profileRepo,
|
profileRepo: profileRepo,
|
||||||
|
spaceRepo: spaceRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,8 +182,8 @@ func (s *RecurringExpenseService) ProcessDueRecurrences(now time.Time) error {
|
||||||
|
|
||||||
tzCache := make(map[string]*time.Location)
|
tzCache := make(map[string]*time.Location)
|
||||||
for _, re := range dues {
|
for _, re := range dues {
|
||||||
userNow := s.getUserNow(re.CreatedBy, now, tzCache)
|
localNow := s.getLocalNow(re.SpaceID, re.CreatedBy, now, tzCache)
|
||||||
if err := s.processRecurrence(re, userNow); err != nil {
|
if err := s.processRecurrence(re, localNow); err != nil {
|
||||||
slog.Error("failed to process recurring expense", "id", re.ID, "error", err)
|
slog.Error("failed to process recurring expense", "id", re.ID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,8 +198,8 @@ func (s *RecurringExpenseService) ProcessDueRecurrencesForSpace(spaceID string,
|
||||||
|
|
||||||
tzCache := make(map[string]*time.Location)
|
tzCache := make(map[string]*time.Location)
|
||||||
for _, re := range dues {
|
for _, re := range dues {
|
||||||
userNow := s.getUserNow(re.CreatedBy, now, tzCache)
|
localNow := s.getLocalNow(re.SpaceID, re.CreatedBy, now, tzCache)
|
||||||
if err := s.processRecurrence(re, userNow); err != nil {
|
if err := s.processRecurrence(re, localNow); err != nil {
|
||||||
slog.Error("failed to process recurring expense", "id", re.ID, "error", err)
|
slog.Error("failed to process recurring expense", "id", re.ID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -253,10 +255,24 @@ func (s *RecurringExpenseService) processRecurrence(re *model.RecurringExpense,
|
||||||
return s.recurringRepo.UpdateNextOccurrence(re.ID, re.NextOccurrence)
|
return s.recurringRepo.UpdateNextOccurrence(re.ID, re.NextOccurrence)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserNow converts the server time to the user's local time based on their
|
// getLocalNow resolves the effective timezone for a recurring expense.
|
||||||
// profile timezone setting. Falls back to UTC if no timezone is set.
|
// Resolution order: space timezone → user profile timezone → UTC.
|
||||||
func (s *RecurringExpenseService) getUserNow(userID string, now time.Time, cache map[string]*time.Location) time.Time {
|
func (s *RecurringExpenseService) getLocalNow(spaceID, userID string, now time.Time, cache map[string]*time.Location) time.Time {
|
||||||
if loc, ok := cache[userID]; ok {
|
spaceKey := "space:" + spaceID
|
||||||
|
if loc, ok := cache[spaceKey]; ok {
|
||||||
|
return now.In(loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
space, err := s.spaceRepo.ByID(spaceID)
|
||||||
|
if err == nil && space != nil {
|
||||||
|
if loc := space.Location(); loc != nil {
|
||||||
|
cache[spaceKey] = loc
|
||||||
|
return now.In(loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey := "user:" + userID
|
||||||
|
if loc, ok := cache[userKey]; ok {
|
||||||
return now.In(loc)
|
return now.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,7 +281,7 @@ func (s *RecurringExpenseService) getUserNow(userID string, now time.Time, cache
|
||||||
if err == nil && profile != nil {
|
if err == nil && profile != nil {
|
||||||
loc = profile.Location()
|
loc = profile.Location()
|
||||||
}
|
}
|
||||||
cache[userID] = loc
|
cache[userKey] = loc
|
||||||
return now.In(loc)
|
return now.In(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,3 +110,11 @@ func (s *SpaceService) UpdateSpaceName(spaceID, name string) error {
|
||||||
}
|
}
|
||||||
return s.spaceRepo.UpdateName(spaceID, name)
|
return s.spaceRepo.UpdateName(spaceID, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSpaceTimezone updates the timezone of a space.
|
||||||
|
func (s *SpaceService) UpdateSpaceTimezone(spaceID, timezone string) error {
|
||||||
|
if _, err := time.LoadLocation(timezone); err != nil {
|
||||||
|
return ErrInvalidTimezone
|
||||||
|
}
|
||||||
|
return s.spaceRepo.UpdateTimezone(spaceID, timezone)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ package pages
|
||||||
import (
|
import (
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
"git.juancwu.dev/juancwu/budgit/internal/ctxkeys"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/model"
|
"git.juancwu.dev/juancwu/budgit/internal/model"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/timezone"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/card"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/csrf"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/dialog"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/form"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/label"
|
||||||
|
"git.juancwu.dev/juancwu/budgit/internal/ui/components/selectbox"
|
||||||
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
"git.juancwu.dev/juancwu/budgit/internal/ui/layouts"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -55,6 +59,61 @@ templ SpaceSettingsPage(space *model.Space, members []*model.SpaceMemberWithProf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Timezone Section
|
||||||
|
@card.Card() {
|
||||||
|
@card.Header() {
|
||||||
|
@card.Title() {
|
||||||
|
Timezone
|
||||||
|
}
|
||||||
|
@card.Description() {
|
||||||
|
if isOwner {
|
||||||
|
Set a timezone for this space. Recurring expenses and reports will use this timezone.
|
||||||
|
} else {
|
||||||
|
The timezone used for recurring expenses and reports in this space.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@card.Content() {
|
||||||
|
if isOwner {
|
||||||
|
<form
|
||||||
|
hx-patch={ "/app/spaces/" + space.ID + "/settings/timezone" }
|
||||||
|
hx-swap="none"
|
||||||
|
class="space-y-4"
|
||||||
|
>
|
||||||
|
@csrf.Token()
|
||||||
|
@form.Item() {
|
||||||
|
@label.Label(label.Props{
|
||||||
|
For: "timezone",
|
||||||
|
Class: "block mb-2",
|
||||||
|
}) {
|
||||||
|
Timezone
|
||||||
|
}
|
||||||
|
@selectbox.SelectBox(selectbox.Props{ID: "space-timezone-select"}) {
|
||||||
|
@selectbox.Trigger(selectbox.TriggerProps{Name: "timezone"}) {
|
||||||
|
@selectbox.Value(selectbox.ValueProps{Placeholder: "Select timezone"})
|
||||||
|
}
|
||||||
|
@selectbox.Content(selectbox.ContentProps{SearchPlaceholder: "Search timezones..."}) {
|
||||||
|
for _, tz := range timezone.CommonTimezones() {
|
||||||
|
@selectbox.Item(selectbox.ItemProps{Value: tz.Value, Selected: space.Timezone != nil && tz.Value == *space.Timezone}) {
|
||||||
|
{ tz.Label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@button.Submit() {
|
||||||
|
Save Timezone
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
} else {
|
||||||
|
if space.Timezone != nil && *space.Timezone != "" {
|
||||||
|
<p class="text-sm">{ *space.Timezone }</p>
|
||||||
|
} else {
|
||||||
|
<p class="text-sm text-muted-foreground">Not set (uses your timezone)</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Members Section
|
// Members Section
|
||||||
@card.Card() {
|
@card.Card() {
|
||||||
@card.Header() {
|
@card.Header() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue