mirror of
https://github.com/jcwimer/wrestlingApp
synced 2026-03-25 01:14:43 +00:00
Added Stimulus and moved the matstats vanilla js to stimulus controllers. Same with the spectate page.
This commit is contained in:
384
app/assets/javascripts/controllers/match_data_controller.js
Normal file
384
app/assets/javascripts/controllers/match_data_controller.js
Normal file
@@ -0,0 +1,384 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Stat", "w2Stat", "statusIndicator"
|
||||
]
|
||||
|
||||
static values = {
|
||||
tournamentId: Number,
|
||||
boutNumber: Number,
|
||||
matchId: Number
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match data controller connected")
|
||||
|
||||
this.w1 = {
|
||||
name: "w1",
|
||||
stats: "",
|
||||
updated_at: null,
|
||||
timers: {
|
||||
"injury": { time: 0, startTime: null, interval: null },
|
||||
"blood": { time: 0, startTime: null, interval: null }
|
||||
}
|
||||
}
|
||||
|
||||
this.w2 = {
|
||||
name: "w2",
|
||||
stats: "",
|
||||
updated_at: null,
|
||||
timers: {
|
||||
"injury": { time: 0, startTime: null, interval: null },
|
||||
"blood": { time: 0, startTime: null, interval: null }
|
||||
}
|
||||
}
|
||||
|
||||
// Initial values
|
||||
this.updateJsValues()
|
||||
|
||||
// Set up debounced handlers for text areas
|
||||
this.debouncedW1Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w1), 400)
|
||||
this.debouncedW2Handler = this.debounce((el) => this.handleTextAreaInput(el, this.w2), 400)
|
||||
|
||||
// Set up text area event listeners
|
||||
this.w1StatTarget.addEventListener('input', (event) => this.debouncedW1Handler(event.target))
|
||||
this.w2StatTarget.addEventListener('input', (event) => this.debouncedW2Handler(event.target))
|
||||
|
||||
// Initialize from local storage
|
||||
this.initializeFromLocalStorage()
|
||||
|
||||
// Setup ActionCable
|
||||
if (this.matchIdValue) {
|
||||
this.setupSubscription(this.matchIdValue)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanupSubscription()
|
||||
}
|
||||
|
||||
// Match stats core functionality
|
||||
updateStats(wrestler, text) {
|
||||
if (!wrestler) {
|
||||
console.error("updateStats called with undefined wrestler")
|
||||
return
|
||||
}
|
||||
|
||||
wrestler.stats += text + " "
|
||||
wrestler.updated_at = new Date().toISOString()
|
||||
this.updateHtmlValues()
|
||||
this.saveToLocalStorage(wrestler)
|
||||
|
||||
// Send the update via Action Cable if subscribed
|
||||
if (this.matchSubscription) {
|
||||
let payload = {}
|
||||
if (wrestler.name === 'w1') payload.new_w1_stat = wrestler.stats
|
||||
else if (wrestler.name === 'w2') payload.new_w2_stat = wrestler.stats
|
||||
|
||||
if (Object.keys(payload).length > 0) {
|
||||
console.log('[ActionCable] updateStats performing send_stat:', payload)
|
||||
this.matchSubscription.perform('send_stat', payload)
|
||||
}
|
||||
} else {
|
||||
console.warn('[ActionCable] updateStats called but matchSubscription is null.')
|
||||
}
|
||||
}
|
||||
|
||||
// Specific methods for updating each wrestler
|
||||
updateW1Stats(event) {
|
||||
const text = event.currentTarget.dataset.matchDataText || ''
|
||||
this.updateStats(this.w1, text)
|
||||
}
|
||||
|
||||
updateW2Stats(event) {
|
||||
const text = event.currentTarget.dataset.matchDataText || ''
|
||||
this.updateStats(this.w2, text)
|
||||
}
|
||||
|
||||
// End period action
|
||||
endPeriod() {
|
||||
this.updateStats(this.w1, '|End Period|')
|
||||
this.updateStats(this.w2, '|End Period|')
|
||||
}
|
||||
|
||||
handleTextAreaInput(textAreaElement, wrestler) {
|
||||
const newValue = textAreaElement.value
|
||||
console.log(`Text area input detected for ${wrestler.name}:`, newValue.substring(0, 50) + "...")
|
||||
|
||||
// Update the internal JS object
|
||||
wrestler.stats = newValue
|
||||
wrestler.updated_at = new Date().toISOString()
|
||||
|
||||
// Save to localStorage
|
||||
this.saveToLocalStorage(wrestler)
|
||||
|
||||
// Send the update via Action Cable if subscribed
|
||||
if (this.matchSubscription) {
|
||||
let payload = {}
|
||||
if (wrestler.name === 'w1') {
|
||||
payload.new_w1_stat = wrestler.stats
|
||||
} else if (wrestler.name === 'w2') {
|
||||
payload.new_w2_stat = wrestler.stats
|
||||
}
|
||||
if (Object.keys(payload).length > 0) {
|
||||
console.log('[ActionCable] Performing send_stat from textarea with payload:', payload)
|
||||
this.matchSubscription.perform('send_stat', payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer functions
|
||||
startTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
if (timer.interval) return // Prevent multiple intervals
|
||||
|
||||
timer.startTime = Date.now()
|
||||
timer.interval = setInterval(() => {
|
||||
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
|
||||
this.updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
stopTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
if (!timer.interval || !timer.startTime) return
|
||||
|
||||
clearInterval(timer.interval)
|
||||
const elapsedSeconds = Math.floor((Date.now() - timer.startTime) / 1000)
|
||||
timer.time += elapsedSeconds
|
||||
timer.interval = null
|
||||
timer.startTime = null
|
||||
|
||||
this.saveToLocalStorage(wrestler)
|
||||
this.updateTimerDisplay(wrestler, timerKey, timer.time)
|
||||
this.updateStatsBox(wrestler, timerKey, elapsedSeconds)
|
||||
}
|
||||
|
||||
resetTimer(wrestler, timerKey) {
|
||||
const timer = wrestler.timers[timerKey]
|
||||
this.stopTimer(wrestler, timerKey)
|
||||
timer.time = 0
|
||||
this.updateTimerDisplay(wrestler, timerKey, 0)
|
||||
this.saveToLocalStorage(wrestler)
|
||||
}
|
||||
|
||||
// Timer control methods for W1
|
||||
startW1InjuryTimer() {
|
||||
this.startTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
stopW1InjuryTimer() {
|
||||
this.stopTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
resetW1InjuryTimer() {
|
||||
this.resetTimer(this.w1, 'injury')
|
||||
}
|
||||
|
||||
startW1BloodTimer() {
|
||||
this.startTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
stopW1BloodTimer() {
|
||||
this.stopTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
resetW1BloodTimer() {
|
||||
this.resetTimer(this.w1, 'blood')
|
||||
}
|
||||
|
||||
// Timer control methods for W2
|
||||
startW2InjuryTimer() {
|
||||
this.startTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
stopW2InjuryTimer() {
|
||||
this.stopTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
resetW2InjuryTimer() {
|
||||
this.resetTimer(this.w2, 'injury')
|
||||
}
|
||||
|
||||
startW2BloodTimer() {
|
||||
this.startTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
stopW2BloodTimer() {
|
||||
this.stopTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
resetW2BloodTimer() {
|
||||
this.resetTimer(this.w2, 'blood')
|
||||
}
|
||||
|
||||
updateTimerDisplay(wrestler, timerKey, totalTime) {
|
||||
const elementId = `${wrestler.name}-${timerKey}-time`
|
||||
const element = document.getElementById(elementId)
|
||||
if (element) {
|
||||
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`
|
||||
}
|
||||
}
|
||||
|
||||
updateStatsBox(wrestler, timerKey, elapsedSeconds) {
|
||||
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time"
|
||||
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`
|
||||
this.updateStats(wrestler, `${timerType}: ${formattedTime}`)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
generateKey(wrestler_name) {
|
||||
return `${wrestler_name}-${this.tournamentIdValue}-${this.boutNumberValue}`
|
||||
}
|
||||
|
||||
loadFromLocalStorage(wrestler_name) {
|
||||
const key = this.generateKey(wrestler_name)
|
||||
const data = localStorage.getItem(key)
|
||||
return data ? JSON.parse(data) : null
|
||||
}
|
||||
|
||||
saveToLocalStorage(person) {
|
||||
const key = this.generateKey(person.name)
|
||||
const data = {
|
||||
stats: person.stats,
|
||||
updated_at: person.updated_at,
|
||||
timers: person.timers
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
updateHtmlValues() {
|
||||
if (this.w1StatTarget) this.w1StatTarget.value = this.w1.stats
|
||||
if (this.w2StatTarget) this.w2StatTarget.value = this.w2.stats
|
||||
}
|
||||
|
||||
updateJsValues() {
|
||||
if (this.w1StatTarget) this.w1.stats = this.w1StatTarget.value
|
||||
if (this.w2StatTarget) this.w2.stats = this.w2StatTarget.value
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout
|
||||
return (...args) => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
initializeTimers(wrestler) {
|
||||
for (const timerKey in wrestler.timers) {
|
||||
this.updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time)
|
||||
}
|
||||
}
|
||||
|
||||
initializeFromLocalStorage() {
|
||||
const w1Data = this.loadFromLocalStorage('w1')
|
||||
if (w1Data) {
|
||||
this.w1.stats = w1Data.stats || ''
|
||||
this.w1.updated_at = w1Data.updated_at
|
||||
if (w1Data.timers) this.w1.timers = w1Data.timers
|
||||
this.initializeTimers(this.w1)
|
||||
}
|
||||
|
||||
const w2Data = this.loadFromLocalStorage('w2')
|
||||
if (w2Data) {
|
||||
this.w2.stats = w2Data.stats || ''
|
||||
this.w2.updated_at = w2Data.updated_at
|
||||
if (w2Data.timers) this.w2.timers = w2Data.timers
|
||||
this.initializeTimers(this.w2)
|
||||
}
|
||||
|
||||
this.updateHtmlValues()
|
||||
}
|
||||
|
||||
cleanupSubscription() {
|
||||
if (this.matchSubscription) {
|
||||
console.log(`[Stats AC Cleanup] Unsubscribing from match channel.`)
|
||||
try {
|
||||
this.matchSubscription.unsubscribe()
|
||||
} catch (e) {
|
||||
console.error(`[Stats AC Cleanup] Error during unsubscribe:`, e)
|
||||
}
|
||||
this.matchSubscription = null
|
||||
}
|
||||
}
|
||||
|
||||
setupSubscription(matchId) {
|
||||
this.cleanupSubscription()
|
||||
console.log(`[Stats AC Setup] Attempting subscription for match ID: ${matchId}`)
|
||||
|
||||
// Update status indicator
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Connecting to server for real-time stat updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-warning', 'alert-danger')
|
||||
this.statusIndicatorTarget.classList.add('alert-info')
|
||||
}
|
||||
|
||||
// Exit if we don't have App.cable
|
||||
if (!window.App || !window.App.cable) {
|
||||
console.error(`[Stats AC Setup] Error: App.cable is not available.`)
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Error: WebSockets unavailable. Stats won't update in real-time."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.matchSubscription = App.cable.subscriptions.create(
|
||||
{
|
||||
channel: "MatchChannel",
|
||||
match_id: matchId
|
||||
},
|
||||
{
|
||||
connected: () => {
|
||||
console.log(`[Stats AC] Connected to MatchStatsChannel for match ID: ${matchId}`)
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Connected: Stats will update in real-time."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-warning', 'alert-danger')
|
||||
this.statusIndicatorTarget.classList.add('alert-success')
|
||||
}
|
||||
},
|
||||
|
||||
disconnected: () => {
|
||||
console.log(`[Stats AC] Disconnected from MatchStatsChannel`)
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Disconnected: Stats updates paused."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-danger')
|
||||
this.statusIndicatorTarget.classList.add('alert-warning')
|
||||
}
|
||||
},
|
||||
|
||||
received: (data) => {
|
||||
console.log(`[Stats AC] Received data:`, data)
|
||||
|
||||
// Update w1 stats
|
||||
if (data.w1_stat !== undefined && this.w1StatTarget) {
|
||||
console.log(`[Stats AC] Updating w1_stat: ${data.w1_stat.substring(0, 30)}...`)
|
||||
this.w1.stats = data.w1_stat
|
||||
this.w1StatTarget.value = data.w1_stat
|
||||
}
|
||||
|
||||
// Update w2 stats
|
||||
if (data.w2_stat !== undefined && this.w2StatTarget) {
|
||||
console.log(`[Stats AC] Updating w2_stat: ${data.w2_stat.substring(0, 30)}...`)
|
||||
this.w2.stats = data.w2_stat
|
||||
this.w2StatTarget.value = data.w2_stat
|
||||
}
|
||||
},
|
||||
|
||||
receive_error: (error) => {
|
||||
console.error(`[Stats AC] Error:`, error)
|
||||
this.matchSubscription = null
|
||||
|
||||
if (this.statusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.innerText = "Error: Connection issue. Stats won't update in real-time."
|
||||
this.statusIndicatorTarget.classList.remove('alert-info', 'alert-success', 'alert-warning')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
237
app/assets/javascripts/controllers/match_score_controller.js
Normal file
237
app/assets/javascripts/controllers/match_score_controller.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"winType", "winnerSelect", "submitButton", "dynamicScoreInput",
|
||||
"finalScoreField", "validationAlerts", "pinTimeTip"
|
||||
]
|
||||
|
||||
static values = {
|
||||
winnerScore: { type: String, default: "0" },
|
||||
loserScore: { type: String, default: "0" }
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match score controller connected")
|
||||
// Use setTimeout to ensure the DOM is fully loaded
|
||||
setTimeout(() => {
|
||||
this.updateScoreInput()
|
||||
this.validateForm()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
winTypeChanged() {
|
||||
this.updateScoreInput()
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
winnerChanged() {
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
updateScoreInput() {
|
||||
const winType = this.winTypeTarget.value
|
||||
this.dynamicScoreInputTarget.innerHTML = ""
|
||||
|
||||
// Add section header
|
||||
const header = document.createElement("h5")
|
||||
header.innerText = `Score Input for ${winType}`
|
||||
header.classList.add("mt-2", "mb-3")
|
||||
this.dynamicScoreInputTarget.appendChild(header)
|
||||
|
||||
if (winType === "Pin") {
|
||||
this.pinTimeTipTarget.style.display = "block"
|
||||
|
||||
const minuteInput = this.createTextInput("minutes", "Minutes (MM)", "Pin Time Minutes")
|
||||
const secondInput = this.createTextInput("seconds", "Seconds (SS)", "Pin Time Seconds")
|
||||
|
||||
this.dynamicScoreInputTarget.appendChild(minuteInput)
|
||||
this.dynamicScoreInputTarget.appendChild(secondInput)
|
||||
|
||||
// Add event listeners to the new inputs
|
||||
const inputs = this.dynamicScoreInputTarget.querySelectorAll("input")
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
this.updatePinTimeScore()
|
||||
this.validateForm()
|
||||
})
|
||||
})
|
||||
|
||||
this.updatePinTimeScore()
|
||||
} else if (["Decision", "Major", "Tech Fall"].includes(winType)) {
|
||||
this.pinTimeTipTarget.style.display = "none"
|
||||
|
||||
const winnerScoreInput = this.createTextInput(
|
||||
"winner-score",
|
||||
"Winner's Score",
|
||||
"Enter the winner's score"
|
||||
)
|
||||
const loserScoreInput = this.createTextInput(
|
||||
"loser-score",
|
||||
"Loser's Score",
|
||||
"Enter the loser's score"
|
||||
)
|
||||
|
||||
this.dynamicScoreInputTarget.appendChild(winnerScoreInput)
|
||||
this.dynamicScoreInputTarget.appendChild(loserScoreInput)
|
||||
|
||||
// Restore stored values
|
||||
const winnerInput = winnerScoreInput.querySelector("input")
|
||||
const loserInput = loserScoreInput.querySelector("input")
|
||||
|
||||
winnerInput.value = this.winnerScoreValue
|
||||
loserInput.value = this.loserScoreValue
|
||||
|
||||
// Add event listeners to the new inputs
|
||||
winnerInput.addEventListener("input", (event) => {
|
||||
this.winnerScoreValue = event.target.value || "0"
|
||||
this.updatePointScore()
|
||||
this.validateForm()
|
||||
})
|
||||
|
||||
loserInput.addEventListener("input", (event) => {
|
||||
this.loserScoreValue = event.target.value || "0"
|
||||
this.updatePointScore()
|
||||
this.validateForm()
|
||||
})
|
||||
|
||||
this.updatePointScore()
|
||||
} else {
|
||||
// For other types (forfeit, etc.), clear the input and hide pin time tip
|
||||
this.pinTimeTipTarget.style.display = "none"
|
||||
this.finalScoreFieldTarget.value = ""
|
||||
|
||||
// Show message for non-score win types
|
||||
const message = document.createElement("p")
|
||||
message.innerText = `No score required for ${winType} win type.`
|
||||
message.classList.add("text-muted")
|
||||
this.dynamicScoreInputTarget.appendChild(message)
|
||||
}
|
||||
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
updatePinTimeScore() {
|
||||
const minuteInput = this.dynamicScoreInputTarget.querySelector("#minutes")
|
||||
const secondInput = this.dynamicScoreInputTarget.querySelector("#seconds")
|
||||
|
||||
if (minuteInput && secondInput) {
|
||||
const minutes = (minuteInput.value || "0").padStart(2, "0")
|
||||
const seconds = (secondInput.value || "0").padStart(2, "0")
|
||||
this.finalScoreFieldTarget.value = `${minutes}:${seconds}`
|
||||
|
||||
// Validate after updating pin time
|
||||
this.validateForm()
|
||||
}
|
||||
}
|
||||
|
||||
updatePointScore() {
|
||||
const winnerScore = this.winnerScoreValue || "0"
|
||||
const loserScore = this.loserScoreValue || "0"
|
||||
this.finalScoreFieldTarget.value = `${winnerScore}-${loserScore}`
|
||||
|
||||
// Validate immediately after updating scores
|
||||
this.validateForm()
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
const winType = this.winTypeTarget.value
|
||||
const winner = this.winnerSelectTarget?.value
|
||||
let isValid = true
|
||||
let alertMessage = ""
|
||||
let winTypeShouldBe = "Decision"
|
||||
|
||||
// Clear previous validation messages
|
||||
this.validationAlertsTarget.innerHTML = ""
|
||||
this.validationAlertsTarget.style.display = "none"
|
||||
this.validationAlertsTarget.classList.remove("alert", "alert-danger", "p-3")
|
||||
|
||||
if (["Decision", "Major", "Tech Fall"].includes(winType)) {
|
||||
// Get scores and ensure they're valid numbers
|
||||
const winnerScore = parseInt(this.winnerScoreValue || "0", 10)
|
||||
const loserScore = parseInt(this.loserScoreValue || "0", 10)
|
||||
|
||||
console.log(`Validating scores: winner=${winnerScore}, loser=${loserScore}, type=${winType}`)
|
||||
|
||||
// Check if winner score > loser score
|
||||
if (winnerScore <= loserScore) {
|
||||
isValid = false
|
||||
alertMessage += "<strong>Error:</strong> Winner's score must be higher than loser's score.<br>"
|
||||
} else {
|
||||
// Calculate score difference and determine correct win type
|
||||
const scoreDifference = winnerScore - loserScore
|
||||
|
||||
if (scoreDifference < 8) {
|
||||
winTypeShouldBe = "Decision"
|
||||
} else if (scoreDifference >= 8 && scoreDifference < 15) {
|
||||
winTypeShouldBe = "Major"
|
||||
} else if (scoreDifference >= 15) {
|
||||
winTypeShouldBe = "Tech Fall"
|
||||
}
|
||||
|
||||
// Check if selected win type matches the correct one based on score difference
|
||||
if (winTypeShouldBe !== winType) {
|
||||
isValid = false
|
||||
alertMessage += `
|
||||
<strong>Win Type Error:</strong> Win type should be <strong>${winTypeShouldBe}</strong>.<br>
|
||||
<ul>
|
||||
<li>Decisions are wins with a score difference less than 8.</li>
|
||||
<li>Majors are wins with a score difference between 8 and 14.</li>
|
||||
<li>Tech Falls are wins with a score difference of 15 or more.</li>
|
||||
</ul>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a winner is selected
|
||||
if (!winner) {
|
||||
isValid = false
|
||||
alertMessage += "<strong>Error:</strong> Please select a winner.<br>"
|
||||
}
|
||||
|
||||
// Display validation messages if any
|
||||
if (alertMessage) {
|
||||
this.validationAlertsTarget.innerHTML = alertMessage
|
||||
this.validationAlertsTarget.style.display = "block"
|
||||
this.validationAlertsTarget.classList.add("alert", "alert-danger", "p-3")
|
||||
}
|
||||
|
||||
// Enable/disable submit button based on validation result
|
||||
this.submitButtonTarget.disabled = !isValid
|
||||
|
||||
// Return validation result for potential use elsewhere
|
||||
return isValid
|
||||
}
|
||||
|
||||
createTextInput(id, placeholder, label) {
|
||||
const container = document.createElement("div")
|
||||
container.classList.add("form-group", "mb-2")
|
||||
|
||||
const inputLabel = document.createElement("label")
|
||||
inputLabel.innerText = label
|
||||
inputLabel.classList.add("form-label")
|
||||
inputLabel.setAttribute("for", id)
|
||||
|
||||
const input = document.createElement("input")
|
||||
input.type = "text"
|
||||
input.id = id
|
||||
input.placeholder = placeholder
|
||||
input.classList.add("form-control")
|
||||
input.style.width = "100%"
|
||||
input.style.maxWidth = "400px"
|
||||
|
||||
container.appendChild(inputLabel)
|
||||
container.appendChild(input)
|
||||
return container
|
||||
}
|
||||
|
||||
confirmWinner(event) {
|
||||
const winnerSelect = this.winnerSelectTarget;
|
||||
const selectedOption = winnerSelect.options[winnerSelect.selectedIndex];
|
||||
|
||||
if (!confirm('Is the name of the winner ' + selectedOption.text + '?')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
134
app/assets/javascripts/controllers/match_spectate_controller.js
Normal file
134
app/assets/javascripts/controllers/match_spectate_controller.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Stats", "w2Stats", "winner", "winType",
|
||||
"score", "finished", "statusIndicator"
|
||||
]
|
||||
|
||||
static values = {
|
||||
matchId: Number
|
||||
}
|
||||
|
||||
connect() {
|
||||
console.log("Match spectate controller connected")
|
||||
|
||||
// Setup ActionCable connection if match ID is available
|
||||
if (this.matchIdValue) {
|
||||
this.setupSubscription(this.matchIdValue)
|
||||
} else {
|
||||
console.warn("No match ID provided for spectate controller")
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.cleanupSubscription()
|
||||
}
|
||||
|
||||
// Clean up the existing subscription
|
||||
cleanupSubscription() {
|
||||
if (this.matchSubscription) {
|
||||
console.log('[Spectator AC Cleanup] Unsubscribing...')
|
||||
this.matchSubscription.unsubscribe()
|
||||
this.matchSubscription = null
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the Action Cable subscription for a given matchId
|
||||
setupSubscription(matchId) {
|
||||
this.cleanupSubscription() // Ensure clean state
|
||||
console.log(`[Spectator AC Setup] Attempting subscription for match ID: ${matchId}`)
|
||||
|
||||
if (typeof App === 'undefined' || typeof App.cable === 'undefined') {
|
||||
console.error("[Spectator AC Setup] Action Cable consumer not found.")
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Error: AC Not Loaded"
|
||||
this.statusIndicatorTarget.classList.remove('text-dark', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger', 'text-danger')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set initial connecting state for indicator
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
|
||||
}
|
||||
|
||||
// Assign to the instance property
|
||||
this.matchSubscription = App.cable.subscriptions.create(
|
||||
{ channel: "MatchChannel", match_id: matchId },
|
||||
{
|
||||
initialized: () => {
|
||||
console.log(`[Spectator AC Callback] Initialized: ${matchId}`)
|
||||
// Set connecting state again in case of retry
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connecting to backend for live updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success')
|
||||
this.statusIndicatorTarget.classList.add('alert-secondary', 'text-dark')
|
||||
}
|
||||
},
|
||||
connected: () => {
|
||||
console.log(`[Spectator AC Callback] CONNECTED: ${matchId}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connected to backend for live updates..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-danger', 'alert-secondary', 'text-danger', 'text-dark')
|
||||
this.statusIndicatorTarget.classList.add('alert-success')
|
||||
}
|
||||
},
|
||||
disconnected: () => {
|
||||
console.log(`[Spectator AC Callback] Disconnected: ${matchId}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Disconnected from backend for live updates. Retrying..."
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
},
|
||||
rejected: () => {
|
||||
console.error(`[Spectator AC Callback] REJECTED: ${matchId}`)
|
||||
if (this.hasStatusIndicatorTarget) {
|
||||
this.statusIndicatorTarget.textContent = "Connection to backend rejected"
|
||||
this.statusIndicatorTarget.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark')
|
||||
this.statusIndicatorTarget.classList.add('alert-danger')
|
||||
}
|
||||
this.matchSubscription = null
|
||||
},
|
||||
received: (data) => {
|
||||
console.log("[Spectator AC Callback] Received:", data)
|
||||
this.updateDisplayElements(data)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Update UI elements with received data
|
||||
updateDisplayElements(data) {
|
||||
// Update display elements if they exist and data is provided
|
||||
if (data.w1_stat !== undefined && this.hasW1StatsTarget) {
|
||||
this.w1StatsTarget.textContent = data.w1_stat
|
||||
}
|
||||
|
||||
if (data.w2_stat !== undefined && this.hasW2StatsTarget) {
|
||||
this.w2StatsTarget.textContent = data.w2_stat
|
||||
}
|
||||
|
||||
if (data.score !== undefined && this.hasScoreTarget) {
|
||||
this.scoreTarget.textContent = data.score || '-'
|
||||
}
|
||||
|
||||
if (data.win_type !== undefined && this.hasWinTypeTarget) {
|
||||
this.winTypeTarget.textContent = data.win_type || '-'
|
||||
}
|
||||
|
||||
if (data.winner_name !== undefined && this.hasWinnerTarget) {
|
||||
this.winnerTarget.textContent = data.winner_name || (data.winner_id ? `ID: ${data.winner_id}` : '-')
|
||||
} else if (data.winner_id !== undefined && this.hasWinnerTarget) {
|
||||
this.winnerTarget.textContent = data.winner_id ? `ID: ${data.winner_id}` : '-'
|
||||
}
|
||||
|
||||
if (data.finished !== undefined && this.hasFinishedTarget) {
|
||||
this.finishedTarget.textContent = data.finished ? 'Yes' : 'No'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [
|
||||
"w1Takedown", "w1Escape", "w1Reversal", "w1Nf2", "w1Nf3", "w1Nf4", "w1Nf5", "w1Penalty", "w1Penalty2",
|
||||
"w1Top", "w1Bottom", "w1Neutral", "w1Defer", "w1Stalling", "w1Caution", "w1ColorSelect",
|
||||
"w2Takedown", "w2Escape", "w2Reversal", "w2Nf2", "w2Nf3", "w2Nf4", "w2Nf5", "w2Penalty", "w2Penalty2",
|
||||
"w2Top", "w2Bottom", "w2Neutral", "w2Defer", "w2Stalling", "w2Caution", "w2ColorSelect"
|
||||
]
|
||||
|
||||
connect() {
|
||||
console.log("Wrestler color controller connected")
|
||||
this.initializeColors()
|
||||
}
|
||||
|
||||
initializeColors() {
|
||||
// Set initial colors based on select values
|
||||
this.changeW1Color({ preventRecursion: true })
|
||||
}
|
||||
|
||||
changeW1Color(options = {}) {
|
||||
const color = this.w1ColorSelectTarget.value
|
||||
this.setElementsColor("w1", color)
|
||||
|
||||
// Update w2 color to the opposite color unless we're already in a recursive call
|
||||
if (!options.preventRecursion) {
|
||||
const oppositeColor = color === "green" ? "red" : "green"
|
||||
this.w2ColorSelectTarget.value = oppositeColor
|
||||
this.setElementsColor("w2", oppositeColor)
|
||||
}
|
||||
}
|
||||
|
||||
changeW2Color(options = {}) {
|
||||
const color = this.w2ColorSelectTarget.value
|
||||
this.setElementsColor("w2", color)
|
||||
|
||||
// Update w1 color to the opposite color unless we're already in a recursive call
|
||||
if (!options.preventRecursion) {
|
||||
const oppositeColor = color === "green" ? "red" : "green"
|
||||
this.w1ColorSelectTarget.value = oppositeColor
|
||||
this.setElementsColor("w1", oppositeColor)
|
||||
}
|
||||
}
|
||||
|
||||
setElementsColor(wrestler, color) {
|
||||
// Define which targets to update for each wrestler
|
||||
const targetSuffixes = [
|
||||
"Takedown", "Escape", "Reversal", "Nf2", "Nf3", "Nf4", "Nf5", "Penalty", "Penalty2",
|
||||
"Top", "Bottom", "Neutral", "Defer", "Stalling", "Caution"
|
||||
]
|
||||
|
||||
// For each target type, update the class
|
||||
targetSuffixes.forEach(suffix => {
|
||||
const targetName = `${wrestler}${suffix}Target`
|
||||
if (this[targetName]) {
|
||||
// Remove existing color classes
|
||||
this[targetName].classList.remove("btn-success", "btn-danger")
|
||||
|
||||
// Add new color class
|
||||
if (color === "green") {
|
||||
this[targetName].classList.add("btn-success")
|
||||
} else if (color === "red") {
|
||||
this[targetName].classList.add("btn-danger")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user