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:
@@ -18,5 +18,15 @@
|
|||||||
//= require jquery.dataTables.min.js
|
//= require jquery.dataTables.min.js
|
||||||
//= require turbolinks
|
//= require turbolinks
|
||||||
//
|
//
|
||||||
|
//= require actioncable
|
||||||
|
//= require_self
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
||||||
|
// Create the Action Cable consumer instance
|
||||||
|
(function() {
|
||||||
|
this.App || (this.App = {});
|
||||||
|
|
||||||
|
App.cable = ActionCable.createConsumer();
|
||||||
|
|
||||||
|
}).call(this);
|
||||||
|
|
||||||
|
|||||||
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Channel < ActionCable::Channel::Base
|
||||||
|
end
|
||||||
|
end
|
||||||
4
app/channels/application_cable/connection.rb
Normal file
4
app/channels/application_cable/connection.rb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module ApplicationCable
|
||||||
|
class Connection < ActionCable::Connection::Base
|
||||||
|
end
|
||||||
|
end
|
||||||
63
app/channels/match_channel.rb
Normal file
63
app/channels/match_channel.rb
Normal 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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class MatchesController < ApplicationController
|
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]
|
before_action :check_access, only: [:edit,:update, :stat]
|
||||||
|
|
||||||
# GET /matches/1
|
# GET /matches/1
|
||||||
@@ -54,12 +54,42 @@ class MatchesController < ApplicationController
|
|||||||
session[:error_return_path] = "/matches/#{@match.id}/stat"
|
session[:error_return_path] = "/matches/#{@match.id}/stat"
|
||||||
end
|
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
|
||||||
# PATCH/PUT /matches/1.json
|
# PATCH/PUT /matches/1.json
|
||||||
def update
|
def update
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
if @match.update(match_params)
|
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]
|
if session[:return_path]
|
||||||
sanitized_return_path = sanitize_return_path(session[:return_path])
|
sanitized_return_path = sanitize_return_path(session[:return_path])
|
||||||
format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' }
|
format.html { redirect_to sanitized_return_path, notice: 'Match was successfully updated.' }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class Match < ApplicationRecord
|
|||||||
belongs_to :tournament, touch: true
|
belongs_to :tournament, touch: true
|
||||||
belongs_to :weight, touch: true
|
belongs_to :weight, touch: true
|
||||||
belongs_to :mat, touch: true, optional: 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 :wrestlers, :through => :weight
|
||||||
has_many :schools, :through => :wrestlers
|
has_many :schools, :through => :wrestlers
|
||||||
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
|
validate :score_validation, :win_type_validation, :bracket_position_validation, :overtime_type_validation
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<% if params[:print] %>
|
<% if params[:print] %>
|
||||||
<head>
|
<head>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
|
<%= action_cable_meta_tag %>
|
||||||
<title>WrestlingDev</title>
|
<title>WrestlingDev</title>
|
||||||
<%= stylesheet_link_tag "application", media: "all",
|
<%= stylesheet_link_tag "application", media: "all",
|
||||||
"data-turbolinks-track" => true %>
|
"data-turbolinks-track" => true %>
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<head>
|
<head>
|
||||||
<title>WrestlingDev</title>
|
<title>WrestlingDev</title>
|
||||||
|
<%= action_cable_meta_tag %>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<% if Rails.env.production? %>
|
<% if Rails.env.production? %>
|
||||||
<%= render 'layouts/analytics' %>
|
<%= render 'layouts/analytics' %>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% 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>
|
<h4>Bout <strong><%= @match.bout_number %></strong></h4>
|
||||||
<% if @show_next_bout_button && @next_match %>
|
<% 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" %>
|
<%= 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' %>
|
<%= render 'matches/matchstats_color_change' %>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Get variables
|
// ############### CORE STATE & HELPERS (Define First) #############
|
||||||
var tournament = <%= @match.tournament.id %>;
|
var tournament = <%= @match.tournament.id %>;
|
||||||
var bout = <%= @match.bout_number %>;
|
var bout = <%= @match.bout_number %>;
|
||||||
var match_finsihed = "<%= @match.finished %>";
|
var match_finsihed = "<%= @match.finished %>";
|
||||||
|
|
||||||
// ############### STATS
|
|
||||||
// Create person object
|
|
||||||
function Person(stats, name) {
|
function Person(stats, name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
@@ -181,25 +180,17 @@ function Person(stats, name) {
|
|||||||
"blood": { time: 0, startTime: null, interval: null },
|
"blood": { time: 0, startTime: null, interval: null },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declare variables
|
|
||||||
var w1 = new Person("", "w1");
|
var w1 = new Person("", "w1");
|
||||||
var w2 = new Person("", "w2");
|
var w2 = new Person("", "w2");
|
||||||
updateJsValues();
|
|
||||||
|
|
||||||
// Generate unique localStorage key
|
|
||||||
function generateKey(wrestler_name) {
|
function generateKey(wrestler_name) {
|
||||||
return `${wrestler_name}-${tournament}-${bout}`;
|
return `${wrestler_name}-${tournament}-${bout}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load values from localStorage
|
|
||||||
function loadFromLocalStorage(wrestler_name) {
|
function loadFromLocalStorage(wrestler_name) {
|
||||||
const key = generateKey(wrestler_name);
|
const key = generateKey(wrestler_name);
|
||||||
const data = localStorage.getItem(key);
|
const data = localStorage.getItem(key);
|
||||||
return data ? JSON.parse(data) : null;
|
return data ? JSON.parse(data) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save values to localStorage
|
|
||||||
function saveToLocalStorage(person) {
|
function saveToLocalStorage(person) {
|
||||||
const key = generateKey(person.name);
|
const key = generateKey(person.name);
|
||||||
const data = {
|
const data = {
|
||||||
@@ -209,77 +200,74 @@ function saveToLocalStorage(person) {
|
|||||||
};
|
};
|
||||||
localStorage.setItem(key, JSON.stringify(data));
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update HTML values
|
|
||||||
function updateHtmlValues() {
|
function updateHtmlValues() {
|
||||||
document.getElementById("match_w1_stat").value = w1.stats;
|
document.getElementById("match_w1_stat").value = w1.stats;
|
||||||
document.getElementById("match_w2_stat").value = w2.stats;
|
document.getElementById("match_w2_stat").value = w2.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update JS object values from HTML
|
|
||||||
function updateJsValues() {
|
function updateJsValues() {
|
||||||
w1.stats = document.getElementById("match_w1_stat").value;
|
w1.stats = document.getElementById("match_w1_stat").value;
|
||||||
w2.stats = document.getElementById("match_w2_stat").value;
|
w2.stats = document.getElementById("match_w2_stat").value;
|
||||||
}
|
}
|
||||||
|
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 stats and persist to localStorage
|
// Update the internal JS object
|
||||||
function updateStats(wrestler, text) {
|
wrestler.stats = newValue;
|
||||||
updateJsValues();
|
wrestler.updated_at = new Date().toISOString();
|
||||||
wrestler.stats += text + " ";
|
|
||||||
wrestler.updated_at = new Date().toISOString(); // Update timestamp
|
|
||||||
updateHtmlValues();
|
|
||||||
|
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
if (wrestler === w1) {
|
saveToLocalStorage(wrestler);
|
||||||
saveToLocalStorage(w1);
|
|
||||||
} else if (wrestler === w2) {
|
|
||||||
saveToLocalStorage(w2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize data on page load
|
// Send the update via Action Cable if subscribed
|
||||||
function initializeTimers(wrestler) {
|
if (matchSubscription) {
|
||||||
// Iterate over each timer in the wrestler object
|
let payload = {};
|
||||||
Object.keys(wrestler.timers).forEach((timerKey) => {
|
if (wrestler.name === 'w1') {
|
||||||
const savedData = loadFromLocalStorage(wrestler.name);
|
payload.new_w1_stat = wrestler.stats;
|
||||||
if (savedData && savedData.timers && savedData.timers[timerKey]) {
|
} else if (wrestler.name === 'w2') {
|
||||||
wrestler.timers[timerKey].time = savedData.timers[timerKey].time || 0;
|
payload.new_w2_stat = wrestler.stats;
|
||||||
updateTimerDisplay(wrestler, timerKey, wrestler.timers[timerKey].time);
|
|
||||||
}
|
}
|
||||||
});
|
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() {
|
var debouncedW1Handler = debounce((el) => { if(typeof w1 !== 'undefined') handleTextAreaInput(el, w1); }, 400);
|
||||||
const localW1 = loadFromLocalStorage("w1");
|
var debouncedW2Handler = debounce((el) => { if(typeof w2 !== 'undefined') handleTextAreaInput(el, w2); }, 400);
|
||||||
const localW2 = loadFromLocalStorage("w2");
|
|
||||||
|
|
||||||
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) {
|
function startTimer(wrestler, timerKey) {
|
||||||
const timer = wrestler.timers[timerKey];
|
const timer = wrestler.timers[timerKey];
|
||||||
if (timer.interval) return; // Prevent multiple intervals
|
if (timer.interval) return; // Prevent multiple intervals
|
||||||
@@ -289,8 +277,6 @@ function startTimer(wrestler, timerKey) {
|
|||||||
updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds); // Show total time
|
updateTimerDisplay(wrestler, timerKey, timer.time + elapsedSeconds); // Show total time
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop a timer for a wrestler
|
|
||||||
function stopTimer(wrestler, timerKey) {
|
function stopTimer(wrestler, timerKey) {
|
||||||
const timer = wrestler.timers[timerKey];
|
const timer = wrestler.timers[timerKey];
|
||||||
if (!timer.interval || !timer.startTime) return; // Timer not running
|
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
|
updateTimerDisplay(wrestler, timerKey, timer.time); // Update final display
|
||||||
updateStatsBox(wrestler, timerKey, elapsedSeconds); // Update wrestler stats
|
updateStatsBox(wrestler, timerKey, elapsedSeconds); // Update wrestler stats
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset a timer for a wrestler
|
|
||||||
function resetTimer(wrestler, timerKey) {
|
function resetTimer(wrestler, timerKey) {
|
||||||
const timer = wrestler.timers[timerKey];
|
const timer = wrestler.timers[timerKey];
|
||||||
stopTimer(wrestler, timerKey); // Stop if running
|
stopTimer(wrestler, timerKey); // Stop if running
|
||||||
@@ -314,8 +298,6 @@ function resetTimer(wrestler, timerKey) {
|
|||||||
updateTimerDisplay(wrestler, timerKey, 0); // Update display
|
updateTimerDisplay(wrestler, timerKey, 0); // Update display
|
||||||
saveToLocalStorage(wrestler); // Save wrestler data
|
saveToLocalStorage(wrestler); // Save wrestler data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the timer display
|
|
||||||
function updateTimerDisplay(wrestler, timerKey, totalTime) {
|
function updateTimerDisplay(wrestler, timerKey, totalTime) {
|
||||||
const elementId = `${wrestler.name}-${timerKey}-time`; // Construct element ID
|
const elementId = `${wrestler.name}-${timerKey}-time`; // Construct element ID
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
@@ -323,11 +305,146 @@ function updateTimerDisplay(wrestler, timerKey, totalTime) {
|
|||||||
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`;
|
element.innerText = `${Math.floor(totalTime / 60)}m ${totalTime % 60}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update wrestler stats box with elapsed timer information
|
|
||||||
function updateStatsBox(wrestler, timerKey, elapsedSeconds) {
|
function updateStatsBox(wrestler, timerKey, elapsedSeconds) {
|
||||||
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time";
|
const timerType = timerKey.includes("injury") ? "Injury Time" : "Blood Time";
|
||||||
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`;
|
const formattedTime = `${Math.floor(elapsedSeconds / 60)}m ${elapsedSeconds % 60}s`;
|
||||||
updateStats(wrestler, `${timerType}: ${formattedTime}`);
|
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>
|
</script>
|
||||||
|
|||||||
222
app/views/matches/spectate.html.erb
Normal file
222
app/views/matches/spectate.html.erb
Normal 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>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="round">
|
<div class="round">
|
||||||
<div class="game">
|
<div class="game">
|
||||||
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
<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 class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ table.smallText tr td { font-size: 10px; }
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
/*padding-top: 15px;*/
|
/*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 {
|
.bracket-winner {
|
||||||
border-bottom:1px solid #ddd;
|
border-bottom:1px solid #ddd;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
|
<% @round_matches.sort_by{|m| m.bracket_position_number}.each do |match| %>
|
||||||
<div class="game">
|
<div class="game">
|
||||||
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
<div class="game-top "><%= match.w1_bracket_name.html_safe %> <span></span></div>
|
||||||
<div class="bout-number"><%= match.bout_number %> <%= match.bracket_score_string %> </div>
|
<div class="bout-number"><%= link_to match.bout_number, spectate_match_path(match) %> <%= match.bracket_score_string %> </div>
|
||||||
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
<div class="game-bottom "><%= match.w2_bracket_name.html_safe %><span></span></div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -3,16 +3,15 @@
|
|||||||
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
|
# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
|
||||||
# to make the web console appear.
|
# to make the web console appear.
|
||||||
development:
|
development:
|
||||||
adapter: solid_cable
|
adapter: async
|
||||||
database: cable
|
|
||||||
polling_interval: 0.1.seconds
|
|
||||||
message_retention: 1.day
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
adapter: test
|
adapter: test
|
||||||
|
|
||||||
production:
|
production:
|
||||||
adapter: solid_cable
|
adapter: solid_cable
|
||||||
database: cable
|
connects_to:
|
||||||
|
database:
|
||||||
|
writing: cable
|
||||||
polling_interval: 0.1.seconds
|
polling_interval: 0.1.seconds
|
||||||
message_retention: 1.day
|
message_retention: 1.day
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
Wrestling::Application.routes.draw do
|
Wrestling::Application.routes.draw do
|
||||||
|
# Mount Action Cable server
|
||||||
|
mount ActionCable.server => '/cable'
|
||||||
|
|
||||||
resources :mats
|
resources :mats
|
||||||
post "mats/:id/assign_next_match" => "mats#assign_next_match", :as => :assign_next_match
|
post "mats/:id/assign_next_match" => "mats#assign_next_match", :as => :assign_next_match
|
||||||
|
|
||||||
resources :matches
|
resources :matches do
|
||||||
|
member do
|
||||||
|
get :stat
|
||||||
|
get :spectate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Replace devise_for :users with custom routes
|
# Replace devise_for :users with custom routes
|
||||||
get '/login', to: 'sessions#new'
|
get '/login', to: 'sessions#new'
|
||||||
@@ -89,8 +97,6 @@ Wrestling::Application.routes.draw do
|
|||||||
get "/api/index" => "api#index"
|
get "/api/index" => "api#index"
|
||||||
post "/api/tournaments/new" => "newTournament"
|
post "/api/tournaments/new" => "newTournament"
|
||||||
|
|
||||||
get "/matches/:id/stat" => "matches#stat", :as => :stat_match_path
|
|
||||||
|
|
||||||
resources :tournaments do
|
resources :tournaments do
|
||||||
member do
|
member do
|
||||||
post :generate_school_keys
|
post :generate_school_keys
|
||||||
|
|||||||
8
test/channels/match_channel_test.rb
Normal file
8
test/channels/match_channel_test.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class MatchChannelTest < ActionCable::Channel::TestCase
|
||||||
|
# test "subscribes" do
|
||||||
|
# subscribe
|
||||||
|
# assert subscription.confirmed?
|
||||||
|
# end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user