Au Stade Maurice-David Aix-en-Provence, Aix-en-Provence bat Montauban 29 à 7 (mi-temps: 24-7) pour le compte de la compétition prod2 – 25e-journée – Poule 1
(1) Malino Vanai, (2) Kevin Firmin, (3) Mirian Burduli, (4) Frank Bradshaw, (5) Lewis Bean, (6) Kyllian Ringuet, (7) Otar Giorgadze, (8) Quentin Witt, (9) Yoan Cottin, (10) Jérôme Bosviel, (11) Romain Fonnicola, (12) Maxime Mathy, (13) Simon Renda, (14) Josua Vici, (15) Semesa Rokoduguni, (16) German Kessler, (17) Nicolas Agnesi, (18) Kevin Gimeno, (19) Noa Kanika, (20) Lui Naeata, (21) Shaun Venter, (22) Thomas Larregain, (23) Victor Delmas
* {
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’) || ‘pro-d2’;
}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
`;
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();
});
});
Résultats PRO D2 Résultats Pro D2 et tout le calendrier de ce championnat.
Matchs de Pro D2 en direct, cliquez sur le club ou le match de votre choix pour suivre la rencontre en direct/live, voir ou revoir les actions du match, les joueurs qui ont marqué, ainsi que la composition des équipes. Vous pouvez également conserver cette page afin de suivre l’évolution des scores de chaque équipe, y compris les Résultats PRO D2.
Résultats PRO D2 : suivez l’action palpitante du championnat La PRO D2 est la deuxième division du rugby français. Elle offre une plateforme compétitive aux clubs aspirant à intégrer l’élite du rugby. Ce championnat, à travers ses Résultats PRO D2 , est un pilier essentiel du développement du rugby en France. Découvrons ensemble les grandes lignes de cette compétition, son format, et les équipes qui l’animent.
Le championnat PRO D2 regroupe 16 équipes professionnelles. Ces clubs s’affrontent au cours d’une saison régulière, où chaque équipe joue à domicile et à l’extérieur. Les Résultats PRO D2 reflètent la lutte acharnée entre ces formations, avec pour objectif ultime de terminer parmi les meilleures et ainsi rêver de promotion.
La promotion est au cœur de la compétition. Pour chaque équipe, l’objectif principal est d’accéder au TOP 14, le sommet du rugby français. Les Résultats PRO D2 jouent un rôle crucial dans ce parcours, car ils déterminent les équipes qui se qualifieront pour les phases finales, menant potentiellement à la montée.
Les clubs emblématiques de la PRO D2 Des clubs prestigieux comme l’US Oyonnax, le SU Agen ou le Biarritz Olympique sont des noms bien connus du rugby français. Ces équipes, souvent en quête de promotion, contribuent à rendre la compétition plus intense. Les Résultats PRO D2 témoignent de leur ambition et de leur lutte pour atteindre les sommets.
Le rôle de la PRO D2 dans le développement des talents La PRO D2 est une véritable pépinière de jeunes talents. De nombreux joueurs font leurs débuts dans cette compétition avant d’atteindre l’élite. Les Résultats PRO D2 permettent aux recruteurs et observateurs de repérer ces futurs stars du rugby, qui gagnent en expérience au fil des matchs.
Des matchs sous haute intensité Les rencontres de la PRO D2 sont souvent intenses, et chaque point compte dans la course à la promotion. Les équipes rivalisent d’efforts pour grimper au classement, et les Résultats PRO D2 reflètent cette détermination. Que ce soit lors des matchs à domicile ou à l’extérieur, les équipes donnent tout sur le terrain.
Suivre les Résultats PRO D2 en direct Pour les passionnés de rugby, il est désormais facile de suivre les Résultats PRO D2 en direct. Grâce à une couverture complète, que ce soit à la radio, en ligne ou via des applications dédiées, vous ne manquerez rien des exploits de vos équipes préférées. Chaque essai, transformation et pénalité est commenté en direct pour enrichir votre expérience.
Conclusion La PRO D2 n’est pas seulement une compétition de rugby, c’est un vivier de talents et une bataille féroce pour la promotion vers le TOP 14. Grâce aux Résultats PRO D2 , les amateurs peuvent suivre de près l’évolution de leurs équipes favorites. Ne manquez aucun moment fort de cette compétition captivante et plongez dans l’univers du rugby avec passion.
Pour Résumer : La PRO D2 est un championnat clé du rugby français. Les Résultats permettent aux fans de suivre l’évolution de la saison et d’observer la lutte des équipes pour la promotion vers le TOP 14.
Résultats Pro D2