Implement controls and live scoreboard overview
This commit is contained in:
parent
195a208b5a
commit
853de6f18f
748
css/live-scoreboard.css
Normal file
748
css/live-scoreboard.css
Normal file
@ -0,0 +1,748 @@
|
|||||||
|
/**
|
||||||
|
* Live Scoreboard CSS
|
||||||
|
* Comprehensive styling for scoreboards, fixtures, and admin controls
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Base Scoreboard Styles */
|
||||||
|
.scoreboard {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 20px auto;
|
||||||
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard .border {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .border {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Team Styles */
|
||||||
|
.team {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 0 10px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid rgba(255,255,255,0.3);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .team-logo {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .team-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFD700;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||||
|
margin-left: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .team-score {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score.score-changed {
|
||||||
|
transform: scale(1.1);
|
||||||
|
color: #FFF700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timer Container */
|
||||||
|
.timer-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 30px;
|
||||||
|
color: white;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .timer-container {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer.active {
|
||||||
|
background: rgba(0,255,0,0.2);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer.inactive {
|
||||||
|
background: rgba(255,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .timer {
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.half-indicator {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.compact .half-indicator {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game State Classes */
|
||||||
|
.scoreboard.game-off {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.state-waiting-for-start .timer {
|
||||||
|
background: rgba(128,128,128,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.state-finished .timer {
|
||||||
|
background: rgba(0,0,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live Fixtures Grid */
|
||||||
|
.live-fixtures-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-fixtures-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-state {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-state.active {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-state.inactive {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-scoreboard {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-scoreboard .scoreboard {
|
||||||
|
margin: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Fixtures Message */
|
||||||
|
.no-fixtures {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-fixtures h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixtures Pagination */
|
||||||
|
.fixtures-pagination {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard.loading {
|
||||||
|
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid rgba(255,255,255,0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid #FFD700;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error States */
|
||||||
|
.scoreboard.error {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin Controls */
|
||||||
|
.scoreboard-controls {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-controls {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-controls h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finished-message {
|
||||||
|
padding: 15px;
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Team Controls */
|
||||||
|
.team-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-control-section {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-control-section:first-child {
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-control-section h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-buttons .btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-buttons .btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Timeline */
|
||||||
|
.event-timeline {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-timeline h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
top: 10px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #007bff;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid white;
|
||||||
|
box-shadow: 0 0 0 2px #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item.match-event::before {
|
||||||
|
background: #28a745;
|
||||||
|
box-shadow: 0 0 0 2px #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content.away {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 8px;
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: -60px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: -30px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.yellow-card {
|
||||||
|
background: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon.red-card {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scoreboard .border {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-container {
|
||||||
|
padding: 0;
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
font-size: 36px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-fixtures-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-control-section:first-child {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-buttons {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item::before {
|
||||||
|
left: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-panel {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.scoreboard {
|
||||||
|
margin: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard .border {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
padding: 10px;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
margin-left: 0;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.half-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-fixtures-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixture-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard-controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-controls, .team-control-section, .event-timeline {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons .btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-control-section h5 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoring-buttons .btn, .card-buttons .btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin-specific styles */
|
||||||
|
.wp-admin .scoreboard {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-admin .scoreboard-controls {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.scoreboard-controls,
|
||||||
|
.delete-btn,
|
||||||
|
.control-buttons {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreboard {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 2px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer, .half-indicator {
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +0,0 @@
|
|||||||
jQuery(document).ready(function ($) {
|
|
||||||
fetch(LiveScoreAjax.templatesUrl + 'live-score.html')
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(html => {
|
|
||||||
const container = $(html).appendTo('[data-live-score-shortcode]');
|
|
||||||
function fetchLiveScore() {
|
|
||||||
$.get(LiveScoreAjax.ajaxUrl, { action: 'get_live_score' }, function (res) {
|
|
||||||
if (res.success) {
|
|
||||||
const score = res.data.score;
|
|
||||||
$('#live-score-content').html(`
|
|
||||||
<strong>${res.data.event_title}</strong><br>
|
|
||||||
<span>Home: ${score.home} | Away: ${score.away}</span>
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
$('#live-score-content').html('<em>No live event at the moment.</em>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchLiveScore();
|
|
||||||
setInterval(fetchLiveScore, 10000); // Poll every 10 seconds
|
|
||||||
});
|
|
||||||
});
|
|
||||||
613
js/live-scoreboard.js
Normal file
613
js/live-scoreboard.js
Normal file
@ -0,0 +1,613 @@
|
|||||||
|
/**
|
||||||
|
* Live Scoreboard JavaScript
|
||||||
|
* Handles both single scoreboards and live fixtures with admin controls
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LiveScoreboard {
|
||||||
|
constructor(container, config) {
|
||||||
|
this.container = container;
|
||||||
|
this.eventId = config.eventId;
|
||||||
|
this.ajaxUrl = config.ajaxUrl;
|
||||||
|
this.nonce = config.nonce;
|
||||||
|
this.autoUpdate = config.autoUpdate;
|
||||||
|
this.updateInterval = config.updateInterval || 5000;
|
||||||
|
this.maxFixtures = config.maxFixtures || 10;
|
||||||
|
this.showFinished = config.showFinished || false;
|
||||||
|
this.showControls = config.showControls || false;
|
||||||
|
this.canEdit = config.canEdit || false;
|
||||||
|
this.intervalId = null;
|
||||||
|
this.lastUpdateTime = 0;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.maxRetries = 3;
|
||||||
|
|
||||||
|
// Determine mode from container data or config
|
||||||
|
this.mode = this.container.getAttribute('data-mode') || 'single';
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.showLoading();
|
||||||
|
|
||||||
|
if (this.mode === 'fixtures') {
|
||||||
|
this.fetchLiveFixtures();
|
||||||
|
} else {
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.autoUpdate) {
|
||||||
|
this.startAutoUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="scoreboard loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div>Loading ${this.mode === 'fixtures' ? 'live fixtures' : 'scoreboard'}...</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLiveFixtures() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'get_live_fixtures');
|
||||||
|
formData.append('max_fixtures', this.maxFixtures);
|
||||||
|
formData.append('show_finished', this.showFinished);
|
||||||
|
|
||||||
|
fetch(this.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
this.updateLiveFixtures(data.data);
|
||||||
|
this.lastUpdateTime = data.data.timestamp || Date.now();
|
||||||
|
this.retryCount = 0;
|
||||||
|
} else {
|
||||||
|
this.handleError('Failed to load live fixtures');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('AJAX error:', error);
|
||||||
|
this.handleError('Connection error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchScoreboardData() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'get_scoreboard_data');
|
||||||
|
formData.append('event_id', this.eventId);
|
||||||
|
formData.append('last_update', this.lastUpdateTime);
|
||||||
|
|
||||||
|
fetch(this.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.data) {
|
||||||
|
this.updateScoreboard(data.data);
|
||||||
|
this.lastUpdateTime = data.data.timestamp || Date.now();
|
||||||
|
this.retryCount = 0;
|
||||||
|
} else {
|
||||||
|
this.handleError('Failed to load scoreboard data');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('AJAX error:', error);
|
||||||
|
this.handleError('Connection error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLiveFixtures(data) {
|
||||||
|
if (!data.fixtures || data.fixtures.length === 0) {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="no-fixtures">
|
||||||
|
<h3>No Live Fixtures</h3>
|
||||||
|
<p>There are currently no active matches to display.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="live-fixtures-grid">';
|
||||||
|
|
||||||
|
data.fixtures.forEach(fixture => {
|
||||||
|
html += this.generateFixtureCard(fixture);
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (data.total_count > this.maxFixtures) {
|
||||||
|
html += `<div class="fixtures-pagination">Showing ${this.maxFixtures} of ${data.total_count} active fixtures</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFixtureCard(fixture) {
|
||||||
|
const gameStateClass = this.getGameStateClass(fixture.game_state);
|
||||||
|
const isActive = fixture.game_on;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="fixture-card ${gameStateClass}" data-event-id="${fixture.event_id}">
|
||||||
|
<div class="fixture-header">
|
||||||
|
<div class="fixture-title">${this.escapeHtml(fixture.event_title || 'Match')}</div>
|
||||||
|
<div class="fixture-state ${isActive ? 'active' : 'inactive'}">${fixture.half_indicator}</div>
|
||||||
|
</div>
|
||||||
|
<div class="fixture-scoreboard">
|
||||||
|
${this.generateScoreboardHTML(fixture, true)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScoreboard(data) {
|
||||||
|
let html = this.generateScoreboardHTML(data, false);
|
||||||
|
|
||||||
|
// Add admin controls if user has permissions
|
||||||
|
if (this.showControls && this.canEdit) {
|
||||||
|
html += this.generateControlsHTML(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.innerHTML = html;
|
||||||
|
|
||||||
|
// Bind control events if controls are shown
|
||||||
|
if (this.showControls && this.canEdit) {
|
||||||
|
this.bindControlEvents(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animateScoreChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateScoreboardHTML(data, isCompact = false) {
|
||||||
|
const compactClass = isCompact ? 'compact' : '';
|
||||||
|
const gameStateClass = this.getGameStateClass(data.game_state);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="scoreboard ${compactClass} ${gameStateClass} ${data.game_on ? 'game-on' : 'game-off'}">
|
||||||
|
<div class="border">
|
||||||
|
<div class="team home-team" style="background-color: ${data.home_team_color || '#1e3c72'}">
|
||||||
|
<div class="team-info">
|
||||||
|
${!isCompact ? `<div class="team-logo">
|
||||||
|
<img src="${data.home_team_logo || this.getDefaultLogo('H')}"
|
||||||
|
alt="${data.home_team_name} Logo"
|
||||||
|
onerror="this.src='${this.getDefaultLogo('H')}'">
|
||||||
|
</div>` : ''}
|
||||||
|
<div class="team-name">${this.escapeHtml(data.home_team_name || 'Home Team')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="team-score" data-score="${data.home_team_points || 0}">${data.home_team_points || 0}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="timer-container">
|
||||||
|
<div class="timer ${data.game_on ? 'active' : 'inactive'}">${data.timer_value || '00:00'}</div>
|
||||||
|
<div class="half-indicator">${this.escapeHtml(data.half_indicator || 'Pre-Match')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team away-team" style="background-color: ${data.away_team_color || '#2a5298'}">
|
||||||
|
<div class="team-info">
|
||||||
|
${!isCompact ? `<div class="team-logo">
|
||||||
|
<img src="${data.away_team_logo || this.getDefaultLogo('A')}"
|
||||||
|
alt="${data.away_team_name} Logo"
|
||||||
|
onerror="this.src='${this.getDefaultLogo('A')}'">
|
||||||
|
</div>` : ''}
|
||||||
|
<div class="team-name">${this.escapeHtml(data.away_team_name || 'Away Team')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="team-score" data-score="${data.away_team_points || 0}">${data.away_team_points || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateControlsHTML(data) {
|
||||||
|
const gameState = data.game_state || 'WaitingForStart';
|
||||||
|
|
||||||
|
let controlsHTML = `
|
||||||
|
<div class="scoreboard-controls">
|
||||||
|
<div class="match-controls">
|
||||||
|
<h4>Match Controls</h4>
|
||||||
|
<div class="control-buttons">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Game state controls based on current state
|
||||||
|
switch (gameState) {
|
||||||
|
case 'WaitingForStart':
|
||||||
|
controlsHTML += `<button class="btn btn-primary" data-action="start-match">Start Match</button>`;
|
||||||
|
break;
|
||||||
|
case 'FirstHalf':
|
||||||
|
controlsHTML += `
|
||||||
|
<button class="btn btn-warning" data-action="time-off">Time Off</button>
|
||||||
|
<button class="btn btn-secondary" data-action="half-time">Half Time</button>`;
|
||||||
|
break;
|
||||||
|
case 'TimeOff':
|
||||||
|
controlsHTML += `<button class="btn btn-success" data-action="time-on">Time On</button>`;
|
||||||
|
break;
|
||||||
|
case 'HalfTime':
|
||||||
|
controlsHTML += `<button class="btn btn-primary" data-action="start-second-half">Start Second Half</button>`;
|
||||||
|
break;
|
||||||
|
case 'SecondHalf':
|
||||||
|
controlsHTML += `
|
||||||
|
<button class="btn btn-warning" data-action="time-off">Time Off</button>
|
||||||
|
<button class="btn btn-danger" data-action="finish-match">Finish Match</button>`;
|
||||||
|
break;
|
||||||
|
case 'Finished':
|
||||||
|
controlsHTML += `<div class="finished-message">Match has finished, no actions to perform.</div>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
controlsHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Team event controls (only show during active play)
|
||||||
|
if (gameState !== 'WaitingForStart' && gameState !== 'Finished') {
|
||||||
|
controlsHTML += `
|
||||||
|
<div class="team-controls">
|
||||||
|
<div class="team-control-section">
|
||||||
|
<h5>${data.home_team_name || 'Home Team'}</h5>
|
||||||
|
<div class="scoring-buttons">
|
||||||
|
<button class="btn btn-sm btn-success" data-action="add-event" data-team="Home" data-event="Try">Try</button>
|
||||||
|
<button class="btn btn-sm btn-info" data-action="add-event" data-team="Home" data-event="Conversion">Conversion</button>
|
||||||
|
<button class="btn btn-sm btn-primary" data-action="add-event" data-team="Home" data-event="Penalty">Penalty</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" data-action="add-event" data-team="Home" data-event="DropGoal">Drop Goal</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-buttons">
|
||||||
|
<button class="btn btn-sm btn-warning" data-action="add-event" data-team="Home" data-event="YellowCard">Yellow Card</button>
|
||||||
|
<button class="btn btn-sm btn-danger" data-action="add-event" data-team="Home" data-event="RedCard">Red Card</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team-control-section">
|
||||||
|
<h5>${data.away_team_name || 'Away Team'}</h5>
|
||||||
|
<div class="scoring-buttons">
|
||||||
|
<button class="btn btn-sm btn-success" data-action="add-event" data-team="Away" data-event="Try">Try</button>
|
||||||
|
<button class="btn btn-sm btn-info" data-action="add-event" data-team="Away" data-event="Conversion">Conversion</button>
|
||||||
|
<button class="btn btn-sm btn-primary" data-action="add-event" data-team="Away" data-event="Penalty">Penalty</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" data-action="add-event" data-team="Away" data-event="DropGoal">Drop Goal</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-buttons">
|
||||||
|
<button class="btn btn-sm btn-warning" data-action="add-event" data-team="Away" data-event="YellowCard">Yellow Card</button>
|
||||||
|
<button class="btn btn-sm btn-danger" data-action="add-event" data-team="Away" data-event="RedCard">Red Card</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event timeline
|
||||||
|
if (data.event_list && data.event_list.length > 0) {
|
||||||
|
controlsHTML += `
|
||||||
|
<div class="event-timeline">
|
||||||
|
<h4>Match Events</h4>
|
||||||
|
<div class="timeline">
|
||||||
|
`;
|
||||||
|
|
||||||
|
data.event_list.forEach((event, index) => {
|
||||||
|
const eventMessage = this.getEventMessage(event, data);
|
||||||
|
const scoreAtEvent = this.getScoreAtEvent(event.id, data.event_list);
|
||||||
|
|
||||||
|
controlsHTML += `
|
||||||
|
<div class="timeline-item ${event.category === 'Match' ? 'match-event' : 'team-event'}">
|
||||||
|
<div class="timeline-content ${event.category === 'Away' ? 'away' : ''}"
|
||||||
|
style="border-color: ${event.category === 'Home' ? data.home_team_color : data.away_team_color}">
|
||||||
|
${event.category !== 'Match' ? `<button class="delete-btn" data-action="delete-event" data-event-id="${event.id}">×</button>` : ''}
|
||||||
|
<p>${eventMessage}</p>
|
||||||
|
</div>
|
||||||
|
${!['YellowCard', 'RedCard'].includes(event.eventType) ? `<span class="score-panel">${scoreAtEvent}</span>` : ''}
|
||||||
|
${['YellowCard', 'RedCard'].includes(event.eventType) ? `<i class="card-icon ${event.eventType === 'YellowCard' ? 'yellow-card' : 'red-card'}"></i>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
controlsHTML += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
controlsHTML += `</div>`;
|
||||||
|
|
||||||
|
return controlsHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindControlEvents(data) {
|
||||||
|
// Bind all control button events
|
||||||
|
this.container.querySelectorAll('[data-action]').forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const action = button.getAttribute('data-action');
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'add-event':
|
||||||
|
const team = button.getAttribute('data-team');
|
||||||
|
const eventType = button.getAttribute('data-event');
|
||||||
|
this.addTeamEvent(team, eventType);
|
||||||
|
break;
|
||||||
|
case 'delete-event':
|
||||||
|
const eventId = button.getAttribute('data-event-id');
|
||||||
|
this.deleteEvent(eventId);
|
||||||
|
break;
|
||||||
|
case 'start-match':
|
||||||
|
this.updateGameState('FirstHalf');
|
||||||
|
break;
|
||||||
|
case 'time-off':
|
||||||
|
this.updateGameState('TimeOff');
|
||||||
|
break;
|
||||||
|
case 'time-on':
|
||||||
|
this.updateGameState(data.game_state === 'FirstHalf' ? 'FirstHalf' : 'SecondHalf');
|
||||||
|
break;
|
||||||
|
case 'half-time':
|
||||||
|
this.updateGameState('HalfTime');
|
||||||
|
break;
|
||||||
|
case 'start-second-half':
|
||||||
|
this.updateGameState('SecondHalf');
|
||||||
|
break;
|
||||||
|
case 'finish-match':
|
||||||
|
this.updateGameState('Finished');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addTeamEvent(team, eventType) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'add_team_event');
|
||||||
|
formData.append('nonce', this.nonce);
|
||||||
|
formData.append('event_id', this.eventId);
|
||||||
|
formData.append('team', team);
|
||||||
|
formData.append('event_type', eventType);
|
||||||
|
|
||||||
|
fetch(this.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Refresh scoreboard immediately
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to add event:', data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error adding event:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteEvent(eventId) {
|
||||||
|
if (!confirm('Are you sure you want to delete this event?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'delete_event');
|
||||||
|
formData.append('nonce', this.nonce);
|
||||||
|
formData.append('event_id', this.eventId);
|
||||||
|
formData.append('match_event_id', eventId);
|
||||||
|
|
||||||
|
fetch(this.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to delete event:', data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting event:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGameState(newState) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'update_game_state');
|
||||||
|
formData.append('nonce', this.nonce);
|
||||||
|
formData.append('event_id', this.eventId);
|
||||||
|
formData.append('game_state', newState);
|
||||||
|
|
||||||
|
fetch(this.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update game state:', data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error updating game state:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventMessage(event, data) {
|
||||||
|
const teamName = event.category === 'Home' ? (data.home_team_name || 'Home') : (data.away_team_name || 'Away');
|
||||||
|
const minute = event.minute || '00:00';
|
||||||
|
|
||||||
|
switch (event.eventType) {
|
||||||
|
case 'Try':
|
||||||
|
return `${minute} - ${teamName} scores a try!`;
|
||||||
|
case 'Conversion':
|
||||||
|
return `${minute} - ${teamName} converts the try`;
|
||||||
|
case 'Penalty':
|
||||||
|
return `${minute} - ${teamName} scores a penalty`;
|
||||||
|
case 'DropGoal':
|
||||||
|
return `${minute} - ${teamName} scores a drop goal`;
|
||||||
|
case 'YellowCard':
|
||||||
|
return `${minute} - ${teamName} receives a yellow card`;
|
||||||
|
case 'RedCard':
|
||||||
|
return `${minute} - ${teamName} receives a red card`;
|
||||||
|
default:
|
||||||
|
return `${minute} - ${teamName} ${event.eventType}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getScoreAtEvent(eventId, eventList) {
|
||||||
|
let homeScore = 0;
|
||||||
|
let awayScore = 0;
|
||||||
|
|
||||||
|
for (let event of eventList) {
|
||||||
|
// Calculate score up to this event
|
||||||
|
if (event.category === 'Home') {
|
||||||
|
homeScore += this.getEventPoints(event.eventType);
|
||||||
|
} else if (event.category === 'Away') {
|
||||||
|
awayScore += this.getEventPoints(event.eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop when we reach the target event
|
||||||
|
if (event.id === eventId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${homeScore} - ${awayScore}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEventPoints(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'Try': return 5;
|
||||||
|
case 'Conversion': return 2;
|
||||||
|
case 'Penalty': return 3;
|
||||||
|
case 'DropGoal': return 3;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameStateClass(gameState) {
|
||||||
|
return `state-${(gameState || 'waiting').toLowerCase().replace(/([A-Z])/g, '-$1')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultLogo(letter) {
|
||||||
|
return `https://via.placeholder.com/60x60/cccccc/ffffff?text=${letter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(message) {
|
||||||
|
this.retryCount++;
|
||||||
|
|
||||||
|
if (this.retryCount <= this.maxRetries) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.mode === 'fixtures') {
|
||||||
|
this.fetchLiveFixtures();
|
||||||
|
} else {
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="scoreboard error">
|
||||||
|
<div class="loading">
|
||||||
|
<div style="color: #ff6b6b;">${message}</div>
|
||||||
|
<div style="font-size: 14px; margin-top: 10px;">Retrying... (${this.retryCount}/${this.maxRetries})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="scoreboard error">
|
||||||
|
<div class="loading">
|
||||||
|
<div style="color: #ff6b6b;">${message}</div>
|
||||||
|
<div style="font-size: 14px; margin-top: 10px;">Maximum retries reached</div>
|
||||||
|
<button onclick="location.reload()" style="margin-top: 10px; padding: 5px 10px;">Reload Page</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animateScoreChanges() {
|
||||||
|
// Add subtle animation to score changes
|
||||||
|
this.container.querySelectorAll('.team-score').forEach(scoreElement => {
|
||||||
|
const currentScore = parseInt(scoreElement.textContent);
|
||||||
|
const lastScore = parseInt(scoreElement.getAttribute('data-last-score') || '0');
|
||||||
|
|
||||||
|
if (currentScore !== lastScore) {
|
||||||
|
scoreElement.classList.add('score-changed');
|
||||||
|
setTimeout(() => {
|
||||||
|
scoreElement.classList.remove('score-changed');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreElement.setAttribute('data-last-score', currentScore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoUpdate() {
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
if (this.mode === 'fixtures') {
|
||||||
|
this.fetchLiveFixtures();
|
||||||
|
} else {
|
||||||
|
this.fetchScoreboardData();
|
||||||
|
}
|
||||||
|
}, this.updateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoUpdate() {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopAutoUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize scoreboards when document is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize single scoreboards
|
||||||
|
document.querySelectorAll('.live-scoreboard-container').forEach(container => {
|
||||||
|
const eventId = container.getAttribute('data-event-id');
|
||||||
|
const showControls = container.getAttribute('data-show-controls') === 'true';
|
||||||
|
|
||||||
|
if (eventId && window.scoreboardConfig) {
|
||||||
|
const config = Object.assign({}, window.scoreboardConfig, {
|
||||||
|
eventId: eventId,
|
||||||
|
showControls: showControls
|
||||||
|
});
|
||||||
|
|
||||||
|
new LiveScoreboard(container, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize live fixtures containers
|
||||||
|
document.querySelectorAll('.live-fixtures-container').forEach(container => {
|
||||||
|
const maxFixtures = parseInt(container.getAttribute('data-max-fixtures')) || 10;
|
||||||
|
const showFinished = container.getAttribute('data-show-finished') === 'true';
|
||||||
|
|
||||||
|
if (window.scoreboardConfig) {
|
||||||
|
const config = Object.assign({}, window.scoreboardConfig, {
|
||||||
|
maxFixtures: maxFixtures,
|
||||||
|
showFinished: showFinished
|
||||||
|
});
|
||||||
|
|
||||||
|
new LiveScoreboard(container, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up on page unload
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
// Cleanup will be handled by individual instances
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for global access
|
||||||
|
window.LiveScoreboard = LiveScoreboard;
|
||||||
688
scoreboard.php
688
scoreboard.php
@ -1,145 +1,595 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/*
|
||||||
* Plugin Name: Sportspress Scoreboard Extension
|
Plugin Name: Live Rugby Union Sportspress Scoreboard
|
||||||
* Description: Adds a live scoreboard with edit controls for users with permission.
|
Plugin URI: http://wattsyproductions.co.uk:23000/Chris/ScoreboardPlugin
|
||||||
* Version: 1.0
|
Description: Adds live scoreboard for Rugby Union events
|
||||||
*/
|
Version: 1.0
|
||||||
|
Author: Chris Watts
|
||||||
|
*/
|
||||||
|
|
||||||
add_action('init', function() {
|
class LiveScoreboardShortcode {
|
||||||
add_role('score_editor', 'Score Editor', ['sportspress_can_control_score' => true]);
|
|
||||||
});
|
|
||||||
|
|
||||||
register_activation_hook(__FILE__, function() {
|
public function __construct() {
|
||||||
$role = get_role('administrator');
|
add_shortcode('live_scoreboard', array($this, 'render_scoreboard'));
|
||||||
if ($role) {
|
add_shortcode('live_fixtures', array($this, 'render_live_fixtures'));
|
||||||
$role->add_cap('sportspress_can_control_score');
|
|
||||||
|
// Public endpoints (no auth required)
|
||||||
|
add_action('wp_ajax_get_scoreboard_data', array($this, 'ajax_get_scoreboard_data'));
|
||||||
|
add_action('wp_ajax_nopriv_get_scoreboard_data', array($this, 'ajax_get_scoreboard_data'));
|
||||||
|
add_action('wp_ajax_get_live_fixtures', array($this, 'ajax_get_live_fixtures'));
|
||||||
|
add_action('wp_ajax_nopriv_get_live_fixtures', array($this, 'ajax_get_live_fixtures'));
|
||||||
|
|
||||||
|
// Admin-only endpoints
|
||||||
|
add_action('wp_ajax_update_scoreboard_data', array($this, 'ajax_update_scoreboard_data'));
|
||||||
|
add_action('wp_ajax_add_team_event', array($this, 'ajax_add_team_event'));
|
||||||
|
add_action('wp_ajax_delete_event', array($this, 'ajax_delete_event'));
|
||||||
|
add_action('wp_ajax_update_game_state', array($this, 'ajax_update_game_state'));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
register_deactivation_hook(__FILE__, function() {
|
/**
|
||||||
$role = get_role('administrator');
|
* Render single scoreboard shortcode
|
||||||
if ($role) {
|
* Usage: [live_scoreboard event_id="123" auto_update="true" update_interval="5000"]
|
||||||
$role->remove_cap('sportspress_can_control_score');
|
*/
|
||||||
|
public function render_scoreboard($atts) {
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'event_id' => '',
|
||||||
|
'auto_update' => 'true',
|
||||||
|
'update_interval' => '5000',
|
||||||
|
'width' => '100%',
|
||||||
|
'height' => 'auto',
|
||||||
|
'show_controls' => 'false'
|
||||||
|
), $atts);
|
||||||
|
|
||||||
|
if (empty($atts['event_id'])) {
|
||||||
|
return '<div class="scoreboard-error">Error: Event ID is required for the scoreboard.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = get_post($atts['event_id']);
|
||||||
|
if (!$event || $event->post_type !== 'sp_event') {
|
||||||
|
return '<div class="scoreboard-error">Error: Invalid event ID or not a SportsPress event.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->enqueue_scoreboard_assets($atts);
|
||||||
|
$container_id = 'scoreboard-' . $atts['event_id'] . '-' . uniqid();
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<div id="%s" class="live-scoreboard-container" data-event-id="%s" data-mode="single" data-show-controls="%s" style="width: %s; height: %s;"></div>',
|
||||||
|
esc_attr($container_id),
|
||||||
|
esc_attr($atts['event_id']),
|
||||||
|
esc_attr($atts['show_controls']),
|
||||||
|
esc_attr($atts['width']),
|
||||||
|
esc_attr($atts['height'])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Add meta box for scoreboard timeline
|
/**
|
||||||
add_action('add_meta_boxes', function() {
|
* Render live fixtures shortcode (all active matches)
|
||||||
add_meta_box('scoreboard_timeline', 'Scoreboard Timeline', 'render_scoreboard_meta_box', 'sp_event');
|
* Usage: [live_fixtures auto_update="true" update_interval="10000" max_fixtures="5"]
|
||||||
});
|
*/
|
||||||
|
public function render_live_fixtures($atts) {
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'auto_update' => 'true',
|
||||||
|
'update_interval' => '10000',
|
||||||
|
'max_fixtures' => '10',
|
||||||
|
'width' => '100%',
|
||||||
|
'show_finished' => 'false'
|
||||||
|
), $atts);
|
||||||
|
|
||||||
function render_scoreboard_meta_box($post) {
|
$this->enqueue_scoreboard_assets($atts);
|
||||||
$json = get_post_meta($post->ID, '_scoreboard_timeline', true);
|
$container_id = 'live-fixtures-' . uniqid();
|
||||||
$json = $json ? esc_textarea($json) : '{}';
|
|
||||||
echo '<textarea style="width:100%;height:200px;" name="scoreboard_timeline_json">' . $json . '</textarea>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save scoreboard meta
|
return sprintf(
|
||||||
add_action('save_post_sp_event', function($post_id) {
|
'<div id="%s" class="live-fixtures-container" data-mode="fixtures" data-max-fixtures="%s" data-show-finished="%s" style="width: %s;"></div>',
|
||||||
if (isset($_POST['scoreboard_timeline_json'])) {
|
esc_attr($container_id),
|
||||||
update_post_meta($post_id, '_scoreboard_timeline', wp_unslash($_POST['scoreboard_timeline_json']));
|
esc_attr($atts['max_fixtures']),
|
||||||
|
esc_attr($atts['show_finished']),
|
||||||
|
esc_attr($atts['width'])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
add_action('wp_enqueue_scripts', function() {
|
/**
|
||||||
if (is_singular('sp_event')) {
|
* Enqueue necessary scripts and styles
|
||||||
wp_enqueue_script('scoreboard-js', plugins_url('/js/scoreboard.js', __FILE__), ['jquery'], null, true);
|
*/
|
||||||
wp_enqueue_script('scoreboard-controls-js', plugins_url('/js/scoreboard-controls.js', __FILE__), ['jquery'], null, true);
|
private function enqueue_scoreboard_assets($atts) {
|
||||||
|
wp_enqueue_script('jquery');
|
||||||
|
|
||||||
wp_localize_script('scoreboard-js', 'scoreboardData', [
|
wp_enqueue_script(
|
||||||
|
'live-scoreboard-js',
|
||||||
|
plugin_dir_url(__FILE__) . 'js/live-scoreboard.js',
|
||||||
|
array('jquery'),
|
||||||
|
'1.0.1',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'live-scoreboard-css',
|
||||||
|
plugin_dir_url(__FILE__) . 'css/live-scoreboard.css',
|
||||||
|
array(),
|
||||||
|
'1.0.1'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Localize script with AJAX data
|
||||||
|
wp_localize_script('live-scoreboard-js', 'scoreboardConfig', array(
|
||||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
'eventId' => get_the_ID(),
|
'nonce' => wp_create_nonce('scoreboard_nonce'),
|
||||||
'canEdit' => current_user_can('sportspress_can_control_score'),
|
'eventId' => isset($atts['event_id']) ? $atts['event_id'] : '',
|
||||||
'timeline' => get_post_meta(get_the_ID(), '_scoreboard_timeline', true) ?: '{}',
|
'autoUpdate' => $atts['auto_update'] === 'true',
|
||||||
'templatesUrl' => plugins_url('/html/', __FILE__)
|
'updateInterval' => intval($atts['update_interval']),
|
||||||
]);
|
'maxFixtures' => isset($atts['max_fixtures']) ? intval($atts['max_fixtures']) : 10,
|
||||||
}
|
'showFinished' => isset($atts['show_finished']) ? ($atts['show_finished'] === 'true') : false,
|
||||||
});
|
'showControls' => isset($atts['show_controls']) ? ($atts['show_controls'] === 'true') : false,
|
||||||
// AJAX endpoint for saving scoreboard
|
'canEdit' => current_user_can('manage_scoreboard')
|
||||||
add_action('wp_ajax_update_scoreboard', function() {
|
));
|
||||||
if (!current_user_can('sportspress_can_control_score')) {
|
|
||||||
wp_send_json_error('Unauthorized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$event_id = intval($_POST['event_id']);
|
/**
|
||||||
$timeline = wp_unslash($_POST['timeline']);
|
* AJAX handler to get live fixtures data
|
||||||
update_post_meta($event_id, '_scoreboard_timeline', $timeline);
|
*/
|
||||||
|
public function ajax_get_live_fixtures() {
|
||||||
|
$max_fixtures = isset($_POST['max_fixtures']) ? intval($_POST['max_fixtures']) : 10;
|
||||||
|
$show_finished = isset($_POST['show_finished']) ? (bool)$_POST['show_finished'] : false;
|
||||||
|
|
||||||
wp_send_json_success();
|
$fixtures_data = $this->get_live_fixtures_data($max_fixtures, $show_finished);
|
||||||
});
|
|
||||||
|
|
||||||
add_shortcode('live_score', function($atts) {
|
if ($fixtures_data !== false) {
|
||||||
ob_start();
|
wp_send_json_success($fixtures_data);
|
||||||
?>
|
} else {
|
||||||
<div id="live-score-widget" data-refresh="1">
|
wp_send_json_error('Failed to retrieve fixtures data');
|
||||||
<strong>Loading live score...</strong>
|
}
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
return ob_get_clean();
|
|
||||||
});
|
|
||||||
|
|
||||||
add_action('wp_ajax_nopriv_get_live_score', 'get_live_score_callback');
|
|
||||||
add_action('wp_ajax_get_live_score', 'get_live_score_callback');
|
|
||||||
|
|
||||||
function get_live_score_callback() {
|
|
||||||
$args = [
|
|
||||||
'post_type' => 'sp_event',
|
|
||||||
'posts_per_page' => 1,
|
|
||||||
'meta_query' => [
|
|
||||||
[
|
|
||||||
'key' => 'is_live',
|
|
||||||
'value' => '1',
|
|
||||||
'compare' => '='
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$live_event = get_posts($args);
|
|
||||||
if (!$live_event) {
|
|
||||||
wp_send_json_error('No live event');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$event = $live_event[0];
|
/**
|
||||||
$timeline = get_post_meta($event->ID, '_scoreboard_timeline', true);
|
* AJAX handler to get single scoreboard data
|
||||||
$timeline_data = json_decode($timeline, true);
|
*/
|
||||||
|
public function ajax_get_scoreboard_data() {
|
||||||
|
$event_id = intval($_POST['event_id']);
|
||||||
|
$last_update = isset($_POST['last_update']) ? intval($_POST['last_update']) : 0;
|
||||||
|
|
||||||
// Simplified for display: extract latest score entry
|
if (!$event_id) {
|
||||||
$latest = end($timeline_data);
|
wp_send_json_error('Invalid event ID');
|
||||||
|
}
|
||||||
|
|
||||||
wp_send_json_success([
|
$scoreboard_data = $this->get_scoreboard_data($event_id);
|
||||||
'event_id' => $event->ID,
|
|
||||||
'event_title' => get_the_title($event),
|
if ($scoreboard_data) {
|
||||||
'score' => $latest ?? ['home' => 0, 'away' => 0]
|
wp_send_json_success($scoreboard_data);
|
||||||
]);
|
} else {
|
||||||
|
wp_send_json_error('Failed to retrieve scoreboard data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get live fixtures data
|
||||||
|
*/
|
||||||
|
private function get_live_fixtures_data($max_fixtures = 10, $show_finished = false) {
|
||||||
|
// Query for SportsPress events
|
||||||
|
$meta_query = array(
|
||||||
|
array(
|
||||||
|
'key' => '_scoreboard_data',
|
||||||
|
'compare' => 'EXISTS'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$show_finished) {
|
||||||
|
$meta_query[] = array(
|
||||||
|
'key' => '_game_state',
|
||||||
|
'value' => 'Finished',
|
||||||
|
'compare' => '!='
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = get_posts(array(
|
||||||
|
'post_type' => 'sp_event',
|
||||||
|
'posts_per_page' => $max_fixtures,
|
||||||
|
'meta_query' => $meta_query,
|
||||||
|
'orderby' => 'meta_value',
|
||||||
|
'meta_key' => '_scoreboard_last_update',
|
||||||
|
'order' => 'DESC'
|
||||||
|
));
|
||||||
|
|
||||||
|
$fixtures = array();
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$scoreboard_data = $this->get_scoreboard_data($event->ID);
|
||||||
|
if ($scoreboard_data && ($show_finished || $scoreboard_data['game_state'] !== 'Finished')) {
|
||||||
|
$scoreboard_data['event_id'] = $event->ID;
|
||||||
|
$scoreboard_data['event_title'] = $event->post_title;
|
||||||
|
$fixtures[] = $scoreboard_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'fixtures' => $fixtures,
|
||||||
|
'timestamp' => time(),
|
||||||
|
'total_count' => count($fixtures)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get scoreboard data from event metadata
|
||||||
|
*/
|
||||||
|
private function get_scoreboard_data($event_id) {
|
||||||
|
$event = get_post($event_id);
|
||||||
|
if (!$event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SportsPress event data
|
||||||
|
$teams = get_post_meta($event_id, 'sp_team', false);
|
||||||
|
$results = get_post_meta($event_id, 'sp_results', true);
|
||||||
|
|
||||||
|
// Get custom scoreboard metadata
|
||||||
|
$scoreboard_meta = get_post_meta($event_id, '_scoreboard_data', true);
|
||||||
|
if (!is_array($scoreboard_meta)) {
|
||||||
|
$scoreboard_meta = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get game state and events
|
||||||
|
$game_state = get_post_meta($event_id, '_game_state', true) ?: 'WaitingForStart';
|
||||||
|
$event_list = get_post_meta($event_id, '_match_events', true);
|
||||||
|
if (!is_array($event_list)) {
|
||||||
|
$event_list = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default scoreboard data structure
|
||||||
|
$default_data = array(
|
||||||
|
'game_on' => false,
|
||||||
|
'game_state' => $game_state,
|
||||||
|
'timer_value' => '00:00',
|
||||||
|
'half_indicator' => $this->get_half_indicator($game_state),
|
||||||
|
'home_team_points' => 0,
|
||||||
|
'away_team_points' => 0,
|
||||||
|
'event_list' => $event_list,
|
||||||
|
'timestamp' => time()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge with saved metadata
|
||||||
|
$scoreboard_data = array_merge($default_data, $scoreboard_meta);
|
||||||
|
|
||||||
|
// Calculate if game is on
|
||||||
|
$scoreboard_data['game_on'] = in_array($game_state, array('FirstHalf', 'SecondHalf', 'TimeOff'));
|
||||||
|
|
||||||
|
// Get team information from SportsPress
|
||||||
|
if (!empty($teams)) {
|
||||||
|
$home_team_id = isset($teams[0]) ? $teams[0] : null;
|
||||||
|
$away_team_id = isset($teams[1]) ? $teams[1] : null;
|
||||||
|
|
||||||
|
if ($home_team_id) {
|
||||||
|
$scoreboard_data['home_team_name'] = get_the_title($home_team_id);
|
||||||
|
$scoreboard_data['home_team_logo'] = $this->get_team_logo($home_team_id);
|
||||||
|
$scoreboard_data['home_team_color'] = get_post_meta($home_team_id, '_team_color', true) ?: '#1e3c72';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($away_team_id) {
|
||||||
|
$scoreboard_data['away_team_name'] = get_the_title($away_team_id);
|
||||||
|
$scoreboard_data['away_team_logo'] = $this->get_team_logo($away_team_id);
|
||||||
|
$scoreboard_data['away_team_color'] = get_post_meta($away_team_id, '_team_color', true) ?: '#2a5298';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate points from events
|
||||||
|
$scores = $this->calculate_scores_from_events($event_list);
|
||||||
|
$scoreboard_data['home_team_points'] = $scores['home'];
|
||||||
|
$scoreboard_data['away_team_points'] = $scores['away'];
|
||||||
|
|
||||||
|
return $scoreboard_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate scores from match events
|
||||||
|
*/
|
||||||
|
private function calculate_scores_from_events($events) {
|
||||||
|
$scores = array('home' => 0, 'away' => 0);
|
||||||
|
|
||||||
|
foreach ($events as $event) {
|
||||||
|
if (!isset($event['category']) || !isset($event['eventType'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$team = strtolower($event['category']);
|
||||||
|
if (!in_array($team, array('home', 'away'))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($event['eventType']) {
|
||||||
|
case 'Try':
|
||||||
|
$scores[$team] += 5;
|
||||||
|
break;
|
||||||
|
case 'Conversion':
|
||||||
|
$scores[$team] += 2;
|
||||||
|
break;
|
||||||
|
case 'Penalty':
|
||||||
|
case 'DropGoal':
|
||||||
|
$scores[$team] += 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scores;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get half indicator text based on game state
|
||||||
|
*/
|
||||||
|
private function get_half_indicator($game_state) {
|
||||||
|
switch ($game_state) {
|
||||||
|
case 'WaitingForStart':
|
||||||
|
return 'Pre-Match';
|
||||||
|
case 'FirstHalf':
|
||||||
|
return '1st Half';
|
||||||
|
case 'HalfTime':
|
||||||
|
return 'Half Time';
|
||||||
|
case 'SecondHalf':
|
||||||
|
return '2nd Half';
|
||||||
|
case 'TimeOff':
|
||||||
|
return 'Time Off';
|
||||||
|
case 'Finished':
|
||||||
|
return 'Full Time';
|
||||||
|
default:
|
||||||
|
return 'Pre-Match';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler to add team event (admin only)
|
||||||
|
*/
|
||||||
|
public function ajax_add_team_event() {
|
||||||
|
if (!current_user_can('manage_scoreboard')) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'], 'scoreboard_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$event_id = intval($_POST['event_id']);
|
||||||
|
$team = sanitize_text_field($_POST['team']);
|
||||||
|
$event_type = sanitize_text_field($_POST['event_type']);
|
||||||
|
|
||||||
|
if (!$event_id || !$team || !$event_type) {
|
||||||
|
wp_send_json_error('Invalid data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->add_team_event($event_id, $team, $event_type);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Event added successfully');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Failed to add event');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add team event to match
|
||||||
|
*/
|
||||||
|
private function add_team_event($event_id, $team, $event_type) {
|
||||||
|
$events = get_post_meta($event_id, '_match_events', true);
|
||||||
|
if (!is_array($events)) {
|
||||||
|
$events = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_event = array(
|
||||||
|
'id' => uniqid(),
|
||||||
|
'category' => $team,
|
||||||
|
'eventType' => $event_type,
|
||||||
|
'timestamp' => time(),
|
||||||
|
'minute' => $this->get_current_match_minute($event_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
$events[] = $new_event;
|
||||||
|
|
||||||
|
$result = update_post_meta($event_id, '_match_events', $events);
|
||||||
|
|
||||||
|
// Update last modified timestamp
|
||||||
|
update_post_meta($event_id, '_scoreboard_last_update', time());
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current match minute (placeholder - you'd implement based on your timer logic)
|
||||||
|
*/
|
||||||
|
private function get_current_match_minute($event_id) {
|
||||||
|
// This would calculate based on your match start time and current time
|
||||||
|
// For now, return a placeholder
|
||||||
|
return '00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler to delete event (admin only)
|
||||||
|
*/
|
||||||
|
public function ajax_delete_event() {
|
||||||
|
if (!current_user_can('manage_scoreboard')) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'], 'scoreboard_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$event_id = intval($_POST['event_id']);
|
||||||
|
$match_event_id = sanitize_text_field($_POST['match_event_id']);
|
||||||
|
|
||||||
|
if (!$event_id || !$match_event_id) {
|
||||||
|
wp_send_json_error('Invalid data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->delete_match_event($event_id, $match_event_id);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Event deleted successfully');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Failed to delete event');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a specific match event
|
||||||
|
*/
|
||||||
|
private function delete_match_event($event_id, $match_event_id) {
|
||||||
|
$events = get_post_meta($event_id, '_match_events', true);
|
||||||
|
if (!is_array($events)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated_events = array_filter($events, function($event) use ($match_event_id) {
|
||||||
|
return $event['id'] !== $match_event_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
$result = update_post_meta($event_id, '_match_events', array_values($updated_events));
|
||||||
|
|
||||||
|
// Update last modified timestamp
|
||||||
|
update_post_meta($event_id, '_scoreboard_last_update', time());
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler to update game state (admin only)
|
||||||
|
*/
|
||||||
|
public function ajax_update_game_state() {
|
||||||
|
if (!current_user_can('manage_scoreboard')) {
|
||||||
|
wp_send_json_error('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_verify_nonce($_POST['nonce'], 'scoreboard_nonce')) {
|
||||||
|
wp_send_json_error('Security check failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$event_id = intval($_POST['event_id']);
|
||||||
|
$new_state = sanitize_text_field($_POST['game_state']);
|
||||||
|
|
||||||
|
if (!$event_id || !$new_state) {
|
||||||
|
wp_send_json_error('Invalid data');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = update_post_meta($event_id, '_game_state', $new_state);
|
||||||
|
update_post_meta($event_id, '_scoreboard_last_update', time());
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Game state updated successfully');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Failed to update game state');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get team logo URL
|
||||||
|
*/
|
||||||
|
private function get_team_logo($team_id) {
|
||||||
|
$logo_id = get_post_thumbnail_id($team_id);
|
||||||
|
if ($logo_id) {
|
||||||
|
$logo_url = wp_get_attachment_image_url($logo_id, 'thumbnail');
|
||||||
|
return $logo_url ?: 'https://via.placeholder.com/60x60/cccccc/ffffff?text=T';
|
||||||
|
}
|
||||||
|
return 'https://via.placeholder.com/60x60/cccccc/ffffff?text=T';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add_action('wp_enqueue_scripts', function() {
|
// Initialize the shortcode
|
||||||
wp_enqueue_script('live-score-script', plugins_url('/js/live-score.js', __FILE__), ['jquery'], null, true);
|
new LiveScoreboardShortcode();
|
||||||
wp_localize_script('live-score-script', 'LiveScoreAjax', [
|
|
||||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
// function scoreboard_add_to_event_page($content) {
|
|
||||||
// if (get_post_type() === 'sp_event') {
|
|
||||||
// ob_start();
|
|
||||||
// render_scoreboard_timeline();
|
|
||||||
// $timeline_html = ob_get_clean();
|
|
||||||
// return $timeline_html . $content; // Prepend it
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return $content;
|
/**
|
||||||
// }
|
* Add scoreboard control meta box for events (admin only)
|
||||||
// add_filter('the_content', 'scoreboard_add_to_event_page');
|
*/
|
||||||
// // Display scoreboard on match report
|
add_action('add_meta_boxes', 'add_scoreboard_control_meta_box');
|
||||||
// add_action('sportspress_after_event_content', 'render_scoreboard_timeline');
|
|
||||||
// function render_scoreboard_timeline() {
|
|
||||||
// echo '<div style="background:#f0f0f0;padding:10px;margin-bottom:10px;">Scoreboard timeline loaded.</div>';
|
|
||||||
|
|
||||||
// if (current_user_can('scoreboard_can_control')) {
|
function add_scoreboard_control_meta_box() {
|
||||||
// echo '<div style="background:#d1ffd1;padding:10px;margin-bottom:10px;">User CAN control scoreboard.</div>';
|
if (current_user_can('manage_scoreboard')) {
|
||||||
// //include plugin_dir_path(__FILE__) . 'templates/scoreboard-controls.php';
|
add_meta_box(
|
||||||
// } else {
|
'scoreboard-controls',
|
||||||
// echo '<div style="background:#ffe5e5;padding:10px;margin-bottom:10px;">User CANNOT control scoreboard.</div>';
|
'Live Scoreboard Controls',
|
||||||
// }
|
'render_scoreboard_control_meta_box',
|
||||||
|
'sp_event',
|
||||||
|
'side',
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// //include plugin_dir_path(__FILE__) . 'templates/scoreboard-timeline.php';
|
function render_scoreboard_control_meta_box($post) {
|
||||||
// }
|
$scoreboard_data = get_post_meta($post->ID, '_scoreboard_data', true);
|
||||||
|
$game_state = get_post_meta($post->ID, '_game_state', true) ?: 'WaitingForStart';
|
||||||
|
|
||||||
|
if (!is_array($scoreboard_data)) {
|
||||||
|
$scoreboard_data = array(
|
||||||
|
'timer_value' => '00:00'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_nonce_field('scoreboard_meta_box', 'scoreboard_meta_box_nonce');
|
||||||
|
?>
|
||||||
|
<div id="scoreboard-controls">
|
||||||
|
<p>
|
||||||
|
<label>Game State:</label>
|
||||||
|
<select name="game_state">
|
||||||
|
<option value="WaitingForStart" <?php selected($game_state, 'WaitingForStart'); ?>>Waiting for Start</option>
|
||||||
|
<option value="FirstHalf" <?php selected($game_state, 'FirstHalf'); ?>>First Half</option>
|
||||||
|
<option value="HalfTime" <?php selected($game_state, 'HalfTime'); ?>>Half Time</option>
|
||||||
|
<option value="SecondHalf" <?php selected($game_state, 'SecondHalf'); ?>>Second Half</option>
|
||||||
|
<option value="TimeOff" <?php selected($game_state, 'TimeOff'); ?>>Time Off</option>
|
||||||
|
<option value="Finished" <?php selected($game_state, 'Finished'); ?>>Finished</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label>Timer:</label>
|
||||||
|
<input type="text" name="timer_value" value="<?php echo esc_attr($scoreboard_data['timer_value']); ?>" placeholder="00:00">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id="live-scoreboard-preview" data-event-id="<?php echo $post->ID; ?>">
|
||||||
|
<!-- Live preview will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// Initialize live preview in admin
|
||||||
|
if (typeof scoreboardConfig !== 'undefined') {
|
||||||
|
const preview = new LiveScoreboard($('#live-scoreboard-preview')[0], {
|
||||||
|
eventId: <?php echo $post->ID; ?>,
|
||||||
|
ajaxUrl: '<?php echo admin_url('admin-ajax.php'); ?>',
|
||||||
|
nonce: '<?php echo wp_create_nonce('scoreboard_nonce'); ?>',
|
||||||
|
autoUpdate: true,
|
||||||
|
updateInterval: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save scoreboard meta box data
|
||||||
|
add_action('save_post', 'save_scoreboard_control_meta_box', 10, 2);
|
||||||
|
|
||||||
|
function save_scoreboard_control_meta_box($post_id, $post) {
|
||||||
|
if ($post->post_type !== 'sp_event') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_POST['scoreboard_meta_box_nonce']) || !wp_verify_nonce($_POST['scoreboard_meta_box_nonce'], 'scoreboard_meta_box')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!current_user_can('edit_post', $post_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scoreboard_data = array(
|
||||||
|
'timer_value' => sanitize_text_field($_POST['timer_value']),
|
||||||
|
'timestamp' => time()
|
||||||
|
);
|
||||||
|
|
||||||
|
$game_state = sanitize_text_field($_POST['game_state']);
|
||||||
|
|
||||||
|
update_post_meta($post_id, '_scoreboard_data', $scoreboard_data);
|
||||||
|
update_post_meta($post_id, '_game_state', $game_state);
|
||||||
|
update_post_meta($post_id, '_scoreboard_last_update', time());
|
||||||
|
}
|
||||||
|
function add_scoreboard_capability() {
|
||||||
|
$editor = get_role('editor');
|
||||||
|
$editor->add_cap('manage_scoreboard');
|
||||||
|
|
||||||
|
$admin = get_role('administrator');
|
||||||
|
$admin->add_cap('manage_scoreboard');
|
||||||
|
}
|
||||||
|
add_action('init', 'add_scoreboard_capability');
|
||||||
|
?>
|
||||||
Loading…
Reference in New Issue
Block a user