diff --git a/internal/db/migrate/migrate.go b/internal/db/migrate/migrate.go index 4393e3d..c6c1aa7 100644 --- a/internal/db/migrate/migrate.go +++ b/internal/db/migrate/migrate.go @@ -498,13 +498,22 @@ func ResetDatabase(ctx context.Context, cfg *config.Config) error { conn := db.NewDB(cfg.DB) defer func() { _ = conn.Close() }() - models := conn.RegisterModels() + conn.RegisterModels() - for _, model := range models { - if err := conn.ResetModel(ctx, model); err != nil { - return errors.Wrap(err, "reset model") - } + err = RunMigrations(ctx, cfg, "rollback", "all") + if err != nil { + return errors.Wrap(err, "RunMigrations: rollback") } + err = RunMigrations(ctx, cfg, "up", "all") + if err != nil { + return errors.Wrap(err, "RunMigrations: up") + } + + // for _, model := range models { + // if err := conn.ResetModel(ctx, model); err != nil { + // return errors.Wrap(err, "reset model") + // } + // } fmt.Println("✅ Database reset complete") return nil diff --git a/internal/db/migrations/20260210182212_add_leagues.go b/internal/db/migrations/20260210182212_add_leagues.go index 53599d8..bf2de8b 100644 --- a/internal/db/migrations/20260210182212_add_leagues.go +++ b/internal/db/migrations/20260210182212_add_leagues.go @@ -55,13 +55,7 @@ func init() { if err != nil { return err } - - // Remove slap_version column from seasons table - _, err = conn.NewDropColumn(). - Model((*db.Season)(nil)). - ColumnExpr("slap_version"). - Exec(ctx) - return err + return nil }, ) } diff --git a/internal/db/migrations/20260218185128_add_type_to_seasons.go b/internal/db/migrations/20260218185128_add_type_to_seasons.go new file mode 100644 index 0000000..c396c29 --- /dev/null +++ b/internal/db/migrations/20260218185128_add_type_to_seasons.go @@ -0,0 +1,63 @@ +package migrations + +import ( + "context" + + "git.haelnorr.com/h/oslstats/internal/db" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister( + // UP migration + func(ctx context.Context, conn *bun.DB) error { + // Add your migration code here + _, err := conn.NewAddColumn(). + Model((*db.Season)(nil)). + IfNotExists(). + ColumnExpr("type VARCHAR NOT NULL"). + Exec(ctx) + if err != nil { + return err + } + + leagues := []db.League{ + { + Name: "Pro League", + ShortName: "Pro", + Description: "For the most experienced Slapshotters in OSL", + }, + { + Name: "Intermediate League", + ShortName: "IM", + Description: "For returning players who've been practicing in RPUGs and PUBs", + }, + { + Name: "Open League", + ShortName: "Open", + Description: "For new players just getting started with Slapshot", + }, + { + Name: "Draft League", + ShortName: "Draft", + Description: "A league where teams are selected by a draft system", + }, + } + for _, league := range leagues { + _, err = conn.NewInsert(). + Model(&league). + On("CONFLICT DO NOTHING"). + Exec(ctx) + if err != nil { + return err + } + } + return nil + }, + // DOWN migration + func(ctx context.Context, conn *bun.DB) error { + // Add your rollback code here + return nil + }, + ) +} diff --git a/internal/db/season.go b/internal/db/season.go index ec3c572..612e170 100644 --- a/internal/db/season.go +++ b/internal/db/season.go @@ -25,6 +25,17 @@ const ( StatusCompleted SeasonStatus = "completed" ) +type SeasonType string + +func (s SeasonType) String() string { + return string(s) +} + +const ( + SeasonTypeRegular SeasonType = "regular" + SeasonTypeDraft SeasonType = "draft" +) + type Season struct { bun.BaseModel `bun:"table:seasons,alias:s"` @@ -36,13 +47,14 @@ type Season struct { FinalsStartDate bun.NullTime `bun:"finals_start_date" json:"finals_start_date"` FinalsEndDate bun.NullTime `bun:"finals_end_date" json:"finals_end_date"` SlapVersion string `bun:"slap_version,notnull,default:'rebound'" json:"slap_version"` + Type string `bun:"type,notnull" json:"type"` Leagues []League `bun:"m2m:season_leagues,join:Season=League" json:"-"` Teams []Team `bun:"m2m:team_participations,join:Season=Team" json:"-"` } // NewSeason creats a new season -func NewSeason(ctx context.Context, tx bun.Tx, name, version, shortname string, +func NewSeason(ctx context.Context, tx bun.Tx, name, version, shortname, type_ string, start time.Time, audit *AuditMeta, ) (*Season, error) { season := &Season{ @@ -50,12 +62,19 @@ func NewSeason(ctx context.Context, tx bun.Tx, name, version, shortname string, ShortName: strings.ToUpper(shortname), StartDate: start.Truncate(time.Hour * 24), SlapVersion: version, + Type: type_, } err := Insert(tx, season). WithAudit(audit, nil).Exec(ctx) if err != nil { return nil, errors.WithMessage(err, "db.Insert") } + if season.Type == SeasonTypeDraft.String() { + err = NewSeasonLeague(ctx, tx, season.ShortName, "Draft", audit) + if err != nil { + return nil, errors.Wrap(err, "NewSeasonLeague") + } + } return season, nil } diff --git a/internal/embedfs/web/css/output.css b/internal/embedfs/web/css/output.css index 15ea92e..de0cd50 100644 --- a/internal/embedfs/web/css/output.css +++ b/internal/embedfs/web/css/output.css @@ -9,7 +9,6 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --spacing: 0.25rem; - --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 96rem; --container-sm: 24rem; @@ -284,6 +283,12 @@ .z-50 { z-index: 50; } + .col-span-1 { + grid-column: span 1 / span 1; + } + .col-span-2 { + grid-column: span 2 / span 2; + } .container { width: 100%; @media (width >= 40rem) { @@ -308,6 +313,9 @@ .-mt-2 { margin-top: calc(var(--spacing) * -2); } + .-mt-3 { + margin-top: calc(var(--spacing) * -3); + } .mt-0\.5 { margin-top: calc(var(--spacing) * 0.5); } @@ -525,15 +533,15 @@ .max-w-100 { max-width: calc(var(--spacing) * 100); } + .max-w-lg { + max-width: var(--container-lg); + } .max-w-md { max-width: var(--container-md); } .max-w-screen-2xl { max-width: var(--breakpoint-2xl); } - .max-w-screen-lg { - max-width: var(--breakpoint-lg); - } .max-w-screen-xl { max-width: var(--breakpoint-xl); } @@ -597,12 +605,18 @@ .resize-none { resize: none; } + .appearance-none { + appearance: none; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } .grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); } @@ -618,6 +632,9 @@ .items-center { align-items: center; } + .items-end { + align-items: flex-end; + } .items-start { align-items: flex-start; } @@ -819,6 +836,9 @@ border-color: color-mix(in oklab, var(--red) 30%, transparent); } } + .border-surface0 { + border-color: var(--surface0); + } .border-surface1 { border-color: var(--surface1); } @@ -1171,6 +1191,11 @@ .italic { font-style: italic; } + .placeholder-subtext0 { + &::placeholder { + color: var(--subtext0); + } + } .opacity-0 { opacity: 0%; } @@ -1184,6 +1209,10 @@ --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .shadow-sm { --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -1247,6 +1276,14 @@ -webkit-user-select: none; user-select: none; } + .hover\:-translate-y-0\.5 { + &:hover { + @media (hover: hover) { + --tw-translate-y: calc(var(--spacing) * -0.5); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + } .hover\:cursor-pointer { &:hover { @media (hover: hover) { @@ -1492,6 +1529,14 @@ } } } + .hover\:shadow-lg { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } .focus\:border-blue { &:focus { border-color: var(--blue); @@ -1578,6 +1623,12 @@ opacity: 50%; } } + .disabled\:shadow-none { + &:disabled { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } .sm\:end-6 { @media (width >= 40rem) { inset-inline-end: calc(var(--spacing) * 6); diff --git a/internal/handlers/seasons_new.go b/internal/handlers/seasons_new.go index dee53ab..f5a3769 100644 --- a/internal/handlers/seasons_new.go +++ b/internal/handlers/seasons_new.go @@ -16,11 +16,13 @@ import ( "github.com/uptrace/bun" ) +// NewSeason handles GET requests - redirects to the seasons list +// The form is now in a modal on the list page func NewSeason( s *hws.Server, ) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - renderSafely(seasonsview.NewPage(), s, r, w) + respond.HXRedirect(w, "/seasons") }) } @@ -41,6 +43,8 @@ func NewSeasonSubmit( MaxLength(6).MinLength(2).Value version := getter.String("slap_version"). TrimSpace().Required().AllowedValues([]string{"rebound", "slapshot1"}).Value + type_ := getter.String("type"). + TrimSpace().Required().AllowedValues([]string{"regular", "draft"}).Value format := timefmt.NewBuilder(). DayNumeric2().Slash(). MonthNumeric2().Slash(). @@ -52,7 +56,6 @@ func NewSeasonSubmit( nameUnique := false shortNameUnique := false - var season *db.Season if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { var err error nameUnique, err = db.IsUnique(ctx, tx, (*db.Season)(nil), "name", name) @@ -66,7 +69,7 @@ func NewSeasonSubmit( if !nameUnique || !shortNameUnique { return true, nil } - season, err = db.NewSeason(ctx, tx, name, version, shortname, start, db.NewAuditFromRequest(r)) + _, err = db.NewSeason(ctx, tx, name, version, shortname, type_, start, db.NewAuditFromRequest(r)) if err != nil { return false, errors.Wrap(err, "db.NewSeason") } @@ -84,7 +87,26 @@ func NewSeasonSubmit( notify.Warn(s, w, r, "Duplicate Short Name", "This short name is already taken.", nil) return } - respond.HXRedirect(w, "/seasons/%s", season.ShortName) - notify.SuccessWithDelay(s, w, r, "Season Created", fmt.Sprintf("Successfully created season: %s", name), nil) + + // Return the updated seasons list + pageOpts := &db.PageOpts{ + Page: 1, + PerPage: 10, + Order: bun.OrderDesc, + OrderBy: "start_date", + } + var seasons *db.List[db.Season] + if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) { + var err error + seasons, err = db.ListSeasons(ctx, tx, pageOpts) + if err != nil { + return false, errors.Wrap(err, "db.ListSeasons") + } + return true, nil + }); !ok { + return + } + renderSafely(seasonsview.SeasonsList(seasons), s, r, w) + notify.Success(s, w, r, "Season Created", fmt.Sprintf("Successfully created season: %s", name), nil) }) } diff --git a/internal/view/seasonsview/detail_page.templ b/internal/view/seasonsview/detail_page.templ index 3739a23..cdc2791 100644 --- a/internal/view/seasonsview/detail_page.templ +++ b/internal/view/seasonsview/detail_page.templ @@ -25,11 +25,12 @@ templ SeasonDetails(season *db.Season, leaguesWithTeams []db.LeagueWithTeams) {
- Add a new season to the system. All fields are required. -
-Add a new season to the system. All fields are required.
+