1
0
mirror of https://github.com/jcwimer/wrestlingApp synced 2026-03-25 01:14:43 +00:00

Using action cable to send stats updates to the client and made a spectate page.

This commit is contained in:
2025-04-21 17:12:54 -04:00
parent 44fb5388b4
commit 3e4317dbc5
15 changed files with 565 additions and 88 deletions

View File

@@ -18,5 +18,15 @@
//= require jquery.dataTables.min.js
//= require turbolinks
//
//= require actioncable
//= require_self
//= require_tree .
// Create the Action Cable consumer instance
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,63 @@
class MatchChannel < ApplicationCable::Channel
def subscribed
@match = Match.find_by(id: params[:match_id])
Rails.logger.info "[MatchChannel] Client subscribed with match_id: #{params[:match_id]}. Match found: #{@match.present?}"
if @match
stream_for @match
else
Rails.logger.warn "[MatchChannel] Match not found for ID: #{params[:match_id]}. Subscription may fail."
# You might want to reject the subscription if the match isn't found
# reject
end
end
def unsubscribed
Rails.logger.info "[MatchChannel] Client unsubscribed for match #{@match&.id}"
end
# Called when client sends data with action: 'send_stat'
def send_stat(data)
# Explicit check for @match at the start
unless @match
Rails.logger.error "[MatchChannel] Error: send_stat called but @match is nil. Client params on sub: #{params[:match_id]}"
return # Stop if no match context
end
Rails.logger.info "[MatchChannel] Received send_stat for match #{@match.id} with data: #{data.inspect}"
# Prepare attributes to update
attributes_to_update = {}
attributes_to_update[:w1_stat] = data['new_w1_stat'] if data.key?('new_w1_stat')
attributes_to_update[:w2_stat] = data['new_w2_stat'] if data.key?('new_w2_stat')
if attributes_to_update.present?
# Persist the changes to the database
# Note: Consider background job or throttling for very high frequency updates
begin
if @match.update(attributes_to_update)
Rails.logger.info "[MatchChannel] Updated match #{@match.id} stats in DB: #{attributes_to_update.keys.join(', ')}"
# Prepare payload for broadcast (using potentially updated values from @match)
payload = {
w1_stat: @match.w1_stat,
w2_stat: @match.w2_stat
}.compact
if payload.present?
Rails.logger.info "[MatchChannel] Broadcasting DB-persisted stats to match #{@match.id} with payload: #{payload.inspect}"
MatchChannel.broadcast_to(@match, payload)
else
Rails.logger.info "[MatchChannel] Payload empty after DB update for match #{@match.id}, not broadcasting."
end
else
Rails.logger.error "[MatchChannel] Failed to update match #{@match.id} stats in DB: #{@match.errors.full_messages.join(', ')}"
end
rescue => e
Rails.logger.error "[MatchChannel] Exception during match update for #{@match.id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
else
Rails.logger.info "[MatchChannel] No new stat data provided in send_stat for match #{@match.id}, not updating DB or broadcasting."
end
end
end

View File

@@ -1,5 +1,5 @@
class MatchesController < ApplicationController
before_action :set_match, only: [:show, :edit, :update, :stat]
before_action :set_match, only: [:show, :edit, :update, :stat, :spectate]
before_action :check_access, only: [:edit,:update, :stat]
# GET /matches/1
@@ -54,12 +54,42 @@ class MatchesController < ApplicationController
session[:error_return_path] = "/matches/#{@match.id}/stat"
end
# GET /matches/:id/spectate
def spectate
# Similar to stat, but potentially simplified for read-only view
# We mainly need @match for the view to get the ID
# and maybe initial wrestler names/schools
if @match
@wrestler1_name = @match.w1 ? @match.wrestler1.name : "Not assigned"
@wrestler1_school_name = @match.w1 ? @match.wrestler1.school.name : "N/A"
@wrestler2_name = @match.w2 ? @match.wrestler2.name : "Not assigned"
@wrestler2_school_name = @match.w2 ? @match.wrestler2.school.name : "N/A"
@tournament = @match.tournament
else
# Handle case where match isn't found, perhaps redirect or render error
redirect_to root_path, alert: "Match not found."
end
end
# PATCH/PUT /matches/1
# PATCH/PUT /matches/1.json
def update
respond_to do |format|
if @match.update(match_params)
# Broadcast the update
MatchChannel.broadcast_to(
@match,
{
w1_stat: @match.w1_stat,
w2_stat: @match.w2_stat,
score: @match.score,
win_type: @match.win_type,
winner_id: @match.winner_id,
winner_name: @match.winner&.name,
finished: @match.finished
}
)
if session[:return_path]
sanitized_return_path = sanitize_return_path(session[:return_path])
format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' }

