* {
margin: 0;
padding: 0;
box-sizing: border-box;
}body {
font-family: ‘Poppins’, -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f8fafc;
}.competition-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}.loader {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
}.loader.active {
display: block;
}/* Styles communs avec nouveau thème */
.competition-header {
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
color: white;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
box-shadow: 0 4px 16px rgba(31, 41, 55, 0.3);
}.competition-header h1 {
font-size: 1.5rem;
margin: 0;
font-weight: 600;
}.view-toggle {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}.toggle-btn {
padding: 10px 20px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
color: #374151;
}.toggle-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}.toggle-btn.active {
background: #475569;
color: white;
border-color: #475569;
box-shadow: 0 2px 8px rgba(71, 85, 105, 0.3);
}.navigation-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
overflow-x: auto;
padding: 10px 0;
}.tab {
padding: 10px 20px;
background: white;
border: 2px solid #e5e7eb;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
color: #374151;
}.tab:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}.tab.active {
background: #475569;
color: white;
border-color: #475569;
box-shadow: 0 2px 8px rgba(71, 85, 105, 0.2);
}/* Cup/Tournament Styles */
.cup-view .phase-container {
margin-bottom: 40px;
}
.championship-view .phase-container,
.tournament-view .phase-container {
margin-bottom: 40px;
}.phase-header {
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
color: white;
padding: 15px;
border-radius: 5px 5px 0 0;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}.phase-type-badge {
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
text-transform: uppercase;
}/* Header tab buttons */
.header-tab-btn {
padding: 8px 16px !important;
font-size: 13px !important;
font-weight: 500;
transition: all 0.3s ease !important;
}
.header-tab-btn:hover {
background: rgba(255,255,255,0.3) !important;
color: white !important;
transform: translateY(-1px);
}.header-tab-btn.active:hover {
background: rgba(255,255,255,0.25) !important;
}/* Match Cards – Simplified */
.match-card {
background: white;
border: 2px solid #e5e7eb;
border-radius: 10px;
margin-bottom: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
position: relative;
}.match-card:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-color: #475569;
}.match-card.finished {
border-color: #1e40af;
}.match-card.finished::before {
content: ‘✓’;
position: absolute;
top: 8px;
right: 8px;
color: #1e40af;
font-weight: bold;
font-size: 14px;
}.match-card.live {
border-color: #d62243;
animation: pulse-border 2s infinite;
}.match-card.live::before {
content: ‘● LIVE’;
position: absolute;
top: 8px;
right: 8px;
color: #d62243;
font-weight: bold;
font-size: 11px;
animation: pulse 2s infinite;
}@keyframes pulse-border {
0% { border-color: #d62243; }
50% { border-color: #e85a75; }
100% { border-color: #d62243; }
}/* Desktop layout */
@media (min-width: 769px) {
.match-team {
display: flex;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f3f4f6;
transition: all 0.2s;
position: relative;
}.match-team:last-child {
border-bottom: none;
}.match-team.winner {
background: linear-gradient(to right, #dbeafe 0%, #eff6ff 100%);
font-weight: 600;
}.match-team.winner::before {
content: »;
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: #1e40af;
}.match-team.loser {
opacity: 0.7;
}.match-team-logo {
width: 32px;
height: 32px;
object-fit: contain;
margin-right: 12px;
border-radius: 4px;
background: #f9fafb;
padding: 2px;
}.match-team-name {
flex: 1;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #374151;
font-family: ‘Poppins’, sans-serif;
}.match-team-score {
font-size: 18px;
font-weight: bold;
color: #475569;
min-width: 30px;
text-align: center;
background: #f1f5f9;
padding: 4px 10px;
border-radius: 6px;
}
}/* Mobile layout – simplified */
@media (max-width: 768px) {
.match-card {
padding: 15px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
position: relative;
}.match-team {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
flex: 1;
padding: 0;
border: none;
background: none !important;
}.match-team::before {
display: none;
}.match-team-logo {
width: 40px;
height: 40px;
margin-bottom: 6px;
border-radius: 4px;
}.match-team-name {
font-size: 11px;
font-weight: 600;
max-width: 80px;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #374151;
font-family: ‘Poppins’, sans-serif;
}/* Center content (score or date) */
.match-center-score {
font-size: 20px;
font-weight: bold;
color: #475569;
background: #f1f5f9;
padding: 10px 15px;
border-radius: 8px;
text-align: center;
min-width: 70px;
}.match-center-score.live {
background: #d62243;
color: white;
animation: pulse 2s infinite;
}.match-center-date {
text-align: center;
padding: 5px 10px;
}.match-date-day {
font-size: 12px;
font-weight: 600;
color: #475569;
margin-bottom: 2px;
}.match-date-time {
font-size: 11px;
color: #6b7280;
font-weight: 500;
}.match-info {
display: none;
}.match-status {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
border-radius: 4px 4px 0 0;
}.match-card.finished .match-status {
background: #1e40af;
}.match-card.live .match-status {
background: #d62243;
animation: pulse 2s infinite;
}
}.bracket-match-info {
background: #f9fafb;
padding: 6px 12px;
text-align: center;
font-size: 11px;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}.bracket-match-date {
font-weight: 500;
color: #374151;
}.bracket-match-location {
margin-top: 1px;
font-size: 10px;
}/* Match Status Indicator */
.match-status {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: #e5e7eb;
}.bracket-match.finished .match-status {
background: #1e40af;
}.bracket-match.live .match-status {
background: #d62243;
animation: pulse 2s infinite;
}/* Bracket Navigation for Mobile */
.bracket-navigation {
display: none;
position: sticky;
top: 0;
background: white;
padding: 10px;
border-bottom: 2px solid #e5e7eb;
margin-bottom: 15px;
z-index: 100;
}@media (max-width: 768px) {
.bracket-navigation {
display: flex;
gap: 10px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}.bracket-nav-btn {
padding: 8px 16px;
background: #f1f5f9;
border: 2px solid #475569;
border-radius: 20px;
color: #475569;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: all 0.3s ease;
}.bracket-nav-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(71, 85, 105, 0.2);
}.bracket-nav-btn.active {
background: #475569;
color: white;
}
}/* List View for Groups */
.matches-list {
background: white;
border-radius: 0 0 5px 5px;
padding: 20px;
}.match-row {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 15px;
padding: 15px;
background: #f9fafb;
border-radius: 8px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
align-items: center;
border: 2px solid transparent;
}.match-row:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-color: #475569;
}.team-info {
display: flex;
align-items: center;
gap: 10px;
}.team-info.away {
flex-direction: row-reverse;
text-align: right;
}.team-logo {
width: 40px;
height: 40px;
object-fit: contain;
}.match-center {
text-align: center;
min-width: 120px;
}.score-box {
background: #374151;
color: white;
padding: 8px 16px;
border-radius: 5px;
font-weight: bold;
display: inline-block;
}.score-box.live {
background: #d62243;
animation: pulse 2s infinite;
}@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}.match-datetime {
font-size: 14px;
color: #6b7280;
line-height: 1.4;
}.match-time {
display: block;
font-weight: bold;
color: #374151;
margin-top: 4px;
}/* Enhanced Responsive Design */
@media (max-width: 1200px) {
.bracket-container {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 20px;
}
}@media (max-width: 768px) {
.competition-container {
padding: 10px;
}.tournament-bracket {
padding: 0;
}.match-row {
grid-template-columns: 1fr;
gap: 8px;
padding: 12px;
}.team-info {
justify-content: center;
}.team-info.away {
flex-direction: row;
text-align: left;
}.view-toggle {
width: 100%;
justify-content: stretch;
}.toggle-btn {
flex: 1;
padding: 12px 10px;
font-size: 14px;
}.competition-header {
padding: 15px;
}.competition-header h1 {
font-size: 20px;
}.phase-header {
padding: 12px;
font-size: 14px;
}.match-center {
min-width: 80px;
}.score-box {
padding: 6px 12px;
font-size: 14px;
}
}/* Ultra small devices */
@media (max-width: 480px) {
.bracket-match {
margin-bottom: 8px;
}.team-logo {
width: 30px;
height: 30px;
}.rounds-dropdown-btn {
padding: 10px 15px;
font-size: 14px;
}
}/* Phase type specific styling */
.phase-container.knockout .phase-header {
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
}.phase-container.group .phase-header {
background: linear-gradient(135deg, #004382 0%, #003770 100%);
}/* Dropdown selector styles */
.rounds-dropdown-btn:hover {
background: #1f2937 !important;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(31, 41, 55, 0.3);
}.rounds-dropdown-option:hover {
background-color: #f9fafb !important;
}.rounds-dropdown-option:first-child {
border-radius: 6px 6px 0 0;
}.rounds-dropdown-option:last-child {
border-radius: 0 0 6px 6px;
border-bottom: none !important;
}/* Responsive dropdown */
@media (max-width: 768px) {
.rounds-dropdown-btn {
min-width: 100% !important;
justify-content: center !important;
}
.rounds-dropdown-menu {
left: 0 !important;
right: 0 !important;
}
}/* Bracket Card View for Complex Brackets on Mobile */
.bracket-cards-view {
display: none;
}@media (max-width: 768px) {
.bracket-cards-view {
display: block;
padding: 15px;
}.bracket-phase-card {
background: white;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}.bracket-phase-card-header {
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
color: white;
padding: 15px;
font-weight: 600;
font-size: 16px;
text-align: center;
}.bracket-phase-card-content {
padding: 15px;
}.bracket-phase-card.final .bracket-phase-card-header {
background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%);
}.bracket-phase-card.semifinal .bracket-phase-card-header {
background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%);
}
}
class CompetitionManager {
constructor() {
this.competitionId = this.getCompetitionIdFromUrl();
this.apiBase = ‘https://rugby-app-4ebacff10fda.herokuapp.com’;
this.currentData = null;
this.updateInterval = null;
this.cable = null;
this.competitionChannel = null;
}getCompetitionIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(‘competition’) || ‘top-14’;
}async init() {
await this.loadCompetitionData();
this.setupResponsiveHandler();
this.setupWebSocket();
this.startAutoUpdate();
}setupResponsiveHandler() {
let resizeTimeout;
window.addEventListener(‘resize’, () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
this.render();
}, 250);
});
}setupWebSocket() {
try {
// Configuration basique du WebSocket
const wsUrl = this.apiBase.replace(‘http://’, ‘ws://’).replace(‘https://’, ‘wss://’) + ‘/cable’;
console.log(‘Tentative de connexion WebSocket à:’, wsUrl);
this.cable = new WebSocket(wsUrl);
this.cable.onopen = () => {
console.log(‘WebSocket connecté avec succès’);
this.subscribeToCompetition();
};this.cable.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
} catch (parseError) {
console.warn(‘Erreur parsing message WebSocket:’, parseError);
}
};this.cable.onclose = (event) => {
console.log(‘WebSocket fermé:’, event.code, event.reason);
console.log(‘Tentative de reconnexion dans 5 secondes…’);
setTimeout(() => this.setupWebSocket(), 5000);
};this.cable.onerror = (error) => {
console.error(‘Erreur WebSocket:’, error);
};
} catch (error) {
console.warn(‘WebSocket non disponible, utilisation du polling seulement:’, error);
// Fallback: augmenter la fréquence du polling si WebSocket échoue
this.startAutoUpdate(10000); // Polling toutes les 10 secondes
}
}subscribeToCompetition() {
if (this.cable && this.cable.readyState === WebSocket.OPEN) {
const subscription = {
command: ‘subscribe’,
identifier: JSON.stringify({
channel: ‘CompetitionChannel’,
competition_id: this.competitionId
})
};
this.cable.send(JSON.stringify(subscription));
}
}handleWebSocketMessage(data) {
if (data.type === ‘ping’) return;
console.log(‘Message WebSocket reçu:’, data);
if (data.message) {
const message = data.message;
switch (message.type) {
case ‘score_update’:
case ‘live_score_update’:
this.updateMatchInUI(message.rencontre);
this.showNotification(`Score mis à jour: ${message.rencontre.local_team.name} vs ${message.rencontre.visitor_team.name}`);
break;
case ‘state_change’:
this.updateMatchStateInUI(message.rencontre_id, message.state);
this.showNotification(`État du match changé: ${message.state}`);
break;
}
}
}updateMatchInUI(rencontreData) {
console.log(‘Updating match UI for:’, rencontreData);
// Trouver et mettre à jour les éléments de match dans l’interface
const matchElements = document.querySelectorAll(`[data-match-id= »${rencontreData.id} »]`);
console.log(‘Found match elements:’, matchElements.length);
matchElements.forEach(element => {
const homeScore = rencontreData.score_locale || 0;
const awayScore = rencontreData.score_visiteuse || 0;
const isLive = rencontreData.etat === ‘endirect’ || rencontreData.etat === ‘mi-temps’;
const isFinished = rencontreData.etat === ‘termine’;
console.log(‘Updating element with scores:’, homeScore, awayScore, ‘state:’, rencontreData.etat);
// Mobile layout
if (window.innerWidth <= 768) {
let centerElement = element.querySelector('.match-center-score, .match-center-date');
if (isLive || isFinished) {
// Si le match est live ou terminé, on doit afficher le score
if (!element.querySelector('.match-center-score')) {
// Remplacer le container de date par le container de score
const dateElement = element.querySelector('.match-center-date');
if (dateElement) {
const scoreElement = document.createElement('div');
scoreElement.className = `match-center-score ${isLive ? 'live' : ''}`;
scoreElement.textContent = `${homeScore} – ${awayScore}`;
dateElement.parentNode.replaceChild(scoreElement, dateElement);
}
} else {
// Mettre à jour le score existant
const centerScore = element.querySelector('.match-center-score');
if (centerScore) {
centerScore.textContent = `${homeScore} – ${awayScore}`;
if (isLive) {
centerScore.classList.add('live');
} else {
centerScore.classList.remove('live');
}
}
}
}
} else {
// Desktop layout
const matchCenter = element.querySelector('.match-center');
if (matchCenter) {
if (isLive || isFinished) {
// Si le match est live ou terminé, remplacer par score-box
if (!element.querySelector('.score-box')) {
matchCenter.innerHTML = `
${homeScore} – ${awayScore}
`;
} else {
// Mettre à jour le score existant
const scoreBox = element.querySelector(‘.score-box’);
if (scoreBox) {
scoreBox.textContent = `${homeScore} – ${awayScore}`;
if (isLive) {
scoreBox.classList.add(‘live’);
} else {
scoreBox.classList.remove(‘live’);
}
}
}
}
}
}
// Mettre à jour l’état visuel de la card
element.classList.remove(‘finished’, ‘live’);
if (isFinished) {
element.classList.add(‘finished’);
} else if (isLive) {
element.classList.add(‘live’);
}
console.log(‘Updated match state classes’);
});
}updateMatchStateInUI(rencontreId, newState) {
const matchElements = document.querySelectorAll(`[data-match-id= »${rencontreId} »]`);
matchElements.forEach(element => {
// Réinitialiser les classes d’état
element.className = element.className.replace(/\b(finished|live|scheduled)\b/g, »);
// Ajouter la nouvelle classe selon l’état
switch (newState) {
case ‘termine’:
element.classList.add(‘finished’);
break;
case ‘endirect’:
case ‘mi-temps’:
element.classList.add(‘live’);
break;
case ‘programme’:
element.classList.add(‘scheduled’);
break;
}
});
}showNotification(message) {
// Créer une notification simple
const notification = document.createElement(‘div’);
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #374151;
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 10000;
font-size: 14px;
max-width: 300px;
transition: all 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Animation d’apparition
setTimeout(() => {
notification.style.transform = ‘translateX(0)’;
notification.style.opacity = ‘1’;
}, 100);
// Supprimer après 5 secondes
setTimeout(() => {
notification.style.transform = ‘translateX(100%)’;
notification.style.opacity = ‘0’;
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 5000);
}showLoader() {
document.getElementById(‘loader’).classList.add(‘active’);
}hideLoader() {
document.getElementById(‘loader’).classList.remove(‘active’);
}async loadCompetitionData() {
this.showLoader();
try {
const response = await fetch(`${this.apiBase}/competitions/${this.competitionId}/adaptive`);
this.currentData = await response.json();
this.render();
} catch (error) {
console.error(‘Erreur lors du chargement:’, error);
this.showError(‘Erreur lors du chargement des données’);
} finally {
this.hideLoader();
}
}render() {
if (!this.currentData) return;// Update header (only title, no date)
document.getElementById(‘competitionTitle’).textContent = this.currentData.competition.display_name;
document.getElementById(‘competitionInfo’).textContent = »;// Store competition date for inline display
this.competitionDate = this.currentData.competition.years || »;// Render based on competition type
const mainContent = document.getElementById(‘mainContent’);
mainContent.innerHTML = »;switch (this.currentData.type) {
case ‘championship’:
this.renderChampionship(mainContent);
break;
case ‘cup’:
this.renderCup(mainContent);
break;
case ‘tournament’:
this.renderTournament(mainContent);
break;
default:
this.renderDefault(mainContent);
}// Update inline competition dates after rendering
this.updateInlineCompetitionDates();
}updateInlineCompetitionDates() {
// Update all inline competition date elements
const inlineDateElements = document.querySelectorAll(‘.competition-date-inline’);
inlineDateElements.forEach(element => {
element.textContent = this.competitionDate;
});
}renderCup(container) {
const view = document.createElement(‘div’);
view.className = ‘cup-view’;// Render all phases as list
this.renderListView(view, this.currentData.phases);container.appendChild(view);
}groupPhasesByType(phases) {
const grouped = {
group: [],
knockout: []
};phases.forEach(phase => {
if (phase.type === ‘knockout’) {
grouped.knockout.push(phase);
} else {
grouped.group.push(phase);
}
});return grouped;
}renderListView(container, phases) {
phases.forEach(phase => {
const phaseContainer = this.createPhaseContainer(phase, false);
container.appendChild(phaseContainer);
});
}createPhaseContainer(phase) {
const phaseContainer = document.createElement(‘div’);
phaseContainer.className = `phase-container ${phase.type}`;const phaseHeader = document.createElement(‘div’);
phaseHeader.className = ‘phase-header’;
phaseHeader.innerHTML = `
${phase.name} Choisir une journée…
${phase.type === ‘group’ ? ‘‘ : »}
${phase.type === ‘group’ ? ‘‘ : »}
`;
phaseContainer.appendChild(phaseHeader);// Add event listeners to header buttons
const headerBtns = phaseHeader.querySelectorAll(‘.header-tab-btn’);
headerBtns.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
e.stopPropagation();
const tabType = btn.dataset.tab;
// Update button styles for all tabs
headerBtns.forEach(b => {
b.classList.remove(‘active’);
b.style.background = ‘transparent’;
b.style.color = ‘rgba(255,255,255,0.7)’;
});
btn.classList.add(‘active’);
btn.style.background = ‘rgba(255,255,255,0.2)’;
btn.style.color = ‘white’;
this.showPhaseTab(phaseContainer, phase, tabType);
});
});// Initial content
const contentContainer = document.createElement(‘div’);
contentContainer.className = ‘phase-content’;
phaseContainer.appendChild(contentContainer);
// Show rounds by default (matches)
this.showPhaseTab(phaseContainer, phase, ’rounds’);return phaseContainer;
}showPhaseTab(container, phase, tab) {
// Update content
const contentArea = container.querySelector(‘.phase-content’);
contentArea.innerHTML = »;
if (tab === ’rounds’) {
const matchesContainer = this.createMatchesListGrouped(phase.matches);
contentArea.appendChild(matchesContainer);
// Initialize header selector with journée data
const headerSelector = container.querySelector(‘.journee-selector-header’);
if (headerSelector && matchesContainer.journeeData) {
const { sortedJournees, defaultJournee } = matchesContainer.journeeData;
// Clear existing options
headerSelector.innerHTML = »;
// Add journée options
console.log(‘Initializing selector with default journée:’, defaultJournee);
sortedJournees.forEach(journee => {
const option = document.createElement(‘option’);
option.value = journee;
option.textContent = journee;
if (journee === defaultJournee) {
option.selected = true;
console.log(‘Selected option:’, journee);
}
headerSelector.appendChild(option);
});
// Set the selected value explicitly
headerSelector.value = defaultJournee;
console.log(‘Set selector value to:’, headerSelector.value);
// Remove existing event listeners and add new one
const newHeaderSelector = headerSelector.cloneNode(true);
newHeaderSelector.value = defaultJournee; // Ensure value is maintained after cloning
headerSelector.parentNode.replaceChild(newHeaderSelector, headerSelector);
newHeaderSelector.addEventListener(‘change’, (e) => {
if (matchesContainer.showJourneeMatches) {
matchesContainer.showJourneeMatches(e.target.value);
}
});
// Show the selector
newHeaderSelector.style.display = ‘block’;
console.log(‘Final selector value:’, newHeaderSelector.value);
}
} else if (tab === ‘standings’) {
// Keep header selector visible
const headerSelector = container.querySelector(‘.journee-selector-header’);
if (headerSelector) {
headerSelector.style.display = ‘block’;
}
this.renderPhaseStandings(contentArea, phase);
}
}renderPhaseStandings(container, phase) {
const standingsContainer = document.createElement(‘div’);
standingsContainer.className = ‘standings-table’;
standingsContainer.style.cssText = ‘background: white; border-radius: 5px; overflow: hidden; margin-top: 20px;’;
if (phase.standings && phase.standings.length > 0) {
const table = document.createElement(‘table’);
table.style.cssText = ‘width: 100%; border-collapse: collapse;’;
// Header
const thead = document.createElement(‘thead’);
const headerRow = document.createElement(‘tr’);
headerRow.style.cssText = ‘background: #f3f4f6; border-bottom: 2px solid #e5e7eb;’;
headerRow.innerHTML = `
Équipe | J | V | D | +/- | Pts | `;
thead.appendChild(headerRow);
table.appendChild(thead);
// Body
const tbody = document.createElement(‘tbody’);
phase.standings.forEach((team, index) => {
const row = document.createElement(‘tr’);
row.style.cssText = `border-bottom: 1px solid #e5e7eb; ${index % 2 === 0 ? ‘background: #f9fafb;’ : »} transition: background-color 0.2s ease;`;
row.innerHTML = `
${team.rank || index + 1}. ${team.team_name} | ${team.matchs_joues} | ${team.matchs_gagnes} | ${team.matchs_perdus} | = 0 ? ‘#059669’ : ‘#dc2626’}; font-weight: 500; »>${team.diff_buts >= 0 ? ‘+’ : »}${team.diff_buts} | ${team.points} | `;
tbody.appendChild(row);
});
table.appendChild(tbody);
standingsContainer.appendChild(table);
} else {
const noStandings = document.createElement(‘p’);
noStandings.textContent = ‘Aucun classement disponible’;
noStandings.style.cssText = ‘color: #6b7280; text-align: center; padding: 40px;’;
standingsContainer.appendChild(noStandings);
}
container.appendChild(standingsContainer);
}renderChampionship(container) {
const view = document.createElement(‘div’);
view.className = ‘championship-view’;// Render each group
this.currentData.groups.forEach(group => {
const groupContainer = this.createGroupContainer(group);
view.appendChild(groupContainer);
});container.appendChild(view);
}createGroupContainer(group) {
const groupContainer = document.createElement(‘div’);
groupContainer.className = ‘phase-container group’;const groupHeader = document.createElement(‘div’);
groupHeader.className = ‘phase-header’;
groupHeader.innerHTML = `
${group.name === ‘1’ ? ‘Top 14’ : `Groupe ${group.name}`}
`;
groupContainer.appendChild(groupHeader);// Add event listeners to header buttons
const headerBtns = groupHeader.querySelectorAll(‘.header-tab-btn’);
headerBtns.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
e.stopPropagation();
const tabType = btn.dataset.tab;
// Update button styles with smooth transition
headerBtns.forEach(b => {
b.classList.remove(‘active’);
b.style.transition = ‘all 0.3s ease’;
b.style.background = ‘transparent’;
b.style.color = ‘rgba(255,255,255,0.7)’;
});
btn.classList.add(‘active’);
btn.style.transition = ‘all 0.3s ease’;
btn.style.background = ‘rgba(255,255,255,0.2)’;
btn.style.color = ‘white’;
this.switchGroupTab(groupContainer, tabType, group);
// Update inline dates after tab switch
setTimeout(() => this.updateInlineCompetitionDates(), 50);
});
});// Content area
const contentArea = document.createElement(‘div’);
contentArea.className = ‘group-content’;
groupContainer.appendChild(contentArea);// Initially show rounds
this.renderGroupRounds(contentArea, group);return groupContainer;
}switchGroupTab(container, tab, group) {
// Update tab styles
const tabs = container.querySelectorAll(‘.tab’);
tabs.forEach(t => t.classList.remove(‘active’));
const activeTab = Array.from(tabs).find(t =>
(tab === ’rounds’ && t.textContent === ‘Journées’) ||
(tab === ‘standings’ && t.textContent === ‘Classement’)
);
if (activeTab) activeTab.classList.add(‘active’);// Update content
const contentArea = container.querySelector(‘.group-content’);
contentArea.innerHTML = »;
if (tab === ’rounds’) {
this.renderGroupRounds(contentArea, group);
} else {
this.renderGroupStandings(contentArea, group);
}
}renderGroupRounds(container, group) {
const roundsContainer = document.createElement(‘div’);
roundsContainer.className = ‘matches-list’;
if (group.rounds && group.rounds.length > 0) {
group.rounds.forEach(round => {
const roundHeader = document.createElement(‘h3’);
roundHeader.textContent = round.name;
roundHeader.style.cssText = ‘margin: 20px 0 10px 0; color: #475569;’;
roundsContainer.appendChild(roundHeader);
if (round.matches && round.matches.length > 0) {
round.matches.forEach(match => {
const matchCard = this.createMatchCard(match);
roundsContainer.appendChild(matchCard);
});
} else {
const noMatches = document.createElement(‘p’);
noMatches.textContent = ‘Aucun match programmé’;
noMatches.style.cssText = ‘color: #6b7280; font-style: italic; margin-bottom: 20px; padding-left: 20px;’;
roundsContainer.appendChild(noMatches);
}
});
} else {
const noRounds = document.createElement(‘p’);
noRounds.textContent = ‘Aucune journée disponible’;
noRounds.style.cssText = ‘color: #6b7280; text-align: center; padding: 40px;’;
roundsContainer.appendChild(noRounds);
}
container.appendChild(roundsContainer);
}renderGroupStandings(container, group) {
const standingsContainer = document.createElement(‘div’);
standingsContainer.className = ‘standings-table’;
standingsContainer.style.cssText = ‘background: white; border-radius: 5px; overflow: hidden; margin-top: 20px;’;
if (group.standings && group.standings.length > 0) {
const table = document.createElement(‘table’);
table.style.cssText = ‘width: 100%; border-collapse: collapse;’;
// Header
const header = document.createElement(‘thead’);
header.innerHTML = `
| Pos | Équipe | J | G | N | P | +/- | Pts |
|---|
`;
table.appendChild(header);
// Body
const tbody = document.createElement(‘tbody’);
group.standings.forEach((team, index) => {
const row = document.createElement(‘tr’);
row.style.cssText = `border-bottom: 1px solid #e5e7eb; ${index % 2 === 0 ? ‘background: #f9fafb;’ : »} transition: background-color 0.2s ease;`;
row.innerHTML = `
${index + 1} | ${team.team_name} | ${team.matchs_joues || 0} | ${team.matchs_gagnes || 0} | ${team.matchs_nuls || 0} | ${team.matchs_perdus || 0} | ${team.diff_buts || 0} | ${team.points || 0} | `;
tbody.appendChild(row);
});
table.appendChild(tbody);
standingsContainer.appendChild(table);
} else {
const noStandings = document.createElement(‘p’);
noStandings.textContent = ‘Aucun classement disponible’;
noStandings.style.cssText = ‘color: #6b7280; text-align: center; padding: 40px;’;
standingsContainer.appendChild(noStandings);
}
container.appendChild(standingsContainer);
}renderTournament(container) {
const view = document.createElement(‘div’);
view.className = ‘tournament-view’;// Group phases by type for better organization
const groupedPhases = this.groupPhasesByType(this.currentData.phases);// Render championship phase with rounds navigation
if (groupedPhases.group.length > 0) {
const championshipPhase = groupedPhases.group[0];
this.renderChampionshipPhase(view, championshipPhase);
}// Render knockout phases as list
if (groupedPhases.knockout.length > 0) {
this.renderListView(view, groupedPhases.knockout);
}container.appendChild(view);
}renderChampionshipPhase(container, phase) {
const phaseContainer = document.createElement(‘div’);
phaseContainer.className = ‘phase-container group’;const phaseHeader = document.createElement(‘div’);
phaseHeader.className = ‘phase-header’;
phaseHeader.innerHTML = `
${phase.name}
`;
phaseContainer.appendChild(phaseHeader);// Add event listeners to header buttons
const headerBtns = phaseHeader.querySelectorAll(‘.header-tab-btn’);
headerBtns.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
e.stopPropagation();
const tabType = btn.dataset.tab;
// Update button styles with smooth transition
headerBtns.forEach(b => {
b.classList.remove(‘active’);
b.style.transition = ‘all 0.3s ease’;
b.style.background = ‘transparent’;
b.style.color = ‘rgba(255,255,255,0.7)’;
});
btn.classList.add(‘active’);
btn.style.transition = ‘all 0.3s ease’;
btn.style.background = ‘rgba(255,255,255,0.2)’;
btn.style.color = ‘white’;
this.switchChampionshipTab(phaseContainer, tabType, phase);
// Update inline dates after tab switch
setTimeout(() => this.updateInlineCompetitionDates(), 50);
});
});// Setup rounds dropdown in header
this.setupHeaderRoundsDropdown(phaseHeader, phase, phaseContainer);// Content area
const contentArea = document.createElement(‘div’);
contentArea.className = ‘championship-content’;
phaseContainer.appendChild(contentArea);// Initially show rounds with navigation
this.renderChampionshipRounds(contentArea, phase);container.appendChild(phaseContainer);
}switchChampionshipTab(container, tab, phase) {
// Update tab styles
const tabs = container.querySelectorAll(‘.tab’);
tabs.forEach(t => t.classList.remove(‘active’));
const activeTab = Array.from(tabs).find(t =>
(tab === ’rounds’ && t.textContent === ‘Journées’) ||
(tab === ‘standings’ && t.textContent === ‘Classement’)
);
if (activeTab) activeTab.classList.add(‘active’);// Update content
const contentArea = container.querySelector(‘.championship-content’);
contentArea.innerHTML = »;
if (tab === ’rounds’) {
this.renderChampionshipRounds(contentArea, phase);
} else {
this.renderChampionshipStandings(contentArea, phase);
}
}renderChampionshipRounds(container, phase) {
// Matches container only – selector is now in header
const matchesContainer = document.createElement(‘div’);
matchesContainer.className = ’round-matches’;
container.appendChild(matchesContainer);// Show first round by default
if (phase.rounds && phase.rounds.length > 0) {
this.loadRoundMatches(matchesContainer, phase.rounds[0]);
}
}setupHeaderRoundsDropdown(phaseHeader, phase, phaseContainer) {
const dropdownBtn = phaseHeader.querySelector(‘.rounds-dropdown-btn-header’);
const dropdownMenu = phaseHeader.querySelector(‘.rounds-dropdown-menu-header’);
if (!dropdownBtn || !dropdownMenu) return;// Populate dropdown menu with rounds
if (phase.rounds && phase.rounds.length > 0) {
phase.rounds.forEach((round, index) => {
const option = document.createElement(‘div’);
option.className = ’rounds-dropdown-option-header’;
option.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #e5e7eb;
transition: all 0.3s ease;
font-size: 12px;
color: #374151;
`;
option.textContent = round.name;// Hover effects
option.onmouseenter = () => {
option.style.backgroundColor = ‘#f9fafb’;
};
option.onmouseleave = () => {
option.style.backgroundColor = ‘transparent’;
};// Click handler
option.onclick = () => {
// Update button text
dropdownBtn.querySelector(‘span:first-child’).textContent = round.name;
// Hide dropdown
dropdownMenu.style.display = ‘none’;
dropdownBtn.querySelector(‘span:last-child’).textContent = ‘▼’;
// Load round matches
const matchesContainer = phaseContainer.querySelector(‘.round-matches’);
if (matchesContainer) {
this.loadRoundMatches(matchesContainer, round);
}
};if (index === phase.rounds.length – 1) {
option.style.borderBottom = ‘none’;
}dropdownMenu.appendChild(option);
});
}// Toggle dropdown on button click
dropdownBtn.onclick = (e) => {
e.stopPropagation();
const isVisible = dropdownMenu.style.display === ‘block’;
dropdownMenu.style.display = isVisible ? ‘none’ : ‘block’;
dropdownBtn.querySelector(‘span:last-child’).textContent = isVisible ? ‘▼’ : ‘▲’;
};// Close dropdown when clicking outside
document.addEventListener(‘click’, (e) => {
if (!phaseHeader.querySelector(‘.rounds-selector-header’).contains(e.target)) {
dropdownMenu.style.display = ‘none’;
dropdownBtn.querySelector(‘span:last-child’).textContent = ‘▼’;
}
});
}async selectRoundById(container, round, selectedOption) {
// Display matches for selected round
const matchesContainer = container.querySelector(‘.round-matches’);
this.loadRoundMatches(matchesContainer, round);
}async loadRoundMatches(container, round) {
// Show loading
container.innerHTML = ‘
Chargement…
‘;try {
const response = await fetch(`${this.apiBase}/competitions/${this.competitionId}/rounds/${round.id}`);
const data = await response.json();
this.displayRoundMatches(container, data.matches);
} catch (error) {
console.error(‘Error loading round matches:’, error);
container.innerHTML = ‘
Erreur lors du chargement des matches
‘;
}
}groupMatchesByRound(matches) {
const grouped = {};
matches.forEach(match => {
const roundName = this.extractRoundName(match);
if (!grouped[roundName]) {
grouped[roundName] = [];
}
grouped[roundName].push(match);
});
return grouped;
}extractRoundName(match) {
// Try to extract round name from match data
if (match.phase_name) {
return match.phase_name;
}
// Fallback to date-based grouping
const date = new Date(match.date);
return `Journée du ${date.toLocaleDateString(‘fr-FR’, { day: ‘numeric’, month: ‘short’ })}`;
}selectRound(container, roundName, matchesByRound, selectedBtn) {
// Update button styles
const buttons = container.querySelectorAll(‘.round-btn’);
buttons.forEach(btn => {
btn.style.background = ‘white’;
btn.style.color = ‘#374151’;
btn.style.borderColor = ‘#e5e7eb’;
btn.classList.remove(‘active’);
});
selectedBtn.style.background = ‘#475569’;
selectedBtn.style.color = ‘white’;
selectedBtn.style.borderColor = ‘#475569’;
selectedBtn.classList.add(‘active’);// Display matches for selected round
const matchesContainer = container.querySelector(‘.round-matches’);
this.displayRoundMatches(matchesContainer, matchesByRound[roundName]);
}displayRoundMatches(container, matches) {
container.innerHTML = »;
const matchesList = document.createElement(‘div’);
matchesList.className = ‘matches-list’;
matches.forEach(match => {
const matchCard = this.createMatchCard(match);
matchesList.appendChild(matchCard);
});
container.appendChild(matchesList);
}renderChampionshipStandings(container, phase) {
const standingsContainer = document.createElement(‘div’);
standingsContainer.className = ‘standings-table’;
standingsContainer.style.cssText = ‘background: white; border-radius: 5px; overflow: hidden; margin-top: 20px;’;
if (phase.standings && phase.standings.length > 0) {
const table = document.createElement(‘table’);
table.style.cssText = ‘width: 100%; border-collapse: collapse;’;
// Header
const header = document.createElement(‘thead’);
header.innerHTML = `
| Pos | Équipe | J | G | N | P | +/- | Pts |
|---|
`;
table.appendChild(header);
// Body
const tbody = document.createElement(‘tbody’);
phase.standings.forEach((team, index) => {
const row = document.createElement(‘tr’);
row.style.cssText = `border-bottom: 1px solid #e5e7eb; ${index % 2 === 0 ? ‘background: #f9fafb;’ : »} transition: background-color 0.2s ease;`;
row.innerHTML = `
${index + 1} | ${team.team_name} | ${team.matchs_joues || 0} | ${team.matchs_gagnes || 0} | ${team.matchs_nuls || 0} | ${team.matchs_perdus || 0} | ${team.diff_buts || 0} | ${team.points || 0} | `;
tbody.appendChild(row);
});
table.appendChild(tbody);
standingsContainer.appendChild(table);
} else {
const noStandings = document.createElement(‘p’);
noStandings.textContent = ‘Aucun classement disponible’;
noStandings.style.cssText = ‘color: #6b7280; text-align: center; padding: 40px;’;
standingsContainer.appendChild(noStandings);
}
container.appendChild(standingsContainer);
}renderDefault(container) {
// Fallback rendering
const matchesList = this.createMatchesList(this.currentData.matches || []);
container.appendChild(matchesList);
}createMatchesListGrouped(matches) {
const container = document.createElement(‘div’);
container.className = ‘matches-list-grouped’;// Group matches by journée (phase_name)
const matchesByJournee = {};
matches.forEach(match => {
const journee = match.phase_name || ‘Matches’;
if (!matchesByJournee[journee]) {
matchesByJournee[journee] = [];
}
matchesByJournee[journee].push(match);
});// Sort journées chronologically
const sortedJournees = Object.keys(matchesByJournee).sort((a, b) => {
// Extract number from « 1re journée », « 2e journée », etc.
const getJourneeNumber = (str) => {
const match = str.match(/(\d+)[re]+\s+journ/i);
return match ? parseInt(match[1]) : 999;
};
return getJourneeNumber(a) – getJourneeNumber(b);
});if (sortedJournees.length === 0) {
const noMatches = document.createElement(‘div’);
noMatches.style.cssText = ‘text-align: center; padding: 40px; color: #6b7280; background: white; border-radius: 8px;’;
noMatches.innerHTML = `
⚽
Aucun match disponible
À lire aussi | Vannes : Simione Kuruvoli suspendu et absent en début de saison
`;
container.appendChild(noMatches);
return container;
}// Find the closest journée to current date
const getCurrentClosestJournee = () => {
const today = new Date();
today.setHours(0, 0, 0, 0); // Reset time to compare only dates
let closestJournee = sortedJournees[0];
let minDiff = Infinity;console.log(‘Looking for closest journée to today:’, today.toDateString());sortedJournees.forEach(journee => {
const journeeMatches = matchesByJournee[journee];
if (journeeMatches.length > 0) {
// Get the earliest match date in this journée
const earliestMatch = journeeMatches.reduce((earliest, match) => {
const matchDate = new Date(match.date);
const earliestDate = new Date(earliest.date);
return matchDate < earliestDate ? match : earliest;
});
const matchDate = new Date(earliestMatch.date);
matchDate.setHours(0, 0, 0, 0); // Reset time to compare only dates
const diff = Math.abs(matchDate – today);
console.log(`Journée "${journee}": earliest match ${matchDate.toDateString()}, diff: ${diff} days`);
if (diff {
contentArea.innerHTML = »;
if (!matchesByJournee[selectedJournee]) return;const journeeSection = document.createElement(‘div’);
journeeSection.className = ‘journee-section’;// Matches container (sans header vert)
const matchesContainer = document.createElement(‘div’);
matchesContainer.className = ‘journee-matches’;
matchesContainer.style.cssText = ‘background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);’;
matchesByJournee[selectedJournee].forEach((match, index) => {
const matchCard = this.createMatchCard(match);
matchCard.style.borderRadius = ‘0’;
matchCard.style.marginBottom = ‘0’;
if (index {
const matchCard = this.createMatchCard(match);
container.appendChild(matchCard);
});return container;
}createMatchCard(match) {
const isFinished = match.etat === ‘termine’;
const isLive = match.etat === ‘endirect’;
const card = document.createElement(‘div’);
card.className = `match-card ${isFinished ? ‘finished’ : »} ${isLive ? ‘live’ : »}`;
card.setAttribute(‘data-match-id’, match.id);
card.onclick = () => window.location.href = `https://vibrez-rugby.com/live-en-direct/?id=${match.id}`;// Add status indicator
const statusBar = document.createElement(‘div’);
statusBar.className = ‘match-status’;
card.appendChild(statusBar);// Determine winner if match is finished
let homeWinner = false, awayWinner = false;
if (isFinished && match.score_locale !== null && match.score_visiteuse !== null) {
const homeScore = parseInt(match.score_locale) || 0;
const awayScore = parseInt(match.score_visiteuse) || 0;
if (homeScore > awayScore) {
homeWinner = true;
} else if (awayScore > homeScore) {
awayWinner = true;
}
}// Mobile layout (default)
if (window.innerWidth <= 768) {
// Determine what to show in center (score or date)
let centerContent;
if (isFinished || isLive) {
// Show score
const homeScore = match.score_locale || '0';
const awayScore = match.score_visiteuse || '0';
centerContent = `
${homeScore} – ${awayScore}
`;
} else {
// Show date/time
const matchDate = new Date(match.date);
centerContent = `
${matchDate.toLocaleDateString(‘fr-FR’, { day: ‘numeric’, month: ‘short’ })}
${matchDate.toLocaleTimeString(‘fr-FR’, { hour: ‘2-digit’, minute: ‘2-digit’ })}
`;
}card.innerHTML += `
${match.local_team.name} ${centerContent}
${match.visitor_team.name} `;
} else {
// Desktop layout – back to original row format
card.className = ‘match-row’;
card.setAttribute(‘data-match-id’, match.id);
card.innerHTML = `
${match.local_team.name} ${(isFinished || isLive) ?
`
${match.score_locale || 0} – ${match.score_visiteuse || 0}
` :
`
${new Date(match.date).toLocaleDateString(‘fr-FR’, { day: ‘numeric’, month: ‘long’, year: ‘numeric’ })} ${new Date(match.date).toLocaleTimeString(‘fr-FR’, { hour: ‘2-digit’, minute: ‘2-digit’ })}
`
}
${match.visitor_team.name} `;
}return card;
}createMatchRow(match) {
const row = document.createElement(‘div’);
row.className = ‘match-row’;
row.onclick = () => window.location.href = `https://vibrez-rugby.com/live-en-direct/?id=${match.id}`;// Home team
const homeTeam = document.createElement(‘div’);
homeTeam.className = ‘team-info’;
homeTeam.innerHTML = `
${match.local_team.display_name} `;
row.appendChild(homeTeam);// Center (score or datetime)
const center = document.createElement(‘div’);
center.className = ‘match-center’;
if (match.etat === ‘termine’ || match.etat === ‘endirect’) {
const scoreBox = document.createElement(‘div’);
scoreBox.className = ‘score-box’ + (match.etat === ‘endirect’ ? ‘ live’ : »);
scoreBox.textContent = `${match.score_locale || 0} – ${match.score_visiteuse || 0}`;
center.appendChild(scoreBox);
} else {
const datetime = new Date(match.date);
center.innerHTML = `
${datetime.toLocaleDateString(‘fr-FR’, { day: ‘numeric’, month: ‘long’, year: ‘numeric’ })} ${datetime.toLocaleTimeString(‘fr-FR’, { hour: ‘2-digit’, minute: ‘2-digit’ })}
`;
}
row.appendChild(center);// Away team
const awayTeam = document.createElement(‘div’);
awayTeam.className = ‘team-info away’;
awayTeam.innerHTML = `
${match.visitor_team.display_name} `;
row.appendChild(awayTeam);return row;
}startAutoUpdate() {
// Update scores every 30 seconds
this.updateInterval = setInterval(() => {
this.updateLiveScores();
}, 30000);
}async updateLiveScores() {
// Only update if there are live matches
if (!this.hasLiveMatches()) return;try {
const response = await fetch(`${this.apiBase}/competitions/${this.competitionId}/live_scores`);
const liveData = await response.json();
// Update UI with new scores
this.updateScoresInUI(liveData);
} catch (error) {
console.error(‘Erreur lors de la mise à jour des scores:’, error);
}
}hasLiveMatches() {
// Check if any match is live
return true; // Simplified
}updateScoresInUI(liveData) {
// Update scores without full re-render
// Implementation depends on your needs
}showError(message) {
const mainContent = document.getElementById(‘mainContent’);
mainContent.innerHTML = `
`;
}destroy() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
if (this.cable) {
this.cable.close();
}
}
}// Initialize on page load
document.addEventListener(‘DOMContentLoaded’, () => {
const manager = new CompetitionManager();
manager.init();// Cleanup on page unload
window.addEventListener(‘beforeunload’, () => {
manager.destroy();
});
});