im actually goated

This commit is contained in:
2026-02-16 00:29:22 +11:00
parent 366aebfdf3
commit fbea6569cf
4 changed files with 334 additions and 237 deletions

View File

@@ -60,18 +60,19 @@ templ SeasonLeagueManageFixtures(season *db.Season, league *db.League, fixtures
<div class="flex items-center gap-3 mb-6">
<!-- Generate -->
<div class="flex items-center gap-2">
<input
type="number"
<label class="text-sm text-subtext0">Round:</label>
<select
x-model.number="generateRounds"
min="1"
max="20"
placeholder="Rounds"
class="w-24 py-2 px-3 rounded-lg text-sm bg-base border-2 border-overlay0
class="py-2 px-3 rounded-lg text-sm bg-base border-2 border-overlay0
focus:border-blue outline-none text-text"
/>
>
<template x-for="r in availableRounds" :key="r">
<option :value="r" x-text="r"></option>
</template>
</select>
<button
@click="generate()"
:disabled="isGenerating || generateRounds < 1"
:disabled="isGenerating || availableRounds.length === 0"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-blue hover:bg-blue/80 text-mantle transition
disabled:bg-blue/40 disabled:cursor-not-allowed"
@@ -79,32 +80,40 @@ templ SeasonLeagueManageFixtures(season *db.Season, league *db.League, fixtures
<span x-text="isGenerating ? 'Generating...' : 'Generate Round'"></span>
</button>
</div>
<!-- Clear All -->
<!-- Delete All -->
<button
x-show="allFixtures.length > 0"
@click="clearAll()"
@click="deleteAll()"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-red hover:bg-red/80 text-mantle transition"
>
Clear All
Delete All
</button>
<!-- Save -->
<button
x-show="unsavedChanges"
@click="save()"
:disabled="isSaving || !canSave()"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-green hover:bg-green/75 text-mantle transition
disabled:bg-green/40 disabled:cursor-not-allowed ml-auto"
>
<span x-text="isSaving ? 'Saving...' : 'Save'"></span>
</button>
<span
x-show="unsavedChanges && !canSave()"
class="text-yellow text-xs ml-2"
>
All game weeks must have at least 1 fixture
</span>
<!-- Save / Reset -->
<div x-show="unsavedChanges" class="flex items-center gap-2 ml-auto">
<span
x-show="!canSave()"
class="text-yellow text-xs"
>
All game weeks must have at least 1 fixture
</span>
<button
@click="reset()"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-surface1 hover:bg-surface2 text-text transition"
>
Reset
</button>
<button
@click="save()"
:disabled="isSaving || !canSave()"
class="rounded-lg px-4 py-2 hover:cursor-pointer text-center text-sm
bg-green hover:bg-green/75 text-mantle transition
disabled:bg-green/40 disabled:cursor-not-allowed"
>
<span x-text="isSaving ? 'Saving...' : 'Save'"></span>
</button>
</div>
</div>
<!-- Main content panels -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
@@ -114,7 +123,7 @@ templ SeasonLeagueManageFixtures(season *db.Season, league *db.League, fixtures
<div x-show="selectedGameWeek === null">
<h3 class="text-lg font-bold text-text mb-4">Game Weeks</h3>
<div x-show="allGameWeekNumbers.length === 0" class="text-subtext0 text-sm italic mb-4">
No game weeks yet. Add one to start allocating fixtures.
No game weeks yet. Generate fixtures to get started.
</div>
<div class="space-y-2">
<template x-for="week in allGameWeekNumbers" :key="week">
@@ -271,206 +280,272 @@ templ SeasonLeagueManageFixtures(season *db.Season, league *db.League, fixtures
</div>
<!-- Alpine.js component -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('fixturesManager', (initialFixtures, seasonShortName, leagueShortName) => ({
allFixtures: initialFixtures || [],
seasonShortName: seasonShortName,
leagueShortName: leagueShortName,
document.addEventListener("alpine:init", () => {
Alpine.data(
"fixturesManager",
(initialFixtures, seasonShortName, leagueShortName) => ({
allFixtures: initialFixtures || [],
_initialFixtures: JSON.parse(JSON.stringify(initialFixtures || [])),
seasonShortName: seasonShortName,
leagueShortName: leagueShortName,
// UI state
selectedGameWeek: null,
unsavedChanges: false,
isSaving: false,
isGenerating: false,
generateRounds: 1,
// UI state
selectedGameWeek: null,
unsavedChanges: false,
isSaving: false,
isGenerating: false,
generateRounds: null,
// Drag state
draggedFixture: null,
dropTarget: null,
init() {
this.generateRounds =
this.availableRounds.length > 0 ? this.availableRounds[0] : 1;
},
// Computed
get unallocatedFixtures() {
return this.allFixtures
.filter(f => f.gameWeek === null)
.sort((a, b) => a.round - b.round || a.id - b.id);
},
// Drag state
draggedFixture: null,
dropTarget: null,
get allGameWeekNumbers() {
const weeks = new Set();
for (const f of this.allFixtures) {
if (f.gameWeek !== null) {
weeks.add(f.gameWeek);
}
}
// Also include manually added empty weeks
for (const w of this._emptyWeeks || []) {
weeks.add(w);
}
return [...weeks].sort((a, b) => a - b);
},
// Computed
get unallocatedFixtures() {
return this.allFixtures
.filter((f) => f.gameWeek === null)
.sort((a, b) => a.round - b.round || a.id - b.id);
},
// Track empty weeks that user created
_emptyWeeks: [],
getGameWeekFixtures(week) {
return this.allFixtures
.filter(f => f.gameWeek === week)
.sort((a, b) => a.round - b.round || a.id - b.id);
},
getFixtureCount(week) {
return this.allFixtures.filter(f => f.gameWeek === week).length;
},
getPreview(week) {
return this.getGameWeekFixtures(week).slice(0, 3);
},
// Game week management
addGameWeek() {
const existing = this.allGameWeekNumbers;
const next = existing.length > 0 ? Math.max(...existing) + 1 : 1;
this._emptyWeeks.push(next);
this.unsavedChanges = true;
},
deleteGameWeek(week) {
if (this.getFixtureCount(week) > 0) return;
this._emptyWeeks = this._emptyWeeks.filter(w => w !== week);
if (this.selectedGameWeek === week) {
this.selectedGameWeek = null;
}
this.unsavedChanges = true;
},
selectGameWeek(week) {
this.selectedGameWeek = week;
},
backToList() {
this.selectedGameWeek = null;
},
// Drag and drop
onDragStart(event, fixture) {
this.draggedFixture = fixture;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', fixture.id);
},
onDragEnd() {
this.draggedFixture = null;
this.dropTarget = null;
},
onDrop(target) {
if (!this.draggedFixture) return;
const fixture = this.allFixtures.find(f => f.id === this.draggedFixture.id);
if (!fixture) return;
if (target === 'unallocated') {
fixture.gameWeek = null;
} else {
fixture.gameWeek = target;
// Remove from empty weeks if it now has fixtures
this._emptyWeeks = this._emptyWeeks.filter(w => w !== target);
}
this.unsavedChanges = true;
this.draggedFixture = null;
this.dropTarget = null;
},
unallocateFixture(fixture) {
const f = this.allFixtures.find(ff => ff.id === fixture.id);
if (f) {
const oldWeek = f.gameWeek;
f.gameWeek = null;
// If the old week is now empty, track it
if (oldWeek !== null && this.getFixtureCount(oldWeek) === 0) {
this._emptyWeeks.push(oldWeek);
}
this.unsavedChanges = true;
}
},
// Validation
canSave() {
const weeks = this.allGameWeekNumbers;
if (weeks.length === 0) return false;
for (const week of weeks) {
if (this.getFixtureCount(week) === 0) return false;
}
return true;
},
// Server actions
generate() {
if (this.generateRounds < 1) return;
this.isGenerating = true;
const form = new FormData();
form.append('season_short_name', this.seasonShortName);
form.append('league_short_name', this.leagueShortName);
form.append('round', this.generateRounds);
htmx.ajax('POST', '/fixtures/generate', {
target: '#manage-fixtures-content',
swap: 'outerHTML',
values: Object.fromEntries(form)
}).finally(() => {
this.isGenerating = false;
});
},
save() {
if (!this.canSave()) return;
this.isSaving = true;
const form = new FormData();
form.append('season_short_name', this.seasonShortName);
form.append('league_short_name', this.leagueShortName);
this.allFixtures.forEach((f, i) => {
form.append('allocations[' + i + '][id]', f.id);
form.append('allocations[' + i + '][game_week]', f.gameWeek !== null ? f.gameWeek : 0);
});
fetch('/fixtures/update-game-weeks', {
method: 'POST',
body: form
}).then(response => {
if (response.ok) {
this.unsavedChanges = false;
}
this.isSaving = false;
}).catch(() => {
this.isSaving = false;
});
},
clearAll() {
const seasonShort = this.seasonShortName;
const leagueShort = this.leagueShortName;
window.dispatchEvent(new CustomEvent('confirm-action', {
detail: {
title: 'Clear All Fixtures',
message: 'This will delete all fixtures for this league. This action cannot be undone.',
action: () => {
htmx.ajax('DELETE',
'/seasons/' + seasonShort + '/leagues/' + leagueShort + '/fixtures',
{
target: '#manage-fixtures-content',
swap: 'outerHTML'
}
);
get allGameWeekNumbers() {
const weeks = new Set();
for (const f of this.allFixtures) {
if (f.gameWeek !== null) {
weeks.add(f.gameWeek);
}
}
}));
},
}));
// Also include manually added empty weeks
for (const w of this._emptyWeeks || []) {
weeks.add(w);
}
return [...weeks].sort((a, b) => a - b);
},
get existingRounds() {
const rounds = new Set();
for (const f of this.allFixtures) {
rounds.add(f.round);
}
return rounds;
},
get availableRounds() {
const taken = this.existingRounds;
const maxTaken = taken.size > 0 ? Math.max(...taken) : 0;
const limit = maxTaken + 5;
const available = [];
for (let i = 1; i <= limit; i++) {
if (!taken.has(i)) available.push(i);
}
return available;
},
// Track empty weeks that user created
// Default to [1] if no fixtures have game weeks assigned
_emptyWeeks: (initialFixtures || []).some(
(f) => f.gameWeek !== null,
)
? []
: [1],
getGameWeekFixtures(week) {
return this.allFixtures
.filter((f) => f.gameWeek === week)
.sort((a, b) => a.round - b.round || a.id - b.id);
},
getFixtureCount(week) {
return this.allFixtures.filter((f) => f.gameWeek === week).length;
},
getPreview(week) {
return this.getGameWeekFixtures(week).slice(0, 3);
},
// Game week management
addGameWeek() {
const existing = this.allGameWeekNumbers;
const next = existing.length > 0 ? Math.max(...existing) + 1 : 1;
this._emptyWeeks.push(next);
this.unsavedChanges = true;
},
deleteGameWeek(week) {
if (this.getFixtureCount(week) > 0) return;
this._emptyWeeks = this._emptyWeeks.filter((w) => w !== week);
if (this.selectedGameWeek === week) {
this.selectedGameWeek = null;
}
this.unsavedChanges = true;
},
selectGameWeek(week) {
this.selectedGameWeek = week;
},
backToList() {
this.selectedGameWeek = null;
},
// Drag and drop
onDragStart(event, fixture) {
this.draggedFixture = fixture;
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", fixture.id);
},
onDragEnd() {
this.draggedFixture = null;
this.dropTarget = null;
},
onDrop(target) {
if (!this.draggedFixture) return;
const fixture = this.allFixtures.find(
(f) => f.id === this.draggedFixture.id,
);
if (!fixture) return;
if (target === "unallocated") {
fixture.gameWeek = null;
} else {
fixture.gameWeek = target;
// Remove from empty weeks if it now has fixtures
this._emptyWeeks = this._emptyWeeks.filter((w) => w !== target);
}
this.unsavedChanges = true;
this.draggedFixture = null;
this.dropTarget = null;
},
unallocateFixture(fixture) {
const f = this.allFixtures.find((ff) => ff.id === fixture.id);
if (f) {
const oldWeek = f.gameWeek;
f.gameWeek = null;
// If the old week is now empty, track it
if (oldWeek !== null && this.getFixtureCount(oldWeek) === 0) {
this._emptyWeeks.push(oldWeek);
}
this.unsavedChanges = true;
}
},
// Validation
canSave() {
const weeks = this.allGameWeekNumbers;
if (weeks.length === 0) return false;
for (const week of weeks) {
if (this.getFixtureCount(week) === 0) return false;
}
return true;
},
reset() {
this.allFixtures = JSON.parse(
JSON.stringify(this._initialFixtures),
);
this._emptyWeeks = this._initialFixtures.some(
(f) => f.gameWeek !== null,
)
? []
: [1];
this.selectedGameWeek = null;
this.unsavedChanges = false;
},
// Server actions
generate() {
if (this.generateRounds < 1) return;
this.isGenerating = true;
const form = new FormData();
form.append("season_short_name", this.seasonShortName);
form.append("league_short_name", this.leagueShortName);
form.append("round", this.generateRounds);
htmx
.ajax("POST", "/fixtures/generate", {
target: "#manage-fixtures-content",
swap: "outerHTML",
values: Object.fromEntries(form),
})
.finally(() => {
this.isGenerating = false;
});
},
save() {
if (!this.canSave()) return;
this.isSaving = true;
const params = new URLSearchParams();
params.append("season_short_name", this.seasonShortName);
params.append("league_short_name", this.leagueShortName);
this.allFixtures.forEach((f, i) => {
params.append("allocations[" + i + "][id]", f.id);
params.append(
"allocations[" + i + "][game_week]",
f.gameWeek !== null ? f.gameWeek : 0,
);
});
fetch("/fixtures/update-game-weeks", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params.toString(),
})
.then((response) => {
if (response.ok) {
this.unsavedChanges = false;
}
this.isSaving = false;
})
.catch(() => {
this.isSaving = false;
});
},
deleteAll() {
const seasonShort = this.seasonShortName;
const leagueShort = this.leagueShortName;
window.dispatchEvent(
new CustomEvent("confirm-action", {
detail: {
title: "Delete All Fixtures",
message:
"This will delete all fixtures for this league. This action cannot be undone.",
action: () => {
htmx.ajax(
"DELETE",
"/seasons/" +
seasonShort +
"/leagues/" +
leagueShort +
"/fixtures",
{
target: "#manage-fixtures-content",
swap: "outerHTML",
},
);
},
},
}),
);
},
}),
);
});
</script>
</div>