View File

@@ -2,6 +2,7 @@ class Match < ApplicationRecord
belongs_to :tournament, touch: true
belongs_to :weight, touch: true
belongs_to :mat, touch: true, optional: true
belongs_to :winner, class_name: 'Wrestler', foreign_key: 'winner_id', optional: true
has_many :wrestlers, :through => :weight
has_many :schools, :through => :wrestlers
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation

View File

@@ -3,6 +3,7 @@
<% if params[:print] %>
<head>
<%= csrf_meta_tags %>
<%= action_cable_meta_tag %>
<title>WrestlingDev</title>
<%= stylesheet_link_tag "application", media: "all",
"data-turbolinks-track" => true %>
@@ -16,6 +17,7 @@
<% else %>
<head>
<title>WrestlingDev</title>
<%= action_cable_meta_tag %>
<meta name="viewport" content="width=device-width, initial-scale=1">
<% if Rails.env.production? %>
<%= render 'layouts/analytics' %>

View File

@@ -10,6 +10,7 @@
</ul>
</div>
<% end %>
<div id="cable-status-indicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
<% if @show_next_bout_button && @next_match %>
<%= link_to "Skip to Next Match for Mat #{@mat.name}", mat_path(@mat, bout_number: @next_match.bout_number), class: "btn btn-primary" %>
@@ -165,13 +166,11 @@
<%= render 'matches/matchstats_color_change' %>
<script>
// Get variables
// ############### CORE STATE & HELPERS (Define First) #############
var tournament = <%= @match.tournament.id %>;
var bout = <%= @match.bout_number %>;
var match_finsihed = "<%= @match.finished %>";
// ############### STATS
// Create person object
function Person(stats, name) {
this.name = name;
this.stats = stats;
@@ -181,25 +180,17 @@ function Person(stats, name) {
"blood": { time: 0, startTime: null, interval: null },
};
}
// Declare variables
var w1 = new Person("", "w1");
var w2 = new Person("", "w2");
updateJsValues();
// Generate unique localStorage key
function generateKey(wrestler_name) {
return `${wrestler_name}-${tournament}-${bout}`;
}
// Load values from localStorage
function loadFromLocalStorage(wrestler_name) {
const key = generateKey(wrestler_name);
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
// Save values to localStorage
function saveToLocalStorage(person) {
const key = generateKey(person.name);
const data = {
@@ -209,77 +200,74 @@ function saveToLocalStorage(person) {
};
localStorage.setItem(key, JSON.stringify(data));
}
// Update HTML values
function updateHtmlValues() {
document.getElementById("match_w1_stat").value = w1.stats;
document.getElementById("match_w2_stat").value = w2.stats;
}
// Update JS object values from HTML
function updateJsValues() {
w1.stats = document.getElementById("match_w1_stat").value;
w2.stats = document.getElementById("match_w2_stat").value;
}
// Update stats and persist to localStorage
function updateStats(wrestler, text) {
updateJsValues();
wrestler.stats += text + " ";
wrestler.updated_at = new Date().toISOString(); // Update timestamp
updateHtmlValues();
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function handleTextAreaInput(textAreaElement, wrestler) {
const newValue = textAreaElement.value;
console.log(`Text area input detected for ${wrestler.name}:`, newValue.substring(0, 50) + "..."); // Log input
// Update the internal JS object
wrestler.stats = newValue;
wrestler.updated_at = new Date().toISOString();
// Save to localStorage
if (wrestler === w1) {
saveToLocalStorage(w1);
} else if (wrestler === w2) {
saveToLocalStorage(w2);
}
}
saveToLocalStorage(wrestler);
// Initialize data on page load
function initializeTimers(wrestler) {
// Iterate over each timer in the wrestler object
Object.keys(wrestler.timers).forEach((timerKey) => {
const savedData = loadFromLocalStorage(wrestler.name);
if (savedData && savedData.timers && savedData.timers[timerKey]) {
wrestler.timers[timerKey].time = savedData.timers[timerKey].time || 0;
updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time);
// Send the update via Action Cable if subscribed
if (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);
matchSubscription.perform('send_stat', payload);
}
}
}
function updateStats(wrestler, text) {
if (!wrestler) { console.error("updateStats called with undefined wrestler"); return; }
wrestler.stats += text + " ";
wrestler.updated_at = new Date().toISOString();
updateHtmlValues();
saveToLocalStorage(wrestler);
// Reference the global matchSubscription
if (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);
matchSubscription.perform('send_stat', payload);
}
} else {
console.warn('[ActionCable] updateStats called but matchSubscription is null.');
}
}
function initialize() {
const localW1 = loadFromLocalStorage("w1");
const localW2 = loadFromLocalStorage("w2");
var debouncedW1Handler = debounce((el) => { if(typeof w1 !== 'undefined') handleTextAreaInput(el, w1); }, 400);
var debouncedW2Handler = debounce((el) => { if(typeof w2 !== 'undefined') handleTextAreaInput(el, w2); }, 400);
if (localW1) {
w1.stats = localW1.stats || "";
w1.updated_at = localW1.updated_at || null;
w1.timers = localW1.timers || w1.timers; // Load timer data
// set localStorage values to html
updateHtmlValues();
}
if (localW2) {
w2.stats = localW2.stats || "";
w2.updated_at = localW2.updated_at || null;
w2.timers = localW2.timers || w2.timers; // Load timer data
// set localStorage values to html
updateHtmlValues();
}
initializeTimers(w1);
initializeTimers(w2);
updateJsValues()
}
document.addEventListener("DOMContentLoaded", function () {
initialize();
});
// ############### Blood and Injury time timers
// Timer storage and interval references
// Start a timer for a wrestler
function startTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
if (timer.interval) return; // Prevent multiple intervals
@@ -289,8 +277,6 @@ function startTimer(wrestler, timerKey) {
updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds); // Show total time
}, 1000);
}
// Stop a timer for a wrestler
function stopTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
if (!timer.interval || !timer.startTime) return; // Timer not running
@@ -305,8 +291,6 @@ function stopTimer(wrestler, timerKey) {
updateTimerDisplay(wrestler, timerKey, timer.time); // Update final display
updateStatsBox(wrestler, timerKey, elapsedSeconds); // Update wrestler stats
}
// Reset a timer for a wrestler
function resetTimer(wrestler, timerKey) {
const timer = wrestler.timers[timerKey];
stopTimer(wrestler, timerKey); // Stop if running
@@ -314,8 +298,6 @@ function resetTimer(wrestler, timerKey) {
updateTimerDisplay(wrestler, timerKey, 0); // Update display
saveToLocalStorage(wrestler); // Save wrestler data
}
// Update the timer display
function updateTimerDisplay(wrestler, timerKey, totalTime) {
const elementId = `${wrestler.name}-${timerKey}-time`; // Construct element ID
const element = document.getElementById(elementId);
@@ -323,11 +305,146 @@ function updateTimerDisplay(wrestler, timerKey, totalTime) {
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`;
}
}
// Update wrestler stats box with elapsed timer information
function updateStatsBox(wrestler, timerKey, elapsedSeconds) {
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time";
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`;
updateStats(wrestler, `${timerType}: ${formattedTime}`);
}
function initializeTimers(wrestler) {
// Implementation of initializeTimers method
}
function initializeFromLocalStorage() {
console.log("[Init] Initializing from local storage...");
// ... existing initializeFromLocalStorage logic ...
}
// ############### ACTION CABLE LIFECYCLE (Define Before Listeners) #############
var matchSubscription = null; // Use var for safety with Turbolinks re-evaluation
function cleanupSubscription() {
if (matchSubscription) {
console.log('[AC Cleanup] Unsubscribing...');
matchSubscription.unsubscribe();
matchSubscription = null;
}
}
function setupSubscription(matchId) {
cleanupSubscription(); // Ensure clean state
console.log(`[Stats AC Setup] Attempting subscription for match ID: ${matchId}`);
const statusIndicator = document.getElementById("cable-status-indicator"); // Get indicator
if (typeof App === 'undefined' || typeof App.cable === 'undefined') {
console.error("[Stats AC Setup] Action Cable consumer not found.");
if(statusIndicator) {
statusIndicator.textContent = "Error: AC Not Loaded";
statusIndicator.classList.remove('text-dark', 'text-success');
statusIndicator.classList.add('alert-danger', 'text-danger');
}
return;
}
// Set initial connecting state
if(statusIndicator) {
statusIndicator.textContent = "Connecting to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success');
statusIndicator.classList.add('alert-secondary', 'text-dark');
}
// Assign to the global var
matchSubscription = App.cable.subscriptions.create(
{ channel: "MatchChannel", match_id: matchId },
{
initialized() {
console.log(`[Stats AC Callback] Initialized: ${matchId}`);
// Set connecting state again in case of retry
if(statusIndicator) {
statusIndicator.textContent = "Connecting to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success');
statusIndicator.classList.add('alert-secondary', 'text-dark');
}
},
connected() {
console.log(`[Stats AC Callback] CONNECTED: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Connected to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-secondary', 'text-danger', 'text-dark');
statusIndicator.classList.add('alert-success');
}
},
disconnected() {
console.log(`[Stats AC Callback] Disconnected: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Disconnected from backend for live updates. Retrying...";
statusIndicator.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark');
statusIndicator.classList.add('alert-danger');
}
},
rejected() {
console.error(`[Stats AC Callback] REJECTED: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Connection to backend rejected";
statusIndicator.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark');
statusIndicator.classList.add('alert-danger');
}
matchSubscription = null;
},
received(data) {
console.log("[AC Callback] Received:", data);
const currentW1TextArea = document.getElementById("match_w1_stat");
const currentW2TextArea = document.getElementById("match_w2_stat");
if (data.w1_stat !== undefined && currentW1TextArea) {
currentW1TextArea.value = data.w1_stat;
if(w1) w1.stats = data.w1_stat;
}
if (data.w2_stat !== undefined && currentW2TextArea) {
currentW2TextArea.value = data.w2_stat;
if(w2) w2.stats = data.w2_stat;
}
}
}
);
// Re-attach listeners AFTER subscription is attempted
const w1TextArea = document.getElementById("match_w1_stat");
const w2TextArea = document.getElementById("match_w2_stat");
if (w1TextArea) {
w1TextArea.addEventListener('input', (event) => { debouncedW1Handler(event.target); });
} else { console.warn('[AC Setup] w1StatsTextArea not found for listener'); }
if (w2TextArea) {
w2TextArea.addEventListener('input', (event) => { debouncedW2Handler(event.target); });
} else { console.warn('[AC Setup] w2StatsTextArea not found for listener'); }
}
// ############### EVENT LISTENERS (Define Last) #############
document.addEventListener("turbolinks:load", () => {
console.log("Stats Event: turbolinks:load fired.");
// --- Check if we are actually on the match stats page ---
const statsElementCheck = document.getElementById('match_w1_stat'); // Check for stats textarea
if (!statsElementCheck) {
console.log("Stats Event: Not on match stats page, skipping AC setup.");
cleanupSubscription(); // Cleanup just in case
return;
}
// --- End Check ---
initializeFromLocalStorage(); // Load state first
const matchId = <%= @match.id %>;
if (matchId) {
setupSubscription(matchId); // Then setup ActionCable
} else {
console.warn("Stats Event: turbolinks:load - Could not determine match ID");
}
});
document.addEventListener("turbolinks:before-cache", () => {
console.log("Event: turbolinks:before-cache fired. Cleaning up subscription.");
cleanupSubscription();
});
// Optional: Cleanup on full page unload too
window.addEventListener('beforeunload', cleanupSubscription);
</script>

View File

@@ -0,0 +1,222 @@
<h1>Spectating Match: Bout <%= @match.bout_number %></h1>
<h2><%= @match.weight.max %> lbs</h2>
<h3><%= @tournament.name %></h3>
<div id="cable-status-indicator" class="alert alert-secondary" style="padding: 5px; margin-bottom: 10px; border-radius: 4px;"></div>
<div class="match-details">
<div class="wrestler-info wrestler1">
<h4><%= @wrestler1_name %> (<%= @wrestler1_school_name %>)</h4>
<div class="stats">
<strong>Stats:</strong>
<pre id="w1-stats-display"><%= @match.w1_stat %></pre>
</div>
</div>
<div class="wrestler-info wrestler2">
<h4><%= @wrestler2_name %> (<%= @wrestler2_school_name %>)</h4>
<div class="stats">
<strong>Stats:</strong>
<pre id="w2-stats-display"><%= @match.w2_stat %></pre>
</div>
</div>
<div class="match-result">
<h4>Result</h4>
<p><strong>Winner:</strong> <span id="winner-display"><%= @match.winner_id ? @match.winner.name : '-' %></span></p>
<p><strong>Win Type:</strong> <span id="win-type-display"><%= @match.win_type || '-' %></span></p>
<p><strong>Score:</strong> <span id="score-display"><%= @match.score || '-' %></span></p>
<p><strong>Finished:</strong> <span id="finished-display"><%= @match.finished ? 'Yes' : 'No' %></span></p>
</div>
</div>
<style>
.match-details {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.wrestler-info {
border: 1px solid #ccc;
padding: 15px;
width: 40%;
}
.wrestler-info pre {
background-color: #f8f8f8;
border: 1px solid #eee;
padding: 10px;
white-space: pre-wrap; /* Allow text wrapping */
word-wrap: break-word; /* Break long words */
max-height: 300px; /* Optional: limit height */
overflow-y: auto; /* Optional: add scroll if needed */
}
.match-result {
border: 1px solid #ccc;
padding: 15px;
width: 15%;
}
.match-result span {
font-weight: normal;
}
/* REMOVE Status Indicator Background Styles
#cable-status-indicator {
transition: background-color 0.5s ease, color 0.5s ease;
}
#cable-status-indicator.status-connecting {
background-color: #ffc107;
color: #333;
}
#cable-status-indicator.status-connected {
background-color: #28a745;
color: white;
}
#cable-status-indicator.status-disconnected {
background-color: #dc3545;
color: white;
}
*/
</style>
<script>
// ############### ACTION CABLE LIFECYCLE & SETUP #############
var matchSubscription = null; // Use var for Turbolinks compatibility
// Function to tear down the existing subscription
function cleanupSubscription() {
if (matchSubscription) {
console.log('[Spectator AC Cleanup] Unsubscribing...');
matchSubscription.unsubscribe();
matchSubscription = null;
}
}
// Function to set up the Action Cable subscription for a given matchId
function setupSubscription(matchId) {
cleanupSubscription(); // Ensure clean state
console.log(`[Spectator AC Setup] Attempting subscription for match ID: ${matchId}`);
const statusIndicator = document.getElementById("cable-status-indicator"); // Get indicator
if (typeof App === 'undefined' || typeof App.cable === 'undefined') {
console.error("[Spectator AC Setup] Action Cable consumer not found.");
if(statusIndicator) {
statusIndicator.textContent = "Error: AC Not Loaded";
statusIndicator.classList.remove('text-dark', 'text-success');
statusIndicator.classList.add('alert-danger', 'text-danger'); // Use alert-danger for error state too
}
return;
}
// Set initial connecting state for indicator
if(statusIndicator) {
statusIndicator.textContent = "Connecting to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success');
statusIndicator.classList.add('alert-secondary', 'text-dark'); // Keep grey, dark text
}
// Get references to display elements (needed inside received)
const w1StatsDisplay = document.getElementById("w1-stats-display");
const w2StatsDisplay = document.getElementById("w2-stats-display");
const winnerDisplay = document.getElementById("winner-display");
const winTypeDisplay = document.getElementById("win-type-display");
const scoreDisplay = document.getElementById("score-display");
const finishedDisplay = document.getElementById("finished-display");
// Assign to the global var
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(statusIndicator) {
statusIndicator.textContent = "Connecting to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-success', 'text-danger', 'text-success');
statusIndicator.classList.add('alert-secondary', 'text-dark');
}
},
connected() {
console.log(`[Spectator AC Callback] CONNECTED: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Connected to backend for live updates...";
statusIndicator.classList.remove('alert-danger', 'alert-secondary', 'text-danger', 'text-dark');
statusIndicator.classList.add('alert-success'); // Use alert-success for connected
}
},
disconnected() {
console.log(`[Spectator AC Callback] Disconnected: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Disconnected from backend for live updates. Retrying...";
statusIndicator.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark');
statusIndicator.classList.add('alert-danger'); // Use alert-danger for disconnected
}
},
rejected() {
console.error(`[Spectator AC Callback] REJECTED: ${matchId}`);
if(statusIndicator) {
statusIndicator.textContent = "Connection to backend rejected";
statusIndicator.classList.remove('alert-success', 'alert-secondary', 'text-success', 'text-dark');
statusIndicator.classList.add('alert-danger'); // Use alert-danger for rejected
}
matchSubscription = null;
},
received(data) {
console.log("[Spectator AC Callback] Received:", data);
// Update display elements if they exist
if (data.w1_stat !== undefined && w1StatsDisplay) {
w1StatsDisplay.textContent = data.w1_stat;
}
if (data.w2_stat !== undefined && w2StatsDisplay) {
w2StatsDisplay.textContent = data.w2_stat;
}
if (data.score !== undefined && scoreDisplay) {
scoreDisplay.textContent = data.score || '-';
}
if (data.win_type !== undefined && winTypeDisplay) {
winTypeDisplay.textContent = data.win_type || '-';
}
if (data.winner_name !== undefined && winnerDisplay) {
winnerDisplay.textContent = data.winner_name || (data.winner_id ? `ID: ${data.winner_id}` : '-');
} else if (data.winner_id !== undefined && winnerDisplay) {
winnerDisplay.textContent = data.winner_id ? `ID: ${data.winner_id}` : '-';
}
if (data.finished !== undefined && finishedDisplay) {
finishedDisplay.textContent = data.finished ? 'Yes' : 'No';
}
}
}
);
}
// ############### EVENT LISTENERS (Define Last) #############
document.addEventListener("turbolinks:load", () => {
console.log("Spectator Event: turbolinks:load fired.");
// --- Check if we are actually on the spectator page ---
const spectatorElementCheck = document.getElementById('w1-stats-display');
if (!spectatorElementCheck) {
console.log("Spectator Event: Not on spectator page, skipping AC setup.");
// Ensure any potentially lingering subscription is cleaned up just in case
cleanupSubscription();
return;
}
// --- End Check ---
const matchId = <%= @match.id %>; // Get match ID from ERB
if (matchId) {
setupSubscription(matchId);
} else {
console.warn("Spectator Event: turbolinks:load - Could not determine match ID");
}
});
document.addEventListener("turbolinks:before-cache", () => {
console.log("Spectator Event: turbolinks:before-cache fired. Cleaning up subscription.");
cleanupSubscription();
});
// Optional: Cleanup on full page unload too
window.addEventListener('beforeunload', cleanupSubscription);
</script>

View File

@@ -2,7 +2,7 @@
<div class="round">
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<div class="bout-number"><p><%= match.bout_number %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<div class="bout-number"><p><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %></p><p><%= @winner_place %> Place Winner</p></div>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
</div>

View File

@@ -65,6 +65,17 @@ table.smallText tr td { font-size: 10px; }
text-align: center;
/*padding-top: 15px;*/
}
/* Style links within bout-number like default links */
.bout-number a {
color: #007bff; /* Or your preferred link color */
text-decoration: underline;
}
.bout-number a:hover {
color: #0056b3; /* Darker color on hover */
text-decoration: underline;
}
.bracket-winner {
border-bottom:1px solid #ddd;
padding: 2px;

View File

@@ -2,7 +2,7 @@
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
<div class="game">
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %>&nbsp;</div>
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %>&nbsp;</div>
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
</div>
<% end %>