added season types and changed new season to be a modal
This commit is contained in:
@@ -25,11 +25,12 @@ templ SeasonDetails(season *db.Season, leaguesWithTeams []db.LeagueWithTeams) {
|
||||
<div class="bg-surface0 border-b border-surface1 px-6 py-8">
|
||||
<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">{ season.Name }</h1>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h1 class="text-4xl font-bold text-text">{ season.Name }</h1>
|
||||
<span class="text-lg font-mono text-subtext0 bg-surface1 px-2 py-0.5 rounded">{ season.ShortName }</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="inline-block bg-blue px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
{ season.ShortName }
|
||||
</span>
|
||||
@SeasonTypeBadge(season.Type)
|
||||
@SlapVersionBadge(season.SlapVersion)
|
||||
@StatusBadge(season, false, false)
|
||||
</div>
|
||||
@@ -221,3 +222,16 @@ templ SlapVersionBadge(version string) {
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
templ SeasonTypeBadge(type_ string) {
|
||||
switch type_ {
|
||||
case "regular":
|
||||
<span class="inline-block bg-sapphire px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
Regular
|
||||
</span>
|
||||
case "draft":
|
||||
<span class="inline-block bg-mauve px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
Draft
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,14 @@ import "git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
|
||||
templ ListPage(seasons *db.List[db.Season]) {
|
||||
@baseview.Layout("Seasons") {
|
||||
<!-- Flatpickr CSS -->
|
||||
<link rel="stylesheet" href="/static/vendored/flatpickr@4.6.13.min.css"/>
|
||||
<link rel="stylesheet" href="/static/css/flatpickr-catppuccin.css"/>
|
||||
<!-- Flatpickr JS -->
|
||||
<script src="/static/vendored/flatpickr@4.6.13.min.js"></script>
|
||||
<div class="max-w-screen-2xl mx-auto px-2">
|
||||
@SeasonsList(seasons)
|
||||
@NewSeasonModal()
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -63,13 +69,14 @@ templ SeasonsList(seasons *db.List[db.Season]) {
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6 px-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<span class="text-3xl font-bold">Seasons</span>
|
||||
if canAddSeason {
|
||||
<a
|
||||
href="/seasons/new"
|
||||
class="rounded-lg px-2 py-1 hover:cursor-pointer text-center text-sm
|
||||
if canAddSeason {
|
||||
<button
|
||||
type="button"
|
||||
@click="$dispatch('open-new-season-modal')"
|
||||
class="rounded-lg px-2 py-1 hover:cursor-pointer text-center text-sm
|
||||
bg-green hover:bg-green/75 text-mantle transition"
|
||||
>Add season</a>
|
||||
}
|
||||
>Add season</button>
|
||||
}
|
||||
</div>
|
||||
@sort.Dropdown(seasons.PageOpts, sortOpts)
|
||||
</div>
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/datepicker"
|
||||
|
||||
templ NewForm() {
|
||||
<form
|
||||
hx-post="/seasons/new"
|
||||
hx-swap="none"
|
||||
x-data={ templ.JSFuncCall("newSeasonFormData").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 && $event.detail.xhr.status !== 409) { isSubmitting=false; buttonText='Create Season'; generalError='An error occurred. Please try again.'; }"
|
||||
>
|
||||
<script>
|
||||
function newSeasonFormData() {
|
||||
return {
|
||||
canSubmit: false,
|
||||
buttonText: "Create Season",
|
||||
// Name validation state
|
||||
nameError: "",
|
||||
nameIsChecking: false,
|
||||
nameIsUnique: false,
|
||||
nameIsEmpty: true,
|
||||
// Short name validation state
|
||||
shortNameError: "",
|
||||
shortNameIsChecking: false,
|
||||
shortNameIsUnique: false,
|
||||
shortNameIsEmpty: true,
|
||||
// Date validation state
|
||||
dateError: "",
|
||||
dateIsEmpty: true,
|
||||
// Form state
|
||||
isSubmitting: false,
|
||||
generalError: "",
|
||||
submitTimeout: null,
|
||||
// Reset name errors
|
||||
resetNameErr() {
|
||||
this.nameError = "";
|
||||
this.nameIsChecking = false;
|
||||
this.nameIsUnique = false;
|
||||
},
|
||||
// Reset short name errors
|
||||
resetShortNameErr() {
|
||||
this.shortNameError = "";
|
||||
this.shortNameIsChecking = false;
|
||||
this.shortNameIsUnique = false;
|
||||
},
|
||||
// Reset date errors
|
||||
resetDateErr() {
|
||||
this.dateError = "";
|
||||
},
|
||||
// Check if form can be submitted
|
||||
updateCanSubmit() {
|
||||
this.canSubmit =
|
||||
!this.nameIsEmpty &&
|
||||
this.nameIsUnique &&
|
||||
!this.nameIsChecking &&
|
||||
!this.shortNameIsEmpty &&
|
||||
this.shortNameIsUnique &&
|
||||
!this.shortNameIsChecking &&
|
||||
!this.dateIsEmpty;
|
||||
},
|
||||
// Handle form submission
|
||||
handleSubmit() {
|
||||
this.isSubmitting = true;
|
||||
this.buttonText = "Creating...";
|
||||
this.generalError = "";
|
||||
// Set timeout for 10 seconds
|
||||
this.submitTimeout = setTimeout(() => {
|
||||
this.isSubmitting = false;
|
||||
this.buttonText = "Create Season";
|
||||
this.generalError = "Request timed out. Please try again.";
|
||||
}, 10000);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<div class="grid gap-y-5">
|
||||
<!-- Name Field -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium mb-2">Season Name</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
maxlength="20"
|
||||
x-bind:class="{
|
||||
'py-3 px-4 block w-full rounded-lg text-sm bg-base disabled:opacity-50 disabled:pointer-events-none border-2 outline-none': true,
|
||||
'border-overlay0 focus:border-blue': !nameIsUnique && !nameError,
|
||||
'border-green focus:border-green': nameIsUnique && !nameIsChecking && !nameError,
|
||||
'border-red focus:border-red': nameError && !nameIsChecking && !isSubmitting
|
||||
}"
|
||||
required
|
||||
placeholder="e.g. Season 1"
|
||||
@input="resetNameErr(); nameIsEmpty = $el.value.trim() === ''; if(nameIsEmpty) { nameError='Season name is required'; nameIsUnique=false; } updateCanSubmit();"
|
||||
hx-post="/htmx/isseasonnameunique"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-swap="none"
|
||||
@htmx:before-request="if($el.value.trim() === '') { nameIsEmpty=true; return; } nameIsEmpty=false; nameIsChecking=true; nameIsUnique=false; nameError=''; updateCanSubmit();"
|
||||
@htmx:after-request="nameIsChecking=false; if($event.detail.successful) { nameIsUnique=true; } else if($event.detail.xhr.status === 409) { nameError='This season name is already taken'; nameIsUnique=false; } updateCanSubmit();"
|
||||
/>
|
||||
<p class="text-xs text-subtext1 mt-1">Maximum 20 characters</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-center text-xs text-red mt-2"
|
||||
x-show="nameError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="nameError"
|
||||
></p>
|
||||
</div>
|
||||
<!-- Short Name Field -->
|
||||
<div>
|
||||
<label for="short_name" class="block text-sm font-medium mb-2">Short Name</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="short_name"
|
||||
name="short_name"
|
||||
maxlength="6"
|
||||
x-bind:class="{
|
||||
'py-3 px-4 block w-full rounded-lg text-sm bg-base disabled:opacity-50 disabled:pointer-events-none border-2 outline-none uppercase': true,
|
||||
'border-overlay0 focus:border-blue': !shortNameIsUnique && !shortNameError,
|
||||
'border-green focus:border-green': shortNameIsUnique && !shortNameIsChecking && !shortNameError,
|
||||
'border-red focus:border-red': shortNameError && !shortNameIsChecking && !isSubmitting
|
||||
}"
|
||||
required
|
||||
placeholder="e.g. S1"
|
||||
pattern="[A-Z0-9]+"
|
||||
@input="
|
||||
let val = $el.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
$el.value = val;
|
||||
resetShortNameErr();
|
||||
shortNameIsEmpty = val.trim() === '';
|
||||
if(shortNameIsEmpty) {
|
||||
shortNameError='Short name is required';
|
||||
shortNameIsUnique=false;
|
||||
}
|
||||
updateCanSubmit();
|
||||
"
|
||||
hx-post="/htmx/isseasonshortnameunique"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-swap="none"
|
||||
@htmx:before-request="if($el.value.trim() === '') { shortNameIsEmpty=true; return; } shortNameIsEmpty=false; shortNameIsChecking=true; shortNameIsUnique=false; shortNameError=''; updateCanSubmit();"
|
||||
@htmx:after-request="shortNameIsChecking=false; if($event.detail.successful) { shortNameIsUnique=true; } else if($event.detail.xhr.status === 409) { shortNameError='This short name is already taken'; shortNameIsUnique=false; } updateCanSubmit();"
|
||||
/>
|
||||
<p class="text-xs text-subtext1 mt-1">Maximum 6 characters, alphanumeric only (auto-capitalized)</p>
|
||||
</div>
|
||||
<p
|
||||
class="text-center text-xs text-red mt-2"
|
||||
x-show="shortNameError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="shortNameError"
|
||||
></p>
|
||||
</div>
|
||||
<!-- Slap Version Field -->
|
||||
<div>
|
||||
<label for="slap_version" class="block text-sm font-medium mb-2">Slap Version</label>
|
||||
<select
|
||||
id="slap_version"
|
||||
name="slap_version"
|
||||
class="py-3 px-4 block w-full rounded-lg text-sm bg-base border-2 border-overlay0 focus:border-blue outline-none"
|
||||
required
|
||||
>
|
||||
<option value="rebound" selected>Rebound</option>
|
||||
<option value="slapshot1">Slapshot 1</option>
|
||||
</select>
|
||||
<p class="text-xs text-subtext1 mt-1">Select the game version for this season</p>
|
||||
</div>
|
||||
<!-- Start Date Field -->
|
||||
@datepicker.DatePicker("start_date", "start_date", "Start Date", "DD/MM/YYYY", true, "dateIsEmpty = $el.value === ''; resetDateErr(); if(dateIsEmpty) { dateError='Start date is required'; } updateCanSubmit();")
|
||||
<p
|
||||
class="text-center text-xs text-red mt-2"
|
||||
x-show="dateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="dateError"
|
||||
></p>
|
||||
<!-- General Error Message -->
|
||||
<p
|
||||
class="text-center text-sm text-red"
|
||||
x-show="generalError"
|
||||
x-cloak
|
||||
x-text="generalError"
|
||||
></p>
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
x-bind:disabled="!canSubmit || isSubmitting"
|
||||
x-text="buttonText"
|
||||
type="submit"
|
||||
class="w-full py-3 px-4 inline-flex justify-center items-center
|
||||
gap-x-2 rounded-lg border border-transparent transition font-semibold
|
||||
bg-blue hover:bg-blue/75 text-mantle hover:cursor-pointer
|
||||
disabled:bg-blue/40 disabled:cursor-not-allowed"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
275
internal/view/seasonsview/new_form_modal.templ
Normal file
275
internal/view/seasonsview/new_form_modal.templ
Normal file
@@ -0,0 +1,275 @@
|
||||
package seasonsview
|
||||
|
||||
// NewFormModal is a version of the form for use in a modal
|
||||
// It closes the modal and refreshes the list on success instead of redirecting
|
||||
templ NewFormModal() {
|
||||
<form
|
||||
hx-post="/seasons/new"
|
||||
hx-target="#seasons-list-container"
|
||||
hx-swap="outerHTML"
|
||||
x-data={ templ.JSFuncCall("newSeasonFormModalData").CallInline }
|
||||
@submit="handleSubmit()"
|
||||
@htmx:after-request="
|
||||
if(submitTimeout) clearTimeout(submitTimeout);
|
||||
// Only handle the form submission (not uniqueness checks)
|
||||
const path = $event.detail.requestConfig.path;
|
||||
if(path !== '/seasons/new') return;
|
||||
if($event.detail.successful) {
|
||||
// Close the modal
|
||||
$dispatch('close-new-season-modal');
|
||||
// Reset form
|
||||
$el.reset();
|
||||
isSubmitting=false;
|
||||
buttonText='Create Season';
|
||||
// Reset validation state
|
||||
nameIsEmpty=true;
|
||||
nameIsUnique=false;
|
||||
shortNameIsEmpty=true;
|
||||
shortNameIsUnique=false;
|
||||
dateIsEmpty=true;
|
||||
updateCanSubmit();
|
||||
// Clear flatpickr
|
||||
const fpInput = document.getElementById('start_date');
|
||||
if(fpInput && fpInput._flatpickr) {
|
||||
fpInput._flatpickr.clear();
|
||||
}
|
||||
} else if($event.detail.xhr.status !== 409) {
|
||||
isSubmitting=false;
|
||||
buttonText='Create Season';
|
||||
generalError='An error occurred. Please try again.';
|
||||
}
|
||||
"
|
||||
x-init="initFlatpickrModal()"
|
||||
>
|
||||
<script>
|
||||
function newSeasonFormModalData() {
|
||||
return {
|
||||
canSubmit: false,
|
||||
buttonText: "Create Season",
|
||||
// Name validation state
|
||||
nameError: "",
|
||||
nameIsChecking: false,
|
||||
nameIsUnique: false,
|
||||
nameIsEmpty: true,
|
||||
// Short name validation state
|
||||
shortNameError: "",
|
||||
shortNameIsChecking: false,
|
||||
shortNameIsUnique: false,
|
||||
shortNameIsEmpty: true,
|
||||
// Date validation state
|
||||
dateError: "",
|
||||
dateIsEmpty: true,
|
||||
// Form state
|
||||
isSubmitting: false,
|
||||
generalError: "",
|
||||
submitTimeout: null,
|
||||
// Reset name errors
|
||||
resetNameErr() {
|
||||
this.nameError = "";
|
||||
this.nameIsChecking = false;
|
||||
this.nameIsUnique = false;
|
||||
},
|
||||
// Reset short name errors
|
||||
resetShortNameErr() {
|
||||
this.shortNameError = "";
|
||||
this.shortNameIsChecking = false;
|
||||
this.shortNameIsUnique = false;
|
||||
},
|
||||
// Reset date errors
|
||||
resetDateErr() {
|
||||
this.dateError = "";
|
||||
},
|
||||
// Check if form can be submitted
|
||||
updateCanSubmit() {
|
||||
this.canSubmit =
|
||||
!this.nameIsEmpty &&
|
||||
this.nameIsUnique &&
|
||||
!this.nameIsChecking &&
|
||||
!this.shortNameIsEmpty &&
|
||||
this.shortNameIsUnique &&
|
||||
!this.shortNameIsChecking &&
|
||||
!this.dateIsEmpty;
|
||||
},
|
||||
// Handle form submission
|
||||
handleSubmit() {
|
||||
this.isSubmitting = true;
|
||||
this.buttonText = "Creating...";
|
||||
this.generalError = "";
|
||||
// Set timeout for 10 seconds
|
||||
this.submitTimeout = setTimeout(() => {
|
||||
this.isSubmitting = false;
|
||||
this.buttonText = "Create Season";
|
||||
this.generalError = "Request timed out. Please try again.";
|
||||
}, 10000);
|
||||
},
|
||||
};
|
||||
}
|
||||
function initFlatpickrModal() {
|
||||
// Small delay to ensure DOM is ready after modal opens
|
||||
setTimeout(() => {
|
||||
if (typeof flatpickr !== 'undefined') {
|
||||
flatpickr('#start_date', {
|
||||
dateFormat: 'd/m/Y',
|
||||
allowInput: true,
|
||||
onChange: function(selectedDates, dateStr) {
|
||||
const input = document.getElementById('start_date');
|
||||
input.value = dateStr;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
<div class="grid gap-y-5">
|
||||
<!-- Name and Short Name Fields (Inlined) -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<!-- Name Field -->
|
||||
<div class="col-span-2">
|
||||
<label for="name" class="block text-sm font-medium mb-2 text-subtext0">Season Name</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
maxlength="20"
|
||||
x-bind:class="{
|
||||
'py-3 px-4 block w-full rounded-lg text-sm bg-surface0 disabled:opacity-50 disabled:pointer-events-none border-2 outline-none text-text placeholder-subtext0': true,
|
||||
'border-surface1 focus:border-blue': !nameIsUnique && !nameError,
|
||||
'border-green focus:border-green': nameIsUnique && !nameIsChecking && !nameError,
|
||||
'border-red focus:border-red': nameError && !nameIsChecking && !isSubmitting
|
||||
}"
|
||||
required
|
||||
placeholder="e.g. Season 1"
|
||||
@input="resetNameErr(); nameIsEmpty = $el.value.trim() === ''; if(nameIsEmpty) { nameError='Season name is required'; nameIsUnique=false; } updateCanSubmit();"
|
||||
hx-post="/htmx/isseasonnameunique"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-swap="none"
|
||||
@htmx:before-request="if($el.value.trim() === '') { nameIsEmpty=true; return; } nameIsEmpty=false; nameIsChecking=true; nameIsUnique=false; nameError=''; updateCanSubmit();"
|
||||
@htmx:after-request="nameIsChecking=false; if($event.detail.successful) { nameIsUnique=true; } else if($event.detail.xhr.status === 409) { nameError='This season name is already taken'; nameIsUnique=false; } updateCanSubmit();"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="text-center text-xs text-red mt-2"
|
||||
x-show="nameError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="nameError"
|
||||
></p>
|
||||
</div>
|
||||
<!-- Short Name Field -->
|
||||
<div class="col-span-1">
|
||||
<label for="short_name" class="block text-sm font-medium mb-2 text-subtext0">Short Name</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="short_name"
|
||||
name="short_name"
|
||||
maxlength="6"
|
||||
x-bind:class="{
|
||||
'py-3 px-4 block w-full rounded-lg text-sm bg-surface0 disabled:opacity-50 disabled:pointer-events-none border-2 outline-none uppercase text-text placeholder-subtext0': true,
|
||||
'border-surface1 focus:border-blue': !shortNameIsUnique && !shortNameError,
|
||||
'border-green focus:border-green': shortNameIsUnique && !shortNameIsChecking && !shortNameError,
|
||||
'border-red focus:border-red': shortNameError && !shortNameIsChecking && !isSubmitting
|
||||
}"
|
||||
required
|
||||
placeholder="e.g. S1"
|
||||
pattern="[A-Z0-9]+"
|
||||
@input="
|
||||
let val = $el.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
$el.value = val;
|
||||
resetShortNameErr();
|
||||
shortNameIsEmpty = val.trim() === '';
|
||||
if(shortNameIsEmpty) {
|
||||
shortNameError='Short name is required';
|
||||
shortNameIsUnique=false;
|
||||
}
|
||||
updateCanSubmit();
|
||||
"
|
||||
hx-post="/htmx/isseasonshortnameunique"
|
||||
hx-trigger="input changed delay:500ms"
|
||||
hx-swap="none"
|
||||
@htmx:before-request="if($el.value.trim() === '') { shortNameIsEmpty=true; return; } shortNameIsEmpty=false; shortNameIsChecking=true; shortNameIsUnique=false; shortNameError=''; updateCanSubmit();"
|
||||
@htmx:after-request="shortNameIsChecking=false; if($event.detail.successful) { shortNameIsUnique=true; } else if($event.detail.xhr.status === 409) { shortNameError='This short name is already taken'; shortNameIsUnique=false; } updateCanSubmit();"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="text-center text-xs text-red mt-2"
|
||||
x-show="shortNameError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="shortNameError"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-subtext0 -mt-3">Maximum 20 characters for name, 6 alphanumeric characters for short name</p>
|
||||
<!-- Season Type and Start Date Fields (Inlined) -->
|
||||
<div class="grid grid-cols-2 gap-4 items-end">
|
||||
<!-- Season Type Field -->
|
||||
<div>
|
||||
<label for="type" class="block text-sm font-medium mb-2 text-subtext0">Season Type</label>
|
||||
<select
|
||||
id="type"
|
||||
name="type"
|
||||
class="py-3 px-4 block w-full rounded-lg text-sm bg-surface0 border-2 border-surface1 focus:border-blue outline-none text-text appearance-none cursor-pointer hover:bg-surface1 transition-colors"
|
||||
required
|
||||
style="height: 46px;"
|
||||
>
|
||||
<option value="regular" selected>Regular</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Start Date Field (Flatpickr) -->
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium mb-2 text-subtext0">Start Date</label>
|
||||
<input
|
||||
type="text"
|
||||
id="start_date"
|
||||
name="start_date"
|
||||
class="py-3 px-4 block w-full rounded-lg text-sm bg-surface0 border-2 border-surface1 focus:border-blue outline-none text-text placeholder-subtext0"
|
||||
placeholder="DD/MM/YYYY"
|
||||
required
|
||||
style="height: 46px;"
|
||||
@input="dateIsEmpty = $el.value.trim() === ''; resetDateErr(); if(dateIsEmpty) { dateError='Start date is required'; } updateCanSubmit();"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="text-center text-xs text-red -mt-3"
|
||||
x-show="dateError && !isSubmitting"
|
||||
x-cloak
|
||||
x-text="dateError"
|
||||
></p>
|
||||
<!-- Slap Version Field -->
|
||||
<div>
|
||||
<label for="slap_version" class="block text-sm font-medium mb-2 text-subtext0">Slap Version</label>
|
||||
<select
|
||||
id="slap_version"
|
||||
name="slap_version"
|
||||
class="py-3 px-4 block w-full rounded-lg text-sm bg-surface0 border-2 border-surface1 focus:border-blue outline-none text-text appearance-none cursor-pointer hover:bg-surface1 transition-colors"
|
||||
required
|
||||
style="height: 46px;"
|
||||
>
|
||||
<option value="rebound" selected>Rebound</option>
|
||||
<option value="slapshot1">Slapshot 1</option>
|
||||
</select>
|
||||
<p class="text-xs text-subtext0 mt-1">Select the game game version for this season</p>
|
||||
</div>
|
||||
<!-- General Error Message -->
|
||||
<p
|
||||
class="text-center text-sm text-red"
|
||||
x-show="generalError"
|
||||
x-cloak
|
||||
x-text="generalError"
|
||||
></p>
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
x-bind:disabled="!canSubmit || isSubmitting"
|
||||
x-text="buttonText"
|
||||
type="submit"
|
||||
class="w-full py-3 px-4 inline-flex justify-center items-center
|
||||
gap-x-2 rounded-lg border border-transparent transition-all duration-200 font-semibold
|
||||
bg-blue hover:bg-blue/80 text-mantle hover:cursor-pointer shadow-md
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none hover:shadow-lg hover:-translate-y-0.5"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
|
||||
templ NewPage() {
|
||||
@baseview.Layout("New Season") {
|
||||
<div class="max-w-screen-lg mx-auto px-4 py-8">
|
||||
<div class="bg-mantle border border-surface1 rounded-xl">
|
||||
<div class="p-6 sm:p-8">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-text">Create New Season</h1>
|
||||
<p class="mt-2 text-sm text-subtext0">
|
||||
Add a new season to the system. All fields are required.
|
||||
</p>
|
||||
</div>
|
||||
<div class="max-w-md mx-auto">
|
||||
@NewForm()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
65
internal/view/seasonsview/new_season_modal.templ
Normal file
65
internal/view/seasonsview/new_season_modal.templ
Normal file
@@ -0,0 +1,65 @@
|
||||
package seasonsview
|
||||
|
||||
// NewSeasonModal renders a modal containing the new season form
|
||||
// This is used on the seasons list page for a better UX
|
||||
templ NewSeasonModal() {
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
@open-new-season-modal.window="open = true"
|
||||
@close-new-season-modal.window="open = false"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- Background overlay -->
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-base/75 transition-opacity"
|
||||
@click="open = false"
|
||||
></div>
|
||||
<!-- Modal panel -->
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div
|
||||
x-show="open"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="relative transform overflow-hidden rounded-xl bg-mantle border border-surface0 shadow-xl transition-all w-full max-w-lg"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-surface0">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold text-text" id="modal-title">Create New Season</h3>
|
||||
<button
|
||||
type="button"
|
||||
@click="open = false"
|
||||
class="text-subtext0 hover:text-text transition-colors"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-subtext0 mt-1">Add a new season to the system. All fields are required.</p>
|
||||
</div>
|
||||
<!-- Form content -->
|
||||
<div class="p-6">
|
||||
@NewFormModal()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -11,19 +11,17 @@ templ SeasonLeagueLayout(activeSection string, season *db.Season, league *db.Lea
|
||||
<!-- Header Section -->
|
||||
<div class="bg-surface0 border-b border-surface1 px-6 py-8">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-text mb-2">{ season.Name } - { league.Name }</h1>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="inline-block bg-surface1 px-3 py-1 rounded text-sm text-subtext0 font-mono">
|
||||
{ season.ShortName }
|
||||
</span>
|
||||
<span class="inline-block bg-blue px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
{ league.ShortName }
|
||||
</span>
|
||||
@SlapVersionBadge(season.SlapVersion)
|
||||
@StatusBadge(season, false, false)
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h1 class="text-4xl font-bold text-text">{ season.Name } - { league.Name }</h1>
|
||||
<span class="text-lg font-mono text-subtext0 bg-surface1 px-2 py-0.5 rounded">{ season.ShortName }</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
@SeasonTypeBadge(season.Type)
|
||||
@SlapVersionBadge(season.SlapVersion)
|
||||
@StatusBadge(season, false, false)
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
href={ templ.SafeURL("/seasons/" + season.ShortName) }
|
||||
|
||||
Reference in New Issue
Block a user