added season edits
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/discord"
|
||||
"git.haelnorr.com/h/oslstats/internal/handlers"
|
||||
"git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
"git.haelnorr.com/h/oslstats/internal/rbac"
|
||||
"git.haelnorr.com/h/oslstats/internal/store"
|
||||
)
|
||||
@@ -83,18 +84,28 @@ func addRoutes(
|
||||
{
|
||||
Path: "/seasons/new",
|
||||
Method: hws.MethodGET,
|
||||
Handler: handlers.NewSeason(s, conn),
|
||||
Handler: perms.RequirePermission(s, permissions.SeasonsCreate)(handlers.NewSeason(s, conn)),
|
||||
},
|
||||
{
|
||||
Path: "/seasons/new",
|
||||
Method: hws.MethodPOST,
|
||||
Handler: handlers.NewSeasonSubmit(s, conn),
|
||||
Handler: perms.RequirePermission(s, permissions.SeasonsCreate)(handlers.NewSeasonSubmit(s, conn, audit)),
|
||||
},
|
||||
{
|
||||
Path: "/seasons/{season_short_name}",
|
||||
Method: hws.MethodGET,
|
||||
Handler: handlers.SeasonPage(s, conn),
|
||||
},
|
||||
{
|
||||
Path: "/seasons/{season_short_name}/edit",
|
||||
Method: hws.MethodGET,
|
||||
Handler: perms.RequirePermission(s, permissions.SeasonsUpdate)(handlers.SeasonEditPage(s, conn)),
|
||||
},
|
||||
{
|
||||
Path: "/seasons/{season_short_name}/edit",
|
||||
Method: hws.MethodPOST,
|
||||
Handler: perms.RequirePermission(s, permissions.SeasonsUpdate)(handlers.SeasonEditSubmit(s, conn, audit)),
|
||||
},
|
||||
}
|
||||
|
||||
htmxRoutes := []hws.Route{
|
||||
|
||||
@@ -59,3 +59,32 @@ func GetSeason(ctx context.Context, tx bun.Tx, shortname string) (*Season, error
|
||||
}
|
||||
return GetByField[Season](tx, "short_name", shortname).GetFirst(ctx)
|
||||
}
|
||||
|
||||
func UpdateSeason(ctx context.Context, tx bun.Tx, season *Season) error {
|
||||
if season == nil {
|
||||
return errors.New("season cannot be nil")
|
||||
}
|
||||
if season.ID == 0 {
|
||||
return errors.New("season ID cannot be 0")
|
||||
}
|
||||
// Truncate dates to day precision
|
||||
season.StartDate = season.StartDate.Truncate(time.Hour * 24)
|
||||
if !season.EndDate.IsZero() {
|
||||
season.EndDate.Time = season.EndDate.Time.Truncate(time.Hour * 24)
|
||||
}
|
||||
if !season.FinalsStartDate.IsZero() {
|
||||
season.FinalsStartDate.Time = season.FinalsStartDate.Time.Truncate(time.Hour * 24)
|
||||
}
|
||||
if !season.FinalsEndDate.IsZero() {
|
||||
season.FinalsEndDate.Time = season.FinalsEndDate.Time.Truncate(time.Hour * 24)
|
||||
}
|
||||
_, err := tx.NewUpdate().
|
||||
Model(season).
|
||||
Column("start_date", "end_date", "finals_start_date", "finals_end_date").
|
||||
Where("id = ?", season.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.NewUpdate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -45,6 +45,36 @@
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
--color-rosewater: var(--rosewater);
|
||||
--color-flamingo: var(--flamingo);
|
||||
--color-pink: var(--pink);
|
||||
--color-mauve: var(--mauve);
|
||||
--color-red: var(--red);
|
||||
--color-dark-red: var(--dark-red);
|
||||
--color-maroon: var(--maroon);
|
||||
--color-peach: var(--peach);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-dark-yellow: var(--dark-yellow);
|
||||
--color-green: var(--green);
|
||||
--color-dark-green: var(--dark-green);
|
||||
--color-teal: var(--teal);
|
||||
--color-sky: var(--sky);
|
||||
--color-sapphire: var(--sapphire);
|
||||
--color-blue: var(--blue);
|
||||
--color-dark-blue: var(--dark-blue);
|
||||
--color-lavender: var(--lavender);
|
||||
--color-text: var(--text);
|
||||
--color-subtext1: var(--subtext1);
|
||||
--color-subtext0: var(--subtext0);
|
||||
--color-overlay2: var(--overlay2);
|
||||
--color-overlay1: var(--overlay1);
|
||||
--color-overlay0: var(--overlay0);
|
||||
--color-surface2: var(--surface2);
|
||||
--color-surface1: var(--surface1);
|
||||
--color-surface0: var(--surface0);
|
||||
--color-base: var(--base);
|
||||
--color-mantle: var(--mantle);
|
||||
--color-crust: var(--crust);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
@@ -243,6 +273,9 @@
|
||||
.top-0 {
|
||||
top: calc(var(--spacing) * 0);
|
||||
}
|
||||
.top-1 {
|
||||
top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.top-1\/2 {
|
||||
top: calc(1/2 * 100%);
|
||||
}
|
||||
@@ -276,9 +309,30 @@
|
||||
.z-50 {
|
||||
z-index: 50;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.-mt-2 {
|
||||
margin-top: calc(var(--spacing) * -2);
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
@@ -442,9 +496,25 @@
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.-translate-y-1 {
|
||||
--tw-translate-y: calc(var(--spacing) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.-translate-y-1\/2 {
|
||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -523,6 +593,13 @@
|
||||
margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.space-y-4 {
|
||||
:where(& > :not(:last-child)) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
|
||||
margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
|
||||
}
|
||||
}
|
||||
.gap-x-2 {
|
||||
column-gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -546,6 +623,11 @@
|
||||
border-color: var(--surface2);
|
||||
}
|
||||
}
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -570,6 +652,10 @@
|
||||
.rounded-xl {
|
||||
border-radius: var(--radius-xl);
|
||||
}
|
||||
.rounded-t-lg {
|
||||
border-top-left-radius: var(--radius-lg);
|
||||
border-top-right-radius: var(--radius-lg);
|
||||
}
|
||||
.border {
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
@@ -648,6 +734,15 @@
|
||||
.bg-mauve {
|
||||
background-color: var(--mauve);
|
||||
}
|
||||
.bg-red {
|
||||
background-color: var(--red);
|
||||
}
|
||||
.bg-red\/10 {
|
||||
background-color: var(--red);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--red) 10%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-sapphire {
|
||||
background-color: var(--sapphire);
|
||||
}
|
||||
@@ -838,6 +933,9 @@
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.opacity-50 {
|
||||
opacity: 50%;
|
||||
}
|
||||
@@ -853,6 +951,10 @@
|
||||
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px 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);
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.filter {
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
@@ -1437,6 +1539,11 @@
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-blur {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
@@ -1521,6 +1628,7 @@
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-outline-style: solid;
|
||||
--tw-blur: initial;
|
||||
--tw-brightness: initial;
|
||||
--tw-contrast: initial;
|
||||
|
||||
126
internal/handlers/season_edit.go
Normal file
126
internal/handlers/season_edit.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
"git.haelnorr.com/h/oslstats/internal/auditlog"
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
||||
"git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
||||
"git.haelnorr.com/h/oslstats/internal/validation"
|
||||
"git.haelnorr.com/h/oslstats/internal/view/seasonsview"
|
||||
"git.haelnorr.com/h/timefmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func SeasonEditPage(
|
||||
s *hws.Server,
|
||||
conn *bun.DB,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seasonStr := r.PathValue("season_short_name")
|
||||
var season *db.Season
|
||||
if ok := db.WithReadTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
var err error
|
||||
season, err = db.GetSeason(ctx, tx, seasonStr)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetSeason")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
if season == nil {
|
||||
throw.NotFound(s, w, r, r.URL.Path)
|
||||
return
|
||||
}
|
||||
renderSafely(seasonsview.EditPage(season), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
func SeasonEditSubmit(
|
||||
s *hws.Server,
|
||||
conn *bun.DB,
|
||||
audit *auditlog.Logger,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seasonStr := r.PathValue("season_short_name")
|
||||
|
||||
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
format := timefmt.NewBuilder().
|
||||
DayNumeric2().Slash().
|
||||
MonthNumeric2().Slash().
|
||||
Year4().Build()
|
||||
|
||||
startDate := getter.Time("start_date", format).Required().Value
|
||||
endDate := getter.Time("end_date", format).Value
|
||||
finalsStartDate := getter.Time("finals_start_date", format).Value
|
||||
finalsEndDate := getter.Time("finals_end_date", format).Value
|
||||
|
||||
if !getter.ValidateAndNotify(s, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var season *db.Season
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
var err error
|
||||
season, err = db.GetSeason(ctx, tx, seasonStr)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetSeason")
|
||||
}
|
||||
if season == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Update only the date fields
|
||||
season.StartDate = startDate
|
||||
if !endDate.IsZero() {
|
||||
season.EndDate = bun.NullTime{Time: endDate}
|
||||
} else {
|
||||
season.EndDate = bun.NullTime{}
|
||||
}
|
||||
if !finalsStartDate.IsZero() {
|
||||
season.FinalsStartDate = bun.NullTime{Time: finalsStartDate}
|
||||
} else {
|
||||
season.FinalsStartDate = bun.NullTime{}
|
||||
}
|
||||
if !finalsEndDate.IsZero() {
|
||||
season.FinalsEndDate = bun.NullTime{Time: finalsEndDate}
|
||||
} else {
|
||||
season.FinalsEndDate = bun.NullTime{}
|
||||
}
|
||||
|
||||
err = db.UpdateSeason(ctx, tx, season)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.UpdateSeason")
|
||||
}
|
||||
user := db.CurrentUser(ctx)
|
||||
err = audit.LogSuccess(ctx, tx, user, permissions.SeasonsCreate.String(),
|
||||
"season", season.ID, nil, r)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "audit.LogSuccess")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if season == nil {
|
||||
throw.NotFound(s, w, r, r.URL.Path)
|
||||
return
|
||||
}
|
||||
|
||||
notify.Success(s, w, r, "Season Updated", fmt.Sprintf("Successfully updated season: %s", season.Name), nil)
|
||||
w.Header().Set("HX-Redirect", fmt.Sprintf("/seasons/%s", season.ShortName))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
"git.haelnorr.com/h/oslstats/internal/auditlog"
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
||||
"git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
"git.haelnorr.com/h/oslstats/internal/validation"
|
||||
seasonsview "git.haelnorr.com/h/oslstats/internal/view/seasonsview"
|
||||
"git.haelnorr.com/h/timefmt"
|
||||
@@ -30,6 +32,7 @@ func NewSeason(
|
||||
func NewSeasonSubmit(
|
||||
s *hws.Server,
|
||||
conn *bun.DB,
|
||||
audit *auditlog.Logger,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
||||
@@ -71,6 +74,12 @@ func NewSeasonSubmit(
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.NewSeason")
|
||||
}
|
||||
user := db.CurrentUser(ctx)
|
||||
err = audit.LogSuccess(ctx, tx, user, permissions.SeasonsCreate.String(),
|
||||
"season", season.ID, nil, r)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "audit.LogSuccess")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
@@ -85,7 +94,6 @@ func NewSeasonSubmit(
|
||||
notify.Warn(s, w, r, "Duplicate Short Name", "This short name is already taken.", nil)
|
||||
return
|
||||
}
|
||||
|
||||
notify.Success(s, w, r, "Season Created", fmt.Sprintf("Successfully created season: %s", name), nil)
|
||||
w.Header().Set("HX-Redirect", fmt.Sprintf("/seasons/%s", season.ShortName))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -27,60 +27,94 @@ package datepicker
|
||||
// The component submits the date in DD/MM/YYYY format. To parse on the server:
|
||||
// date, err := time.Parse("02/01/2006", dateString)
|
||||
templ DatePicker(id, name, label, placeholder string, required bool, onChange string) {
|
||||
@DatePickerWithDefault(id, name, label, placeholder, required, onChange, "")
|
||||
}
|
||||
|
||||
// DatePickerWithDefault is the same as DatePicker but accepts a default value in DD/MM/YYYY format
|
||||
templ DatePickerWithDefault(id, name, label, placeholder string, required bool, onChange, defaultValue string) {
|
||||
<div
|
||||
x-data="{
|
||||
showPicker: false,
|
||||
selectedDate: '',
|
||||
displayDate: '',
|
||||
tempYear: new Date().getFullYear(),
|
||||
tempMonth: new Date().getMonth(),
|
||||
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
init() {
|
||||
// Ensure dropdowns are initialized with current month/year
|
||||
this.tempYear = new Date().getFullYear();
|
||||
this.tempMonth = new Date().getMonth();
|
||||
},
|
||||
getDaysInMonth(year, month) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
},
|
||||
getFirstDayOfMonth(year, month) {
|
||||
return new Date(year, month, 1).getDay();
|
||||
},
|
||||
selectDate(day) {
|
||||
const d = String(day).padStart(2, '0');
|
||||
const m = String(this.tempMonth + 1).padStart(2, '0');
|
||||
const y = this.tempYear;
|
||||
this.displayDate = d + '/' + m + '/' + y;
|
||||
this.selectedDate = this.displayDate;
|
||||
$refs.dateInput.value = this.displayDate;
|
||||
this.showPicker = false;
|
||||
// Manually trigger input event so Alpine.js @input handler fires
|
||||
$refs.dateInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
},
|
||||
prevMonth() {
|
||||
if (this.tempMonth === 0) {
|
||||
this.tempMonth = 11;
|
||||
this.tempYear--;
|
||||
} else {
|
||||
this.tempMonth--;
|
||||
}
|
||||
},
|
||||
nextMonth() {
|
||||
if (this.tempMonth === 11) {
|
||||
this.tempMonth = 0;
|
||||
this.tempYear++;
|
||||
} else {
|
||||
this.tempMonth++;
|
||||
}
|
||||
},
|
||||
isToday(day) {
|
||||
const today = new Date();
|
||||
return day === today.getDate() &&
|
||||
this.tempMonth === today.getMonth() &&
|
||||
this.tempYear === today.getFullYear();
|
||||
}
|
||||
}"
|
||||
x-data={ templ.JSFuncCall("datePickerData", defaultValue).CallInline }
|
||||
>
|
||||
<script>
|
||||
function datePickerData(defaultValue) {
|
||||
return {
|
||||
showPicker: false,
|
||||
selectedDate: defaultValue || "",
|
||||
displayDate: defaultValue || "",
|
||||
tempYear: new Date().getFullYear(),
|
||||
tempMonth: new Date().getMonth(),
|
||||
months: [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
],
|
||||
init() {
|
||||
if (this.displayDate) {
|
||||
const parts = this.displayDate.split("/");
|
||||
if (parts.length === 3) {
|
||||
this.tempYear = parseInt(parts[2]);
|
||||
this.tempMonth = parseInt(parts[1]) - 1;
|
||||
}
|
||||
} else {
|
||||
this.tempYear = new Date().getFullYear();
|
||||
this.tempMonth = new Date().getMonth();
|
||||
}
|
||||
},
|
||||
getDaysInMonth(year, month) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
},
|
||||
getFirstDayOfMonth(year, month) {
|
||||
return new Date(year, month, 1).getDay();
|
||||
},
|
||||
selectDate(day) {
|
||||
const d = String(day).padStart(2, "0");
|
||||
const m = String(this.tempMonth + 1).padStart(2, "0");
|
||||
const y = this.tempYear;
|
||||
this.displayDate = d + "/" + m + "/" + y;
|
||||
this.selectedDate = this.displayDate;
|
||||
this.$refs.dateInput.value = this.displayDate;
|
||||
this.showPicker = false;
|
||||
// Manually trigger input event so Alpine.js @input handler fires
|
||||
this.$refs.dateInput.dispatchEvent(
|
||||
new Event("input", { bubbles: true }),
|
||||
);
|
||||
},
|
||||
prevMonth() {
|
||||
if (this.tempMonth === 0) {
|
||||
this.tempMonth = 11;
|
||||
this.tempYear--;
|
||||
} else {
|
||||
this.tempMonth--;
|
||||
}
|
||||
},
|
||||
nextMonth() {
|
||||
if (this.tempMonth === 11) {
|
||||
this.tempMonth = 0;
|
||||
this.tempYear++;
|
||||
} else {
|
||||
this.tempMonth++;
|
||||
}
|
||||
},
|
||||
isToday(day) {
|
||||
const today = new Date();
|
||||
return (
|
||||
day === today.getDate() &&
|
||||
this.tempMonth === today.getMonth() &&
|
||||
this.tempYear === today.getFullYear()
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<label for={ id } class="block text-sm font-medium mb-2">{ label }</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
|
||||
@@ -146,7 +146,7 @@ func formatDateLong(t time.Time) string {
|
||||
func formatDuration(start, end time.Time) string {
|
||||
days := int(end.Sub(start).Hours() / 24)
|
||||
if days == 0 {
|
||||
return "Same day"
|
||||
return "1 day"
|
||||
} else if days == 1 {
|
||||
return "1 day"
|
||||
} else if days < 7 {
|
||||
|
||||
177
internal/view/seasonsview/edit_form.templ
Normal file
177
internal/view/seasonsview/edit_form.templ
Normal file
@@ -0,0 +1,177 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/datepicker"
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "time"
|
||||
|
||||
templ EditForm(season *db.Season) {
|
||||
{{
|
||||
// Format dates for display (DD/MM/YYYY)
|
||||
startDateStr := formatDateInput(season.StartDate)
|
||||
endDateStr := ""
|
||||
if !season.EndDate.IsZero() {
|
||||
endDateStr = formatDateInput(season.EndDate.Time)
|
||||
}
|
||||
finalsStartDateStr := ""
|
||||
if !season.FinalsStartDate.IsZero() {
|
||||
finalsStartDateStr = formatDateInput(season.FinalsStartDate.Time)
|
||||
}
|
||||
finalsEndDateStr := ""
|
||||
if !season.FinalsEndDate.IsZero() {
|
||||
finalsEndDateStr = formatDateInput(season.FinalsEndDate.Time)
|
||||
}
|
||||
}}
|
||||
<form
|
||||
hx-post={ "/seasons/" + season.ShortName + "/edit" }
|
||||
hx-swap="none"
|
||||
x-data={ templ.JSFuncCall("editSeasonFormData", startDateStr, endDateStr, finalsStartDateStr, finalsEndDateStr).CallInline }
|
||||
@submit="handleSubmit()"
|
||||
@htmx:after-request="if(submitTimeout) clearTimeout(submitTimeout); const redirect = $event.detail.xhr.getResponseHeader('HX-Redirect'); if(redirect) return; if(!$event.detail.successful) { isSubmitting=false; buttonText='Save Changes'; generalError='An error occurred. Please try again.'; }"
|
||||
>
|
||||
<script>
|
||||
function editSeasonFormData(
|
||||
initialStart,
|
||||
initialEnd,
|
||||
initialFinalsStart,
|
||||
initialFinalsEnd,
|
||||
) {
|
||||
return {
|
||||
canSubmit: true,
|
||||
buttonText: "Save Changes",
|
||||
// Date validation state
|
||||
startDateError: "",
|
||||
startDateIsEmpty: initialStart === "",
|
||||
endDateError: "",
|
||||
finalsStartDateError: "",
|
||||
finalsEndDateError: "",
|
||||
// Form state
|
||||
isSubmitting: false,
|
||||
generalError: "",
|
||||
submitTimeout: null,
|
||||
// Reset date errors
|
||||
resetStartDateErr() {
|
||||
this.startDateError = "";
|
||||
},
|
||||
resetEndDateErr() {
|
||||
this.endDateError = "";
|
||||
},
|
||||
resetFinalsStartDateErr() {
|
||||
this.finalsStartDateError = "";
|
||||
},
|
||||
resetFinalsEndDateErr() {
|
||||
this.finalsEndDateError = "";
|
||||
},
|
||||
// Check if form can be submitted
|
||||
updateCanSubmit() {
|
||||
this.canSubmit = !this.startDateIsEmpty;
|
||||
},
|
||||
// Handle form submission
|
||||
handleSubmit() {
|
||||
this.isSubmitting = true;
|
||||
this.buttonText = "Saving...";
|
||||
this.generalError = "";
|
||||
// Set timeout for 10 seconds
|
||||
this.submitTimeout = setTimeout(() => {
|
||||
this.isSubmitting = false;
|
||||
this.buttonText = "Save Changes";
|
||||
this.generalError = "Request timed out. Please try again.";
|
||||
}, 10000);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<div class="bg-mantle border border-surface1 rounded-lg">
|
||||
<!-- Header Section -->
|
||||
<div class="bg-surface0 border-b border-surface1 px-6 py-8 rounded-t-lg">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-text mb-2">Edit { season.Name }</h1>
|
||||
<span class="inline-block bg-blue px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
{ season.ShortName }
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
x-bind:disabled="!canSubmit || isSubmitting"
|
||||
x-text="buttonText"
|
||||
type="submit"
|
||||
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
|
||||
bg-blue hover:bg-blue/75 text-mantle transition font-semibold
|
||||
disabled:bg-blue/40 disabled:cursor-not-allowed"
|
||||
></button>
|
||||
<a
|
||||
href={ templ.SafeURL("/seasons/" + season.ShortName) }
|
||||
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center
|
||||
bg-surface1 hover:bg-surface2 text-text transition"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Information Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
||||
<!-- Regular Season Section -->
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold text-text mb-4 flex items-center gap-2">
|
||||
<span class="text-blue">●</span>
|
||||
Regular Season
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@datepicker.DatePickerWithDefault("start_date", "start_date", "Start Date", "DD/MM/YYYY", true, "startDateIsEmpty = $el.value === ''; resetStartDateErr(); if(startDateIsEmpty) { startDateError='Start date is required'; } updateCanSubmit();", startDateStr)
|
||||
<p
|
||||
class="text-xs text-red -mt-2"
|
||||
x-show="startDateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="startDateError"
|
||||
></p>
|
||||
@datepicker.DatePickerWithDefault("end_date", "end_date", "End Date (Optional)", "DD/MM/YYYY", false, "resetEndDateErr();", endDateStr)
|
||||
<p
|
||||
class="text-xs text-red -mt-2"
|
||||
x-show="endDateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="endDateError"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Finals Section -->
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold text-text mb-4 flex items-center gap-2">
|
||||
<span class="text-yellow">★</span>
|
||||
Finals
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
@datepicker.DatePickerWithDefault("finals_start_date", "finals_start_date", "Start Date (Optional)", "DD/MM/YYYY", false, "resetFinalsStartDateErr();", finalsStartDateStr)
|
||||
<p
|
||||
class="text-xs text-red -mt-2"
|
||||
x-show="finalsStartDateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="finalsStartDateError"
|
||||
></p>
|
||||
@datepicker.DatePickerWithDefault("finals_end_date", "finals_end_date", "End Date (Optional)", "DD/MM/YYYY", false, "resetFinalsEndDateErr();", finalsEndDateStr)
|
||||
<p
|
||||
class="text-xs text-red -mt-2"
|
||||
x-show="finalsEndDateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="finalsEndDateError"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- General Error Message -->
|
||||
<div
|
||||
class="px-6 pb-6"
|
||||
x-show="generalError"
|
||||
x-cloak
|
||||
>
|
||||
<div class="bg-red/10 border border-red rounded-lg p-4">
|
||||
<p class="text-sm text-red text-center" x-text="generalError"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
func formatDateInput(t time.Time) string {
|
||||
return t.Format("02/01/2006")
|
||||
}
|
||||
12
internal/view/seasonsview/edit_page.templ
Normal file
12
internal/view/seasonsview/edit_page.templ
Normal file
@@ -0,0 +1,12 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
|
||||
templ EditPage(season *db.Season) {
|
||||
@baseview.Layout("Edit " + season.Name) {
|
||||
<div class="max-w-screen-2xl mx-auto px-4 py-8">
|
||||
@EditForm(season)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user