Compare commits
40 Commits
master
...
6d5cd3c4c3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d5cd3c4c3 | |||
| 656fa6c255 | |||
| fd002a7ad0 | |||
| 2835ef74fc | |||
| 9e729d20b3 | |||
| ad93c44fae | |||
| af42c16faf | |||
| ba0844048a | |||
| 1cab39a4f7 | |||
| 26ee81964d | |||
| e0fd3b0a45 | |||
| 34cba6a81f | |||
| 14e10d095e | |||
| dd1ed61adb | |||
| 9ad29586f2 | |||
| 78db8d0324 | |||
| 04389970ac | |||
| 1194d46613 | |||
| 3b1eeaf12d | |||
| 4064c9c557 | |||
| 9f6a2303a0 | |||
| 8b414ff7f0 | |||
| 7b934295c6 | |||
| c73758c91c | |||
| f5c9e70edf | |||
| b957df8d32 | |||
| b96aeef32e | |||
| ce659f7d56 | |||
| a472314474 | |||
| 76987adceb | |||
| 060301f2c2 | |||
| 788346d269 | |||
| b57fbcd302 | |||
| 1634b27991 | |||
| cade057e42 | |||
| e526f42ac3 | |||
| 088478e6c1 | |||
| 08344877c7 | |||
| 36b42d6267 | |||
| 2b5c43cf61 |
@@ -1,71 +0,0 @@
|
|||||||
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 is_forfeit column
|
|
||||||
_, err := conn.NewAddColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("is_forfeit BOOLEAN NOT NULL DEFAULT false").
|
|
||||||
IfNotExists().
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add forfeit_team_id column
|
|
||||||
_, err = conn.NewAddColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("forfeit_team_id INTEGER REFERENCES teams(id)").
|
|
||||||
IfNotExists().
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add forfeit_reason column
|
|
||||||
_, err = conn.NewAddColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("forfeit_reason VARCHAR").
|
|
||||||
IfNotExists().
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
// DOWN migration
|
|
||||||
func(ctx context.Context, conn *bun.DB) error {
|
|
||||||
_, err := conn.NewDropColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("forfeit_reason").
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.NewDropColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("forfeit_team_id").
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = conn.NewDropColumn().
|
|
||||||
Model((*db.PlayoffSeries)(nil)).
|
|
||||||
ColumnExpr("is_forfeit").
|
|
||||||
Exec(ctx)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -81,18 +81,12 @@ type PlayoffSeries struct {
|
|||||||
LoserNextSlot *string `bun:"loser_next_slot"` // "team1" or "team2" in loser's next series
|
LoserNextSlot *string `bun:"loser_next_slot"` // "team1" or "team2" in loser's next series
|
||||||
CreatedAt int64 `bun:",notnull"`
|
CreatedAt int64 `bun:",notnull"`
|
||||||
|
|
||||||
// Forfeit-related fields
|
Bracket *PlayoffBracket `bun:"rel:belongs-to,join:bracket_id=id"`
|
||||||
IsForfeit bool `bun:"is_forfeit,default:false"`
|
Team1 *Team `bun:"rel:belongs-to,join:team1_id=id"`
|
||||||
ForfeitTeamID *int `bun:"forfeit_team_id"` // Which team forfeited
|
Team2 *Team `bun:"rel:belongs-to,join:team2_id=id"`
|
||||||
ForfeitReason *string `bun:"forfeit_reason"` // Admin-provided reason
|
Winner *Team `bun:"rel:belongs-to,join:winner_team_id=id"`
|
||||||
|
Loser *Team `bun:"rel:belongs-to,join:loser_team_id=id"`
|
||||||
Bracket *PlayoffBracket `bun:"rel:belongs-to,join:bracket_id=id"`
|
Matches []*PlayoffMatch `bun:"rel:has-many,join:id=series_id"`
|
||||||
Team1 *Team `bun:"rel:belongs-to,join:team1_id=id"`
|
|
||||||
Team2 *Team `bun:"rel:belongs-to,join:team2_id=id"`
|
|
||||||
Winner *Team `bun:"rel:belongs-to,join:winner_team_id=id"`
|
|
||||||
Loser *Team `bun:"rel:belongs-to,join:loser_team_id=id"`
|
|
||||||
ForfeitTeam *Team `bun:"rel:belongs-to,join:forfeit_team_id=id"`
|
|
||||||
Matches []*PlayoffMatch `bun:"rel:has-many,join:id=series_id"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlayoffMatch represents a single game within a series
|
// PlayoffMatch represents a single game within a series
|
||||||
@@ -350,7 +344,6 @@ func GetPlayoffSeriesByID(
|
|||||||
Relation("Team2").
|
Relation("Team2").
|
||||||
Relation("Winner").
|
Relation("Winner").
|
||||||
Relation("Loser").
|
Relation("Loser").
|
||||||
Relation("ForfeitTeam").
|
|
||||||
Relation("Matches", func(q *bun.SelectQuery) *bun.SelectQuery {
|
Relation("Matches", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
return q.Order("pm.match_number ASC")
|
return q.Order("pm.match_number ASC")
|
||||||
}).
|
}).
|
||||||
|
|||||||
@@ -329,149 +329,6 @@ func DeleteSeriesResults(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForfeitSeries forfeits a playoff series. The forfeiting team loses and the opponent
|
|
||||||
// is declared the winner and advances through the bracket. Any existing match results
|
|
||||||
// and fixtures are discarded. The series score (Team1Wins/Team2Wins) is left as-is.
|
|
||||||
func ForfeitSeries(
|
|
||||||
ctx context.Context,
|
|
||||||
tx bun.Tx,
|
|
||||||
seriesID int,
|
|
||||||
forfeitTeamID int,
|
|
||||||
reason string,
|
|
||||||
audit *AuditMeta,
|
|
||||||
) error {
|
|
||||||
series, err := GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "GetPlayoffSeriesByID")
|
|
||||||
}
|
|
||||||
if series == nil {
|
|
||||||
return BadRequest("series not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if series.Status == SeriesStatusCompleted {
|
|
||||||
return BadRequest("series is already completed")
|
|
||||||
}
|
|
||||||
if series.Status == SeriesStatusBye {
|
|
||||||
return BadRequest("cannot forfeit a bye series")
|
|
||||||
}
|
|
||||||
if series.Team1ID == nil || series.Team2ID == nil {
|
|
||||||
return BadRequest("both teams must be assigned to forfeit a series")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate forfeit team is one of the teams in the series
|
|
||||||
if forfeitTeamID != *series.Team1ID && forfeitTeamID != *series.Team2ID {
|
|
||||||
return BadRequest("forfeit team must be one of the teams in the series")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine winner and loser
|
|
||||||
var winnerTeamID int
|
|
||||||
if forfeitTeamID == *series.Team1ID {
|
|
||||||
winnerTeamID = *series.Team2ID
|
|
||||||
} else {
|
|
||||||
winnerTeamID = *series.Team1ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard all existing match results and fixtures
|
|
||||||
for _, match := range series.Matches {
|
|
||||||
if match.FixtureID == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := GetFixtureResult(ctx, tx, *match.FixtureID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "GetFixtureResult")
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != nil {
|
|
||||||
// Delete result (CASCADE deletes player stats)
|
|
||||||
err = DeleteByID[FixtureResult](tx, result.ID).
|
|
||||||
WithAudit(audit, &AuditInfo{
|
|
||||||
Action: "fixture_results.discard_for_forfeit",
|
|
||||||
ResourceType: "fixture_result",
|
|
||||||
ResourceID: result.ID,
|
|
||||||
Details: map[string]any{
|
|
||||||
"fixture_id": *match.FixtureID,
|
|
||||||
"series_id": seriesID,
|
|
||||||
},
|
|
||||||
}).Delete(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "DeleteByID fixture_result")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the fixture
|
|
||||||
err = DeleteByID[Fixture](tx, *match.FixtureID).
|
|
||||||
WithAudit(audit, &AuditInfo{
|
|
||||||
Action: "playoff_fixture.delete_for_forfeit",
|
|
||||||
ResourceType: "fixture",
|
|
||||||
ResourceID: *match.FixtureID,
|
|
||||||
Details: map[string]any{
|
|
||||||
"series_id": seriesID,
|
|
||||||
},
|
|
||||||
}).Delete(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "DeleteByID fixture")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear fixture ID from match
|
|
||||||
match.FixtureID = nil
|
|
||||||
match.Status = "pending"
|
|
||||||
err = UpdateByID(tx, match.ID, match).
|
|
||||||
Column("fixture_id", "status").
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "UpdateByID playoff_match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update series with forfeit info
|
|
||||||
var reasonPtr *string
|
|
||||||
if reason != "" {
|
|
||||||
reasonPtr = &reason
|
|
||||||
}
|
|
||||||
|
|
||||||
series.Status = SeriesStatusCompleted
|
|
||||||
series.IsForfeit = true
|
|
||||||
series.ForfeitTeamID = &forfeitTeamID
|
|
||||||
series.ForfeitReason = reasonPtr
|
|
||||||
series.WinnerTeamID = &winnerTeamID
|
|
||||||
series.LoserTeamID = &forfeitTeamID
|
|
||||||
|
|
||||||
err = UpdateByID(tx, series.ID, series).
|
|
||||||
Column("status", "is_forfeit", "forfeit_team_id", "forfeit_reason", "winner_team_id", "loser_team_id").
|
|
||||||
WithAudit(audit, &AuditInfo{
|
|
||||||
Action: "playoff_series.forfeit",
|
|
||||||
ResourceType: "playoff_series",
|
|
||||||
ResourceID: series.ID,
|
|
||||||
Details: map[string]any{
|
|
||||||
"forfeit_team_id": forfeitTeamID,
|
|
||||||
"winner_team_id": winnerTeamID,
|
|
||||||
"reason": reason,
|
|
||||||
},
|
|
||||||
}).Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "UpdateByID series forfeit")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance winner to next series
|
|
||||||
if series.WinnerNextID != nil && series.WinnerNextSlot != nil {
|
|
||||||
err = advanceTeamToSeries(ctx, tx, *series.WinnerNextID, *series.WinnerNextSlot, winnerTeamID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "advanceTeamToSeries winner")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance loser to next series (e.g. lower bracket)
|
|
||||||
if series.LoserNextID != nil && series.LoserNextSlot != nil {
|
|
||||||
err = advanceTeamToSeries(ctx, tx, *series.LoserNextID, *series.LoserNextSlot, forfeitTeamID)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "advanceTeamToSeries loser")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPendingSeriesResults checks if a series has any pending (non-finalized) results.
|
// HasPendingSeriesResults checks if a series has any pending (non-finalized) results.
|
||||||
func HasPendingSeriesResults(ctx context.Context, tx bun.Tx, seriesID int) (bool, error) {
|
func HasPendingSeriesResults(ctx context.Context, tx bun.Tx, seriesID int) (bool, error) {
|
||||||
series, err := GetPlayoffSeriesByID(ctx, tx, seriesID)
|
series, err := GetPlayoffSeriesByID(ctx, tx, seriesID)
|
||||||
|
|||||||
@@ -267,6 +267,9 @@
|
|||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.top-1 {
|
||||||
|
top: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1 / 2 * 100%);
|
top: calc(1 / 2 * 100%);
|
||||||
}
|
}
|
||||||
@@ -339,6 +342,9 @@
|
|||||||
.-mt-3 {
|
.-mt-3 {
|
||||||
margin-top: calc(var(--spacing) * -3);
|
margin-top: calc(var(--spacing) * -3);
|
||||||
}
|
}
|
||||||
|
.mt-0 {
|
||||||
|
margin-top: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.mt-0\.5 {
|
.mt-0\.5 {
|
||||||
margin-top: calc(var(--spacing) * 0.5);
|
margin-top: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@@ -372,6 +378,9 @@
|
|||||||
.mt-12 {
|
.mt-12 {
|
||||||
margin-top: calc(var(--spacing) * 12);
|
margin-top: calc(var(--spacing) * 12);
|
||||||
}
|
}
|
||||||
|
.mt-16 {
|
||||||
|
margin-top: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
.mt-24 {
|
.mt-24 {
|
||||||
margin-top: calc(var(--spacing) * 24);
|
margin-top: calc(var(--spacing) * 24);
|
||||||
}
|
}
|
||||||
@@ -649,12 +658,22 @@
|
|||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.flex-shrink {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.shrink-0 {
|
.shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.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 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -1274,6 +1293,9 @@
|
|||||||
.px-6 {
|
.px-6 {
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
|
.py-0 {
|
||||||
|
padding-block: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.py-0\.5 {
|
.py-0\.5 {
|
||||||
padding-block: calc(var(--spacing) * 0.5);
|
padding-block: calc(var(--spacing) * 0.5);
|
||||||
}
|
}
|
||||||
@@ -1566,6 +1588,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));
|
--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);
|
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;
|
||||||
|
}
|
||||||
.blur {
|
.blur {
|
||||||
--tw-blur: blur(8px);
|
--tw-blur: blur(8px);
|
||||||
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,);
|
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,);
|
||||||
@@ -2715,6 +2741,11 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
initial-value: 0 0 #0000;
|
||||||
}
|
}
|
||||||
|
@property --tw-outline-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
@property --tw-blur {
|
@property --tw-blur {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -2817,6 +2848,7 @@
|
|||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-outline-style: solid;
|
||||||
--tw-blur: initial;
|
--tw-blur: initial;
|
||||||
--tw-brightness: initial;
|
--tw-brightness: initial;
|
||||||
--tw-contrast: initial;
|
--tw-contrast: initial;
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
|
||||||
"git.haelnorr.com/h/oslstats/internal/db"
|
|
||||||
"git.haelnorr.com/h/oslstats/internal/notify"
|
|
||||||
"git.haelnorr.com/h/oslstats/internal/respond"
|
|
||||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
|
||||||
"git.haelnorr.com/h/oslstats/internal/validation"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/uptrace/bun"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ForfeitSeries handles POST /series/{series_id}/forfeit
|
|
||||||
// Forfeits a playoff series. The forfeiting team loses and the opponent wins
|
|
||||||
// and advances through the bracket. Requires playoffs.manage permission.
|
|
||||||
func ForfeitSeries(
|
|
||||||
s *hws.Server,
|
|
||||||
conn *db.DB,
|
|
||||||
) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
seriesID, err := strconv.Atoi(r.PathValue("series_id"))
|
|
||||||
if err != nil {
|
|
||||||
throw.BadRequest(s, w, r, "Invalid series ID", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
forfeitTeamStr := getter.String("forfeit_team").TrimSpace().Required().Value
|
|
||||||
forfeitReason := getter.String("forfeit_reason").TrimSpace().Value
|
|
||||||
if !getter.ValidateAndNotify(s, w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate forfeit_team is "team1" or "team2"
|
|
||||||
if forfeitTeamStr != "team1" && forfeitTeamStr != "team2" {
|
|
||||||
notify.Warn(s, w, r, "Invalid Team", "Please select which team is forfeiting.", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok := conn.WithNotifyTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
|
||||||
series, err := db.GetPlayoffSeriesByID(ctx, tx, seriesID)
|
|
||||||
if err != nil {
|
|
||||||
if db.IsBadRequest(err) {
|
|
||||||
respond.NotFound(w, errors.Wrap(err, "db.GetPlayoffSeriesByID"))
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, errors.Wrap(err, "db.GetPlayoffSeriesByID")
|
|
||||||
}
|
|
||||||
if series == nil {
|
|
||||||
respond.NotFound(w, errors.New("series not found"))
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the forfeit team ID
|
|
||||||
var forfeitTeamID int
|
|
||||||
if forfeitTeamStr == "team1" {
|
|
||||||
if series.Team1ID == nil {
|
|
||||||
notify.Warn(s, w, r, "No Team", "Team 1 is not assigned.", nil)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
forfeitTeamID = *series.Team1ID
|
|
||||||
} else {
|
|
||||||
if series.Team2ID == nil {
|
|
||||||
notify.Warn(s, w, r, "No Team", "Team 2 is not assigned.", nil)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
forfeitTeamID = *series.Team2ID
|
|
||||||
}
|
|
||||||
|
|
||||||
err = db.ForfeitSeries(ctx, tx, seriesID, forfeitTeamID, forfeitReason, db.NewAuditFromRequest(r))
|
|
||||||
if err != nil {
|
|
||||||
if db.IsBadRequest(err) {
|
|
||||||
notify.Warn(s, w, r, "Cannot Forfeit", err.Error(), nil)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return false, errors.Wrap(err, "db.ForfeitSeries")
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}); !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notify.SuccessWithDelay(s, w, r, "Series Forfeited", "The series has been forfeited and the opponent advances.", nil)
|
|
||||||
respond.HXRedirect(w, "/series/%d", seriesID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -417,12 +417,6 @@ func addRoutes(
|
|||||||
Method: hws.MethodPOST,
|
Method: hws.MethodPOST,
|
||||||
Handler: perms.RequirePermission(s, permissions.PlayoffsManage)(handlers.SeriesDiscardResults(s, conn)),
|
Handler: perms.RequirePermission(s, permissions.PlayoffsManage)(handlers.SeriesDiscardResults(s, conn)),
|
||||||
},
|
},
|
||||||
// Series forfeit route
|
|
||||||
{
|
|
||||||
Path: "/series/{series_id}/forfeit",
|
|
||||||
Method: hws.MethodPOST,
|
|
||||||
Handler: perms.RequirePermission(s, permissions.PlayoffsManage)(handlers.ForfeitSeries(s, conn)),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRoutes := []hws.Route{
|
playerRoutes := []hws.Route{
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries)
|
|||||||
{{
|
{{
|
||||||
hasTeams := series.Team1 != nil || series.Team2 != nil
|
hasTeams := series.Team1 != nil || series.Team2 != nil
|
||||||
seriesURL := fmt.Sprintf("/series/%d", series.ID)
|
seriesURL := fmt.Sprintf("/series/%d", series.ID)
|
||||||
isForfeit := series.IsForfeit
|
|
||||||
}}
|
}}
|
||||||
<div
|
<div
|
||||||
data-series={ fmt.Sprint(series.SeriesNumber) }
|
data-series={ fmt.Sprint(series.SeriesNumber) }
|
||||||
@@ -210,8 +209,7 @@ templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries)
|
|||||||
}
|
}
|
||||||
class={ "bg-surface0 border rounded-lg overflow-hidden",
|
class={ "bg-surface0 border rounded-lg overflow-hidden",
|
||||||
templ.KV("border-blue/50", series.Status == db.SeriesStatusInProgress),
|
templ.KV("border-blue/50", series.Status == db.SeriesStatusInProgress),
|
||||||
templ.KV("border-red/50", isForfeit),
|
templ.KV("border-surface1", series.Status != db.SeriesStatusInProgress),
|
||||||
templ.KV("border-surface1", series.Status != db.SeriesStatusInProgress && !isForfeit),
|
|
||||||
templ.KV("hover:bg-surface1 hover:cursor-pointer transition", hasTeams) }
|
templ.KV("hover:bg-surface1 hover:cursor-pointer transition", hasTeams) }
|
||||||
>
|
>
|
||||||
<!-- Series Header -->
|
<!-- Series Header -->
|
||||||
@@ -220,24 +218,17 @@ templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries)
|
|||||||
<span class="text-xs font-semibold text-subtext0">{ series.Label }</span>
|
<span class="text-xs font-semibold text-subtext0">{ series.Label }</span>
|
||||||
@seriesFormatBadge(series.MatchesToWin)
|
@seriesFormatBadge(series.MatchesToWin)
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
@seriesStatusBadge(series.Status)
|
||||||
if isForfeit {
|
|
||||||
<span class="px-1.5 py-0.5 bg-red/20 text-red rounded text-xs font-bold">
|
|
||||||
FF
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@seriesStatusBadge(series.Status)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Teams -->
|
<!-- Teams -->
|
||||||
<div class="divide-y divide-surface1">
|
<div class="divide-y divide-surface1">
|
||||||
@seriesTeamRow(season, league, series.Team1, series.Team1Seed, series.Team1Wins,
|
@seriesTeamRow(season, league, series.Team1, series.Team1Seed, series.Team1Wins,
|
||||||
series.WinnerTeamID, series.ForfeitTeamID, series.MatchesToWin)
|
series.WinnerTeamID, series.MatchesToWin)
|
||||||
@seriesTeamRow(season, league, series.Team2, series.Team2Seed, series.Team2Wins,
|
@seriesTeamRow(season, league, series.Team2, series.Team2Seed, series.Team2Wins,
|
||||||
series.WinnerTeamID, series.ForfeitTeamID, series.MatchesToWin)
|
series.WinnerTeamID, series.MatchesToWin)
|
||||||
</div>
|
</div>
|
||||||
<!-- Series Score -->
|
<!-- Series Score -->
|
||||||
if series.MatchesToWin > 1 && !isForfeit {
|
if series.MatchesToWin > 1 {
|
||||||
<div class="bg-mantle px-3 py-1 text-center text-xs text-subtext0 border-t border-surface1">
|
<div class="bg-mantle px-3 py-1 text-center text-xs text-subtext0 border-t border-surface1">
|
||||||
{ fmt.Sprint(series.Team1Wins) } - { fmt.Sprint(series.Team2Wins) }
|
{ fmt.Sprint(series.Team1Wins) } - { fmt.Sprint(series.Team2Wins) }
|
||||||
</div>
|
</div>
|
||||||
@@ -245,21 +236,16 @@ templ seriesCard(season *db.Season, league *db.League, series *db.PlayoffSeries)
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ seriesTeamRow(season *db.Season, league *db.League, team *db.Team, seed *int, wins int, winnerID *int, forfeitTeamID *int, matchesToWin int) {
|
templ seriesTeamRow(season *db.Season, league *db.League, team *db.Team, seed *int, wins int, winnerID *int, matchesToWin int) {
|
||||||
{{
|
{{
|
||||||
isWinner := false
|
isWinner := false
|
||||||
if team != nil && winnerID != nil {
|
if team != nil && winnerID != nil {
|
||||||
isWinner = team.ID == *winnerID
|
isWinner = team.ID == *winnerID
|
||||||
}
|
}
|
||||||
isForfeiter := false
|
|
||||||
if team != nil && forfeitTeamID != nil {
|
|
||||||
isForfeiter = team.ID == *forfeitTeamID
|
|
||||||
}
|
|
||||||
isTBD := team == nil
|
isTBD := team == nil
|
||||||
}}
|
}}
|
||||||
<div class={ "flex items-center justify-between px-3 py-2",
|
<div class={ "flex items-center justify-between px-3 py-2",
|
||||||
templ.KV("bg-green/5", isWinner && !isForfeiter),
|
templ.KV("bg-green/5", isWinner) }>
|
||||||
templ.KV("bg-red/5", isForfeiter) }>
|
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
if seed != nil {
|
if seed != nil {
|
||||||
<span class="text-xs font-mono text-subtext0 w-4 text-right flex-shrink-0">
|
<span class="text-xs font-mono text-subtext0 w-4 text-right flex-shrink-0">
|
||||||
@@ -274,14 +260,12 @@ templ seriesTeamRow(season *db.Season, league *db.League, team *db.Team, seed *i
|
|||||||
<div class="truncate">
|
<div class="truncate">
|
||||||
@links.TeamLinkInSeason(team, season, league)
|
@links.TeamLinkInSeason(team, season, league)
|
||||||
</div>
|
</div>
|
||||||
if isForfeiter {
|
if isWinner {
|
||||||
<span class="text-red text-xs font-bold flex-shrink-0">FF</span>
|
|
||||||
} else if isWinner {
|
|
||||||
<span class="text-green text-xs flex-shrink-0">✓</span>
|
<span class="text-green text-xs flex-shrink-0">✓</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
if matchesToWin > 1 && forfeitTeamID == nil {
|
if matchesToWin > 1 {
|
||||||
<span class={ "text-sm font-mono flex-shrink-0 ml-2",
|
<span class={ "text-sm font-mono flex-shrink-0 ml-2",
|
||||||
templ.KV("text-text", !isWinner),
|
templ.KV("text-text", !isWinner),
|
||||||
templ.KV("text-green font-bold", isWinner) }>
|
templ.KV("text-green font-bold", isWinner) }>
|
||||||
|
|||||||
@@ -312,164 +312,30 @@ templ seriesUploadPrompt(series *db.PlayoffSeries) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
<div
|
<div class="bg-mantle border border-surface1 rounded-lg p-6 text-center">
|
||||||
x-data="{ open: false }"
|
|
||||||
class="bg-mantle border border-surface1 rounded-lg p-6 text-center"
|
|
||||||
>
|
|
||||||
if hasPendingMatches {
|
if hasPendingMatches {
|
||||||
<div class="text-4xl mb-3">📋</div>
|
<div class="text-4xl mb-3">📋</div>
|
||||||
<p class="text-lg text-text font-medium mb-2">Results Pending Review</p>
|
<p class="text-lg text-text font-medium mb-2">Results Pending Review</p>
|
||||||
<p class="text-sm text-subtext1 mb-4">Uploaded results are waiting to be reviewed and finalized.</p>
|
<p class="text-sm text-subtext1 mb-4">Uploaded results are waiting to be reviewed and finalized.</p>
|
||||||
<div class="flex items-center justify-center gap-3">
|
<a
|
||||||
<a
|
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/review", series.ID)) }
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/review", series.ID)) }
|
class="inline-block px-4 py-2 bg-green hover:bg-green/75 text-mantle rounded-lg
|
||||||
class="inline-block px-4 py-2 bg-green hover:bg-green/75 text-mantle rounded-lg
|
font-medium transition hover:cursor-pointer"
|
||||||
font-medium transition hover:cursor-pointer"
|
>
|
||||||
>
|
Review Results
|
||||||
Review Results
|
</a>
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="open = true"
|
|
||||||
class="inline-block px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
|
|
||||||
font-medium transition hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
Forfeit Series
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
} else {
|
} else {
|
||||||
<div class="text-4xl mb-3">📋</div>
|
<div class="text-4xl mb-3">📋</div>
|
||||||
<p class="text-lg text-text font-medium mb-2">No Results Uploaded</p>
|
<p class="text-lg text-text font-medium mb-2">No Results Uploaded</p>
|
||||||
<p class="text-sm text-subtext1 mb-4">Upload match log files to record the series results.</p>
|
<p class="text-sm text-subtext1 mb-4">Upload match log files to record the series results.</p>
|
||||||
<div class="flex items-center justify-center gap-3">
|
<a
|
||||||
<a
|
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/upload", series.ID)) }
|
||||||
href={ templ.SafeURL(fmt.Sprintf("/series/%d/results/upload", series.ID)) }
|
class="inline-block px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
|
||||||
class="inline-block px-4 py-2 bg-blue hover:bg-blue/80 text-mantle rounded-lg
|
font-medium transition hover:cursor-pointer"
|
||||||
font-medium transition hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
Upload Match Logs
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="open = true"
|
|
||||||
class="inline-block px-4 py-2 bg-red hover:bg-red/80 text-mantle rounded-lg
|
|
||||||
font-medium transition hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
Forfeit Series
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@seriesForfeitModal(series)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ seriesForfeitModal(series *db.PlayoffSeries) {
|
|
||||||
{{
|
|
||||||
team1Name := seriesTeamName(series.Team1)
|
|
||||||
team2Name := seriesTeamName(series.Team2)
|
|
||||||
}}
|
|
||||||
<div
|
|
||||||
x-show="open"
|
|
||||||
x-cloak
|
|
||||||
class="fixed inset-0 z-50 overflow-y-auto"
|
|
||||||
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-lg bg-mantle border-2 border-surface1 shadow-xl transition-all sm:w-full sm:max-w-lg"
|
|
||||||
@click.stop
|
|
||||||
x-data="{ forfeitTeam: '', forfeitReason: '' }"
|
|
||||||
>
|
>
|
||||||
<form
|
Upload Match Logs
|
||||||
hx-post={ fmt.Sprintf("/series/%d/forfeit", series.ID) }
|
</a>
|
||||||
hx-swap="none"
|
}
|
||||||
>
|
|
||||||
<div class="bg-mantle px-4 pb-4 pt-5 sm:p-6">
|
|
||||||
<div class="sm:flex sm:items-start">
|
|
||||||
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red/10 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<svg class="h-6 w-6 text-red" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
|
|
||||||
<h3 class="text-lg font-semibold leading-6 text-text">Forfeit Series</h3>
|
|
||||||
<div class="mt-2">
|
|
||||||
<p class="text-sm text-subtext0 mb-4">
|
|
||||||
This will forfeit the entire series. The selected team will lose and the opponent
|
|
||||||
will be declared the winner and advance. Any existing match results will be discarded.
|
|
||||||
This action is immediate and cannot be undone.
|
|
||||||
</p>
|
|
||||||
<!-- Team Selection -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="text-sm font-medium text-text">Which team is forfeiting?</label>
|
|
||||||
<select
|
|
||||||
name="forfeit_team"
|
|
||||||
x-model="forfeitTeam"
|
|
||||||
class="w-full px-3 py-2 bg-surface0 border border-surface1 rounded-lg text-text
|
|
||||||
focus:border-red focus:outline-none hover:cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="">Select a team...</option>
|
|
||||||
<option value="team1">{ team1Name }</option>
|
|
||||||
<option value="team2">{ team2Name }</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<!-- Reason -->
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
<label class="text-sm font-medium text-text">Reason (optional)</label>
|
|
||||||
<textarea
|
|
||||||
name="forfeit_reason"
|
|
||||||
x-model="forfeitReason"
|
|
||||||
placeholder="Provide a reason for the forfeit..."
|
|
||||||
class="w-full px-3 py-2 bg-surface0 border border-surface1 rounded-lg text-text
|
|
||||||
focus:border-blue focus:outline-none resize-none"
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-surface0 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-2">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="inline-flex w-full justify-center rounded-lg bg-red px-4 py-2 text-sm font-semibold text-mantle shadow-sm hover:bg-red/75 hover:cursor-pointer transition sm:w-auto"
|
|
||||||
:disabled="forfeitTeam === ''"
|
|
||||||
:class="forfeitTeam === '' && 'opacity-50 cursor-not-allowed'"
|
|
||||||
>
|
|
||||||
Confirm Forfeit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
@click="open = false"
|
|
||||||
class="mt-3 inline-flex w-full justify-center rounded-lg bg-surface1 px-4 py-2 text-sm font-semibold text-text shadow-sm hover:bg-surface2 hover:cursor-pointer transition sm:mt-0 sm:w-auto"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,21 +345,11 @@ templ seriesScoreDisplay(series *db.PlayoffSeries) {
|
|||||||
team1Won := series.WinnerTeamID != nil && series.Team1ID != nil && *series.WinnerTeamID == *series.Team1ID
|
team1Won := series.WinnerTeamID != nil && series.Team1ID != nil && *series.WinnerTeamID == *series.Team1ID
|
||||||
team2Won := series.WinnerTeamID != nil && series.Team2ID != nil && *series.WinnerTeamID == *series.Team2ID
|
team2Won := series.WinnerTeamID != nil && series.Team2ID != nil && *series.WinnerTeamID == *series.Team2ID
|
||||||
isBye := series.Status == db.SeriesStatusBye
|
isBye := series.Status == db.SeriesStatusBye
|
||||||
isForfeit := series.IsForfeit
|
|
||||||
forfeitTeamName := ""
|
|
||||||
if isForfeit && series.ForfeitTeam != nil {
|
|
||||||
forfeitTeamName = series.ForfeitTeam.Name
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
<div class="bg-mantle border border-surface1 rounded-lg overflow-hidden">
|
||||||
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
<div class="bg-surface0 border-b border-surface1 px-4 py-3 flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-text">Series Score</h2>
|
<h2 class="text-lg font-bold text-text">Series Score</h2>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
if isForfeit {
|
|
||||||
<span class="px-2 py-0.5 bg-red/20 text-red rounded text-xs font-medium">
|
|
||||||
Forfeited
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@seriesStatusBadge(series.Status)
|
@seriesStatusBadge(series.Status)
|
||||||
@seriesFormatBadge(series.MatchesToWin)
|
@seriesFormatBadge(series.MatchesToWin)
|
||||||
</div>
|
</div>
|
||||||
@@ -530,11 +386,7 @@ templ seriesScoreDisplay(series *db.PlayoffSeries) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span class="text-4xl text-subtext0 font-light leading-none">–</span>
|
<span class="text-4xl text-subtext0 font-light leading-none">–</span>
|
||||||
if isCompleted && isForfeit {
|
if isCompleted {
|
||||||
<span class="px-1.5 py-0.5 bg-red/20 text-red rounded text-xs font-semibold mt-1">
|
|
||||||
FORFEIT
|
|
||||||
</span>
|
|
||||||
} else if isCompleted {
|
|
||||||
<span class="px-1.5 py-0.5 bg-green/20 text-green rounded text-xs font-semibold mt-1">
|
<span class="px-1.5 py-0.5 bg-green/20 text-green rounded text-xs font-semibold mt-1">
|
||||||
FINAL
|
FINAL
|
||||||
</span>
|
</span>
|
||||||
@@ -560,18 +412,6 @@ templ seriesScoreDisplay(series *db.PlayoffSeries) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
if isForfeit && forfeitTeamName != "" {
|
|
||||||
<div class="text-center mt-2">
|
|
||||||
<p class="text-sm text-red/80">
|
|
||||||
{ forfeitTeamName } forfeited the series
|
|
||||||
</p>
|
|
||||||
if series.ForfeitReason != nil && *series.ForfeitReason != "" {
|
|
||||||
<p class="text-xs text-subtext0 mt-1">
|
|
||||||
{ *series.ForfeitReason }
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user