447 lines
13 KiB
JavaScript
447 lines
13 KiB
JavaScript
// Game management Alpine.js component
|
|
function gameManager() {
|
|
return {
|
|
showLogForm: false,
|
|
games: [],
|
|
commanders: [],
|
|
loading: false,
|
|
submitting: false,
|
|
editSubmitting: false,
|
|
editingGame: null,
|
|
serverError: '',
|
|
|
|
// Delete confirmation modal
|
|
deleteConfirm: {
|
|
show: false,
|
|
gameId: null,
|
|
deleting: false
|
|
},
|
|
|
|
// Game form data
|
|
newGame: {
|
|
date: new Date().toISOString().split('T')[0],
|
|
commanderId: '',
|
|
playerCount: 4,
|
|
won: false,
|
|
rounds: 8,
|
|
startingPlayerWon: false,
|
|
solRingTurnOneWon: false,
|
|
notes: ''
|
|
},
|
|
|
|
// Pagination - load more pattern
|
|
pagination: {
|
|
offset: 0,
|
|
limit: 20,
|
|
hasMore: false,
|
|
isLoadingMore: false
|
|
},
|
|
|
|
// Computed form data - returns editingGame if editing, otherwise newGame
|
|
get formData() {
|
|
return this.editingGame || this.newGame
|
|
},
|
|
|
|
async init() {
|
|
await Promise.all([this.loadCommanders(), this.loadGames()])
|
|
this.loadPrefilled()
|
|
},
|
|
|
|
async reloadStats() {
|
|
try {
|
|
const token =
|
|
localStorage.getItem('edh-stats-token') ||
|
|
sessionStorage.getItem('edh-stats-token')
|
|
const response = await fetch('/api/stats/overview', {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
// Set a flag for dashboard to refresh when user navigates back
|
|
localStorage.setItem('edh-stats-dirty', 'true')
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to reload stats:', error)
|
|
}
|
|
},
|
|
|
|
loadPrefilled() {
|
|
const prefilled = localStorage.getItem('edh-prefill-game')
|
|
if (prefilled) {
|
|
try {
|
|
const data = JSON.parse(prefilled)
|
|
|
|
// Populate the form with prefilled values
|
|
this.newGame.date =
|
|
data.date || new Date().toISOString().split('T')[0]
|
|
this.newGame.rounds = data.rounds || 8
|
|
this.newGame.notes =
|
|
`Ended after ${data.rounds} rounds in ${data.duration}.\nAverage time/round: ${data.avgTimePerRound}` ||
|
|
''
|
|
|
|
// Show the form automatically
|
|
this.showLogForm = true
|
|
|
|
// Clear the prefilled data from localStorage
|
|
localStorage.removeItem('edh-prefill-game')
|
|
|
|
// Scroll to the form
|
|
setTimeout(() => {
|
|
document
|
|
.querySelector('form')
|
|
?.scrollIntoView({ behavior: 'smooth' })
|
|
}, 100)
|
|
} catch (error) {
|
|
console.error('Error loading prefilled game data:', error)
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadCommanders() {
|
|
try {
|
|
const response = await fetch('/api/commanders', {
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
|
}
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
this.commanders = data.commanders || []
|
|
}
|
|
} catch (error) {
|
|
console.error('Load commanders error:', error)
|
|
}
|
|
},
|
|
|
|
async loadGames() {
|
|
this.loading = true
|
|
this.serverError = ''
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`/api/games?limit=${this.pagination.limit}&offset=${this.pagination.offset}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
|
}
|
|
}
|
|
)
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
this.games = data.games || []
|
|
this.pagination.hasMore = data.pagination?.hasMore || false
|
|
} else {
|
|
this.serverError = 'Failed to load games'
|
|
}
|
|
} catch (error) {
|
|
console.error('Load games error:', error)
|
|
this.serverError = 'Network error occurred'
|
|
} finally {
|
|
this.loading = false
|
|
}
|
|
},
|
|
|
|
async loadMore() {
|
|
this.pagination.isLoadingMore = true
|
|
this.serverError = ''
|
|
|
|
try {
|
|
this.pagination.offset += this.pagination.limit
|
|
const response = await fetch(
|
|
`/api/games?limit=${this.pagination.limit}&offset=${this.pagination.offset}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
|
}
|
|
}
|
|
)
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
this.games = [...this.games, ...(data.games || [])]
|
|
this.pagination.hasMore = data.pagination?.hasMore || false
|
|
} else {
|
|
this.serverError = 'Failed to load more games'
|
|
// Revert offset on error
|
|
this.pagination.offset -= this.pagination.limit
|
|
}
|
|
} catch (error) {
|
|
console.error('Load more games error:', error)
|
|
this.serverError = 'Network error occurred'
|
|
// Revert offset on error
|
|
this.pagination.offset -= this.pagination.limit
|
|
} finally {
|
|
this.pagination.isLoadingMore = false
|
|
}
|
|
},
|
|
|
|
async handleLogGame() {
|
|
this.serverError = ''
|
|
|
|
// Basic validation
|
|
if (!this.formData.commanderId) {
|
|
this.serverError = 'Please select a commander'
|
|
return
|
|
}
|
|
|
|
if (this.editingGame) {
|
|
await this.handleUpdateGame()
|
|
} else {
|
|
await this.handleCreateGame()
|
|
}
|
|
},
|
|
|
|
async handleCreateGame() {
|
|
this.submitting = true
|
|
|
|
try {
|
|
// Ensure boolean values are actual booleans, not strings
|
|
const payload = {
|
|
date: this.newGame.date,
|
|
commanderId: parseInt(this.newGame.commanderId),
|
|
playerCount: parseInt(this.newGame.playerCount),
|
|
rounds: parseInt(this.newGame.rounds),
|
|
won: this.newGame.won === true || this.newGame.won === 'true',
|
|
startingPlayerWon:
|
|
this.newGame.startingPlayerWon === true ||
|
|
this.newGame.startingPlayerWon === 'true',
|
|
solRingTurnOneWon:
|
|
this.newGame.solRingTurnOneWon === true ||
|
|
this.newGame.solRingTurnOneWon === 'true'
|
|
}
|
|
|
|
// Only include notes if it's not empty
|
|
if (this.newGame.notes && this.newGame.notes.trim()) {
|
|
payload.notes = this.newGame.notes
|
|
}
|
|
|
|
const response = await fetch('/api/games', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
this.games.unshift(data.game)
|
|
this.resetForm()
|
|
this.showLogForm = false
|
|
await this.reloadStats()
|
|
} else {
|
|
const errorData = await response.json()
|
|
this.serverError = errorData.message || 'Failed to log game'
|
|
}
|
|
} catch (error) {
|
|
console.error('Log game error:', error)
|
|
this.serverError = 'Network error occurred'
|
|
} finally {
|
|
this.submitting = false
|
|
}
|
|
},
|
|
|
|
async handleUpdateGame() {
|
|
this.editSubmitting = true
|
|
|
|
try {
|
|
const payload = {
|
|
date: this.editingGame.date,
|
|
commanderId: parseInt(this.editingGame.commanderId),
|
|
playerCount: parseInt(this.editingGame.playerCount),
|
|
rounds: parseInt(this.editingGame.rounds),
|
|
won: this.editingGame.won === true || this.editingGame.won === 'true',
|
|
startingPlayerWon:
|
|
this.editingGame.startingPlayerWon === true ||
|
|
this.editingGame.startingPlayerWon === 'true',
|
|
solRingTurnOneWon:
|
|
this.editingGame.solRingTurnOneWon === true ||
|
|
this.editingGame.solRingTurnOneWon === 'true'
|
|
}
|
|
|
|
// Only include notes if it's not empty
|
|
if (this.editingGame.notes && this.editingGame.notes.trim()) {
|
|
payload.notes = this.editingGame.notes
|
|
}
|
|
|
|
const response = await fetch(`/api/games/${this.editingGame.id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
if (response.ok) {
|
|
const data = await response.json()
|
|
const index = this.games.findIndex(
|
|
(g) => g.id === this.editingGame.id
|
|
)
|
|
if (index !== -1) {
|
|
this.games[index] = data.game
|
|
}
|
|
this.cancelEdit()
|
|
await this.reloadStats()
|
|
} else {
|
|
const errorData = await response.json()
|
|
this.serverError = errorData.message || 'Failed to update game'
|
|
}
|
|
} catch (error) {
|
|
console.error('Update game error:', error)
|
|
this.serverError = 'Network error occurred'
|
|
} finally {
|
|
this.editSubmitting = false
|
|
}
|
|
},
|
|
|
|
editGame(gameId) {
|
|
const game = this.games.find((g) => g.id === gameId)
|
|
if (game) {
|
|
// Convert date from MM/DD/YYYY to YYYY-MM-DD format for input type="date"
|
|
let dateForInput = game.date
|
|
if (dateForInput && dateForInput.includes('/')) {
|
|
const [month, day, year] = dateForInput.split('/')
|
|
dateForInput = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
}
|
|
|
|
this.editingGame = {
|
|
id: game.id,
|
|
date: dateForInput,
|
|
commanderId: game.commanderId,
|
|
playerCount: game.playerCount,
|
|
won: game.won === 1 || game.won === true,
|
|
rounds: game.rounds,
|
|
startingPlayerWon:
|
|
game.startingPlayerWon === 1 || game.startingPlayerWon === true,
|
|
solRingTurnOneWon:
|
|
game.solRingTurnOneWon === 1 || game.solRingTurnOneWon === true,
|
|
notes: game.notes
|
|
}
|
|
this.showLogForm = true
|
|
this.serverError = ''
|
|
setTimeout(() => {
|
|
document.querySelector('form')?.scrollIntoView({ behavior: 'smooth' })
|
|
}, 100)
|
|
}
|
|
},
|
|
|
|
cancelEdit() {
|
|
this.editingGame = null
|
|
this.resetForm()
|
|
this.showLogForm = false
|
|
},
|
|
|
|
deleteGame(gameId) {
|
|
this.deleteConfirm.gameId = gameId
|
|
this.deleteConfirm.show = true
|
|
},
|
|
|
|
async confirmDelete() {
|
|
const gameId = this.deleteConfirm.gameId
|
|
if (!gameId) return
|
|
|
|
this.deleteConfirm.deleting = true
|
|
|
|
try {
|
|
const response = await fetch(`/api/games/${gameId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
|
}
|
|
})
|
|
|
|
if (response.ok) {
|
|
this.games = this.games.filter((g) => g.id !== gameId)
|
|
this.deleteConfirm.show = false
|
|
this.deleteConfirm.gameId = null
|
|
await this.reloadStats()
|
|
} else {
|
|
this.serverError = 'Failed to delete game'
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete game error:', error)
|
|
this.serverError = 'Network error occurred while deleting'
|
|
} finally {
|
|
this.deleteConfirm.deleting = false
|
|
}
|
|
},
|
|
|
|
resetForm() {
|
|
this.newGame = {
|
|
date: new Date().toISOString().split('T')[0],
|
|
commanderId: '',
|
|
playerCount: 4,
|
|
won: false,
|
|
rounds: 8,
|
|
startingPlayerWon: false,
|
|
solRingTurnOneWon: false,
|
|
notes: ''
|
|
}
|
|
this.editingGame = null
|
|
this.serverError = ''
|
|
},
|
|
|
|
|
|
|
|
getCommanderName(id) {
|
|
const commander = this.commanders.find((c) => c.id === id)
|
|
return commander ? commander.name : 'Unknown Commander'
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})
|
|
},
|
|
|
|
async exportGames() {
|
|
try {
|
|
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
|
const response = await fetch('/api/games/export', {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Export failed')
|
|
}
|
|
|
|
// Generate filename with current date
|
|
const today = new Date().toLocaleDateString('en-US').replace(/\//g, '_')
|
|
const filename = `edh_games_${today}.json`
|
|
|
|
// Create blob and download
|
|
const blob = await response.blob()
|
|
const url = window.URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.style.display = 'none'
|
|
a.href = url
|
|
a.download = filename
|
|
document.body.appendChild(a)
|
|
a.click()
|
|
window.URL.revokeObjectURL(url)
|
|
document.body.removeChild(a)
|
|
} catch (error) {
|
|
console.error('Export failed:', error)
|
|
// Show error message to user
|
|
this.serverError = 'Failed to export games. Please try again.'
|
|
setTimeout(() => {
|
|
this.serverError = ''
|
|
}, 5000)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('gameManager', gameManager)
|
|
})
|