added leagues
This commit is contained in:
@@ -26,9 +26,13 @@ templ SeasonDetails(season *db.Season) {
|
||||
<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>
|
||||
<span class="inline-block bg-blue px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
{ season.ShortName }
|
||||
</span>
|
||||
<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>
|
||||
@SlapVersionBadge(season.SlapVersion)
|
||||
@StatusBadge(season, false, false)
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
if canEditSeason {
|
||||
@@ -127,13 +131,25 @@ templ SeasonDetails(season *db.Season) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status Section -->
|
||||
<!-- Leagues Section -->
|
||||
<div class="px-6 pb-6">
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold text-text mb-4">Status</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
@StatusBadge(season, false, false)
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-text mb-4">Leagues</h2>
|
||||
if len(season.Leagues) == 0 {
|
||||
<p class="text-subtext0 text-sm">No leagues assigned to this season.</p>
|
||||
} else {
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
for _, league := range season.Leagues {
|
||||
<a
|
||||
href={ templ.SafeURL("/seasons/" + season.ShortName + "/leagues/" + league.ShortName) }
|
||||
class="bg-mantle border border-surface1 rounded-lg p-4 hover:bg-surface1 transition-colors"
|
||||
>
|
||||
<h3 class="font-semibold text-text mb-1">{ league.Name }</h3>
|
||||
<span class="text-xs text-subtext0 font-mono">{ league.ShortName }</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,3 +185,15 @@ func formatDuration(start, end time.Time) string {
|
||||
return strconv.Itoa(years) + " years"
|
||||
}
|
||||
}
|
||||
|
||||
templ SlapVersionBadge(version string) {
|
||||
if version == "rebound" {
|
||||
<span class="inline-block bg-green px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
Rebound
|
||||
</span>
|
||||
} else if version == "slapshot1" {
|
||||
<span class="inline-block bg-red px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
Slapshot 1
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "git.haelnorr.com/h/oslstats/internal/view/datepicker"
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "time"
|
||||
|
||||
templ EditForm(season *db.Season) {
|
||||
templ EditForm(season *db.Season, allLeagues []*db.League) {
|
||||
{{
|
||||
// Format dates for display (DD/MM/YYYY)
|
||||
startDateStr := formatDateInput(season.StartDate)
|
||||
@@ -86,9 +86,20 @@ templ EditForm(season *db.Season) {
|
||||
<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 class="flex items-center gap-2">
|
||||
<span class="inline-block bg-blue px-3 py-1 rounded-full text-sm font-semibold text-mantle">
|
||||
{ season.ShortName }
|
||||
</span>
|
||||
<select
|
||||
id="slap_version"
|
||||
name="slap_version"
|
||||
class="py-1 pl-3 pr-2 rounded-full text-sm bg-base border-2 border-overlay0 focus:border-blue outline-none"
|
||||
required
|
||||
>
|
||||
<option value="rebound" selected?={ season.SlapVersion == "rebound" }>Rebound</option>
|
||||
<option value="slapshot1" selected?={ season.SlapVersion == "slapshot1" }>Slapshot 1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@@ -158,6 +169,8 @@ templ EditForm(season *db.Season) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Leagues Section -->
|
||||
@LeaguesSection(season, allLeagues)
|
||||
<!-- General Error Message -->
|
||||
<div
|
||||
class="px-6 pb-6"
|
||||
|
||||
@@ -3,10 +3,10 @@ package seasonsview
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/baseview"
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
|
||||
templ EditPage(season *db.Season) {
|
||||
templ EditPage(season *db.Season, allLeagues []*db.League) {
|
||||
@baseview.Layout("Edit " + season.Name) {
|
||||
<div class="max-w-screen-2xl mx-auto px-4 py-8">
|
||||
@EditForm(season)
|
||||
@EditForm(season, allLeagues)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
88
internal/view/seasonsview/leagues_section.templ
Normal file
88
internal/view/seasonsview/leagues_section.templ
Normal file
@@ -0,0 +1,88 @@
|
||||
package seasonsview
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/db"
|
||||
import "git.haelnorr.com/h/oslstats/internal/contexts"
|
||||
import "git.haelnorr.com/h/oslstats/internal/permissions"
|
||||
|
||||
templ LeaguesSection(season *db.Season, allLeagues []*db.League) {
|
||||
{{
|
||||
permCache := contexts.Permissions(ctx)
|
||||
canAddLeague := permCache.HasPermission(permissions.SeasonsAddLeague)
|
||||
canRemoveLeague := permCache.HasPermission(permissions.SeasonsRemoveLeague)
|
||||
|
||||
// Create a map of assigned league IDs for quick lookup
|
||||
assignedLeagueIDs := make(map[int]bool)
|
||||
for _, league := range season.Leagues {
|
||||
assignedLeagueIDs[league.ID] = true
|
||||
}
|
||||
}}
|
||||
if canAddLeague || canRemoveLeague {
|
||||
<div
|
||||
id="leagues-section"
|
||||
class="px-6 pb-6"
|
||||
>
|
||||
<div class="bg-surface0 border border-surface1 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold text-text mb-4">Leagues</h2>
|
||||
<!-- Currently Assigned Leagues -->
|
||||
if len(season.Leagues) > 0 {
|
||||
<div class="mb-4">
|
||||
<h3 class="text-sm font-medium text-subtext0 mb-2">Currently Assigned</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
for _, league := range season.Leagues {
|
||||
<div class="flex items-center gap-2 bg-mantle border border-surface1 rounded-lg px-3 py-2">
|
||||
<span class="text-sm text-text">{ league.Name }</span>
|
||||
<span class="text-xs text-subtext0 font-mono">({ league.ShortName })</span>
|
||||
if canRemoveLeague {
|
||||
<button
|
||||
type="button"
|
||||
@click={ "window.dispatchEvent(new CustomEvent('confirm-action', { detail: { title: 'Remove League', message: 'Are you sure you want to remove " + league.Name + " from this season?', action: () => htmx.ajax('DELETE', '/seasons/" + season.ShortName + "/leagues/" + league.ShortName + "', { target: '#leagues-section', swap: 'outerHTML' }) } }))" }
|
||||
class="text-red hover:text-red/75 hover:cursor-pointer ml-1"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Available Leagues to Add -->
|
||||
if canAddLeague && len(allLeagues) > 0 {
|
||||
{{
|
||||
// Filter out already assigned leagues
|
||||
availableLeagues := []*db.League{}
|
||||
for _, league := range allLeagues {
|
||||
if !assignedLeagueIDs[league.ID] {
|
||||
availableLeagues = append(availableLeagues, league)
|
||||
}
|
||||
}
|
||||
}}
|
||||
if len(availableLeagues) > 0 {
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-subtext0 mb-2">Add League</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
for _, league := range availableLeagues {
|
||||
<button
|
||||
type="button"
|
||||
hx-post={ "/seasons/" + season.ShortName + "/leagues/" + league.ShortName }
|
||||
hx-target="#leagues-section"
|
||||
hx-swap="outerHTML"
|
||||
class="flex items-center gap-2 bg-surface1 hover:bg-surface2 border border-overlay0 rounded-lg px-3 py-2 transition hover:cursor-pointer"
|
||||
>
|
||||
<span class="text-sm text-text">{ league.Name }</span>
|
||||
<span class="text-xs text-subtext0 font-mono">({ league.ShortName })</span>
|
||||
<svg class="w-4 h-4 text-green" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ templ SeasonsList(seasons *db.List[db.Season]) {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
for _, s := range seasons.Items {
|
||||
<a
|
||||
class="bg-mantle border border-surface1 rounded-lg p-6 hover:bg-surface0 transition-colors"
|
||||
class="bg-mantle border border-surface1 rounded-lg p-6 hover:bg-surface0 transition-colors flex flex-col"
|
||||
href={ fmt.Sprintf("/seasons/%s", s.ShortName) }
|
||||
>
|
||||
<!-- Header: Name + Status Badge -->
|
||||
@@ -91,14 +91,45 @@ templ SeasonsList(seasons *db.List[db.Season]) {
|
||||
<h3 class="text-xl font-bold text-text">{ s.Name }</h3>
|
||||
@StatusBadge(s, true, true)
|
||||
</div>
|
||||
<!-- Info Row: Short Name + Start Date -->
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<!-- Info Row: Short Name + Slap Version -->
|
||||
<div class="flex items-center gap-2 text-sm mb-3 flex-wrap">
|
||||
<span class="px-2 py-1 bg-surface1 rounded text-subtext0 font-mono">
|
||||
{ s.ShortName }
|
||||
</span>
|
||||
<span class="text-subtext0">
|
||||
@SlapVersionBadge(s.SlapVersion)
|
||||
</div>
|
||||
<!-- Leagues -->
|
||||
if len(s.Leagues) > 0 {
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
for _, league := range s.Leagues {
|
||||
<span class="px-2 py-0.5 bg-surface0 border border-surface1 rounded text-xs text-subtext0">
|
||||
{ league.ShortName }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Date Info -->
|
||||
{{
|
||||
now := time.Now()
|
||||
}}
|
||||
<div class="text-xs text-subtext1 mt-auto">
|
||||
if now.Before(s.StartDate) {
|
||||
Starts: { formatDate(s.StartDate) }
|
||||
} else if !s.FinalsStartDate.IsZero() {
|
||||
// Finals are scheduled
|
||||
if !s.FinalsEndDate.IsZero() && now.After(s.FinalsEndDate.Time) {
|
||||
Completed: { formatDate(s.FinalsEndDate.Time) }
|
||||
} else if now.After(s.FinalsStartDate.Time) {
|
||||
Finals Started: { formatDate(s.FinalsStartDate.Time) }
|
||||
} else {
|
||||
Finals Start: { formatDate(s.FinalsStartDate.Time) }
|
||||
}
|
||||
} else if !s.EndDate.IsZero() && now.After(s.EndDate.Time) {
|
||||
// No finals scheduled and regular season ended
|
||||
Completed: { formatDate(s.EndDate.Time) }
|
||||
} else {
|
||||
Started: { formatDate(s.StartDate) }
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -50,22 +50,27 @@ templ NewForm() {
|
||||
},
|
||||
// Check if form can be submitted
|
||||
updateCanSubmit() {
|
||||
this.canSubmit = !this.nameIsEmpty && this.nameIsUnique && !this.nameIsChecking &&
|
||||
!this.shortNameIsEmpty && this.shortNameIsUnique && !this.shortNameIsChecking &&
|
||||
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 = '';
|
||||
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.';
|
||||
this.buttonText = "Create Season";
|
||||
this.generalError = "Request timed out. Please try again.";
|
||||
}, 10000);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
@@ -147,6 +152,20 @@ templ NewForm() {
|
||||
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
|
||||
@@ -168,7 +187,7 @@ templ NewForm() {
|
||||
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 text-base font-semibold
|
||||
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>
|
||||
|
||||
@@ -12,60 +12,54 @@ templ StatusBadge(season *db.Season, compact bool, useShortLabels bool) {
|
||||
{{
|
||||
now := time.Now()
|
||||
status := ""
|
||||
statusColor := ""
|
||||
statusBg := ""
|
||||
|
||||
// Determine status based on dates
|
||||
if now.Before(season.StartDate) {
|
||||
status = "Upcoming"
|
||||
statusColor = "text-blue"
|
||||
statusBg = "bg-blue/10 border-blue"
|
||||
} else if !season.EndDate.IsZero() && now.After(season.EndDate.Time) {
|
||||
status = "Completed"
|
||||
statusColor = "text-green"
|
||||
statusBg = "bg-green/10 border-green"
|
||||
} else if !season.FinalsStartDate.IsZero() && now.After(season.FinalsStartDate.Time) {
|
||||
statusBg = "bg-blue"
|
||||
} else if !season.FinalsStartDate.IsZero() {
|
||||
// Finals are scheduled
|
||||
if !season.FinalsEndDate.IsZero() && now.After(season.FinalsEndDate.Time) {
|
||||
// Finals have ended
|
||||
status = "Completed"
|
||||
statusColor = "text-green"
|
||||
statusBg = "bg-green/10 border-green"
|
||||
} else {
|
||||
statusBg = "bg-teal"
|
||||
} else if now.After(season.FinalsStartDate.Time) {
|
||||
// Finals are in progress
|
||||
if useShortLabels {
|
||||
status = "Finals"
|
||||
} else {
|
||||
status = "Finals in Progress"
|
||||
}
|
||||
statusColor = "text-yellow"
|
||||
statusBg = "bg-yellow/10 border-yellow"
|
||||
statusBg = "bg-yellow"
|
||||
} else if !season.EndDate.IsZero() && now.After(season.EndDate.Time) {
|
||||
// Regular season ended, finals upcoming
|
||||
status = "Finals Soon"
|
||||
statusBg = "bg-peach"
|
||||
} else {
|
||||
// Regular season active, finals scheduled for later
|
||||
if useShortLabels {
|
||||
status = "Active"
|
||||
} else {
|
||||
status = "In Progress"
|
||||
}
|
||||
statusBg = "bg-green"
|
||||
}
|
||||
} else if !season.EndDate.IsZero() && now.After(season.EndDate.Time) {
|
||||
// No finals scheduled and regular season ended
|
||||
status = "Completed"
|
||||
statusBg = "bg-teal"
|
||||
} else {
|
||||
// Regular season active, no finals scheduled
|
||||
if useShortLabels {
|
||||
status = "Active"
|
||||
} else {
|
||||
status = "In Progress"
|
||||
}
|
||||
statusColor = "text-green"
|
||||
statusBg = "bg-green/10 border-green"
|
||||
}
|
||||
|
||||
// Determine size classes
|
||||
var sizeClasses string
|
||||
var textSize string
|
||||
var iconSize string
|
||||
if compact {
|
||||
sizeClasses = "px-2 py-1"
|
||||
textSize = "text-xs"
|
||||
iconSize = "text-sm"
|
||||
} else {
|
||||
sizeClasses = "px-4 py-2"
|
||||
textSize = "text-lg"
|
||||
iconSize = "text-2xl"
|
||||
statusBg = "bg-green"
|
||||
}
|
||||
}}
|
||||
<div class={ "rounded-lg border-2 inline-flex items-center gap-2 " + sizeClasses + " " + statusBg }>
|
||||
if !compact {
|
||||
<span class={ iconSize + " " + statusColor }>●</span>
|
||||
}
|
||||
<span class={ textSize + " font-semibold " + statusColor }>{ status }</span>
|
||||
</div>
|
||||
<span class={ "inline-block px-3 py-1 rounded-full text-sm font-semibold text-mantle " + statusBg }>
|
||||
{ status }
|
||||
</span>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user