init
This commit is contained in:
commit
586014e23c
8 changed files with 690 additions and 0 deletions
134
app.py
Normal file
134
app.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fig Fam Fantasy Plinko Score Tracker
|
||||
A local web app for tracking player scores with real-time updates
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
from flask import Flask, render_template, request, jsonify, Response
|
||||
from threading import Lock
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
db_lock = Lock()
|
||||
|
||||
# Database Setup
|
||||
def init_db():
|
||||
"""Initialize SQLite database with players table"""
|
||||
with sqlite3.connect('database.db') as conn:
|
||||
conn.execute('''
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
score INTEGER DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Initialize 14 players if empty
|
||||
count = conn.execute('SELECT COUNT(*) FROM players').fetchone()[0]
|
||||
if count == 0:
|
||||
for i in range(1, 15):
|
||||
conn.execute(
|
||||
'INSERT INTO players (id, name, score) VALUES (?, ?, ?)',
|
||||
(i, f'Player {i}', 0)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
# Database Operations
|
||||
def get_all_players():
|
||||
"""Retrieve all players sorted by ID for input page"""
|
||||
with sqlite3.connect('database.db') as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
return [dict(row) for row in conn.execute(
|
||||
'SELECT * FROM players ORDER BY id'
|
||||
).fetchall()]
|
||||
|
||||
def get_players_by_score():
|
||||
"""Retrieve all players sorted by score (highest first) for scoreboard"""
|
||||
with sqlite3.connect('database.db') as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
return [dict(row) for row in conn.execute(
|
||||
'SELECT * FROM players ORDER BY score DESC, name ASC'
|
||||
).fetchall()]
|
||||
|
||||
def update_player(player_id, name=None, score=None):
|
||||
"""Update player name and/or score"""
|
||||
with db_lock:
|
||||
with sqlite3.connect('database.db') as conn:
|
||||
if name is not None and score is not None:
|
||||
conn.execute(
|
||||
'UPDATE players SET name = ?, score = ? WHERE id = ?',
|
||||
(name, score, player_id)
|
||||
)
|
||||
elif name is not None:
|
||||
conn.execute(
|
||||
'UPDATE players SET name = ? WHERE id = ?',
|
||||
(name, player_id)
|
||||
)
|
||||
elif score is not None:
|
||||
conn.execute(
|
||||
'UPDATE players SET score = ? WHERE id = ?',
|
||||
(score, player_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
# Web Routes
|
||||
@app.route('/')
|
||||
def input_page():
|
||||
"""Mobile-friendly score input page"""
|
||||
players = get_all_players()
|
||||
return render_template('input.html', players=players)
|
||||
|
||||
@app.route('/scoreboard')
|
||||
def scoreboard():
|
||||
"""TV display scoreboard page"""
|
||||
players = get_players_by_score() # Sort by score for display
|
||||
return render_template('scoreboard.html', players=players)
|
||||
|
||||
@app.route('/api/players')
|
||||
def api_get_players():
|
||||
"""API endpoint to get all players (sorted by ID for input)"""
|
||||
return jsonify(get_all_players())
|
||||
|
||||
@app.route('/api/update', methods=['POST'])
|
||||
def api_update_player():
|
||||
"""API endpoint to update player data"""
|
||||
data = request.get_json()
|
||||
player_id = data.get('id')
|
||||
name = data.get('name')
|
||||
score = data.get('score')
|
||||
|
||||
if player_id and (name is not None or score is not None):
|
||||
update_player(player_id, name, score)
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'success': False, 'error': 'Invalid data'})
|
||||
|
||||
@app.route('/events')
|
||||
def events():
|
||||
"""Server-Sent Events for real-time scoreboard updates"""
|
||||
def event_stream():
|
||||
last_data = None
|
||||
while True:
|
||||
players = get_players_by_score() # Sort by score for real-time updates
|
||||
current_data = json.dumps(players)
|
||||
|
||||
# Send update only if data changed
|
||||
if current_data != last_data:
|
||||
yield f"data: {current_data}\n\n"
|
||||
last_data = current_data
|
||||
|
||||
time.sleep(1) # Check every second
|
||||
|
||||
return Response(
|
||||
event_stream(),
|
||||
mimetype='text/plain',
|
||||
headers={
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
app.run(host='0.0.0.0', port=8080, debug=True)
|
||||
52
nginx.conf
Normal file
52
nginx.conf
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# nginx.conf
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
# Static file serving
|
||||
location /static/ {
|
||||
alias /path/to/your/project/static/;
|
||||
expires 1d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Proxy all other requests to Flask
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# SSE specific headers
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
chunked_transfer_encoding off;
|
||||
}
|
||||
|
||||
# SSE endpoint optimization
|
||||
location /events {
|
||||
proxy_pass http://127.0.0.1:5000/events;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Disable buffering for real-time updates
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 24h;
|
||||
proxy_send_timeout 24h;
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
chunked_transfer_encoding off;
|
||||
|
||||
# CORS headers if needed
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Origin, Content-Type";
|
||||
}
|
||||
}
|
||||
|
||||
BIN
static/pricedown-b1.ttf
Normal file
BIN
static/pricedown-b1.ttf
Normal file
Binary file not shown.
69
static/scoreboard.js
Normal file
69
static/scoreboard.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// static/scoreboard.js - Real-time scoreboard updates
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Server-Sent Events for Real-time Updates
|
||||
const eventSource = new EventSource('/events');
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
try {
|
||||
const players = JSON.parse(event.data);
|
||||
updateScoreboard(players);
|
||||
} catch (error) {
|
||||
console.error('Error parsing player data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function(error) {
|
||||
console.error('EventSource error:', error);
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Update scoreboard display with sorted players
|
||||
function updateScoreboard(players) {
|
||||
const columns = document.querySelectorAll('.column');
|
||||
|
||||
// Clear existing rows
|
||||
columns.forEach(col => col.innerHTML = '');
|
||||
|
||||
// Rebuild grid with current rankings
|
||||
players.forEach((player, index) => {
|
||||
const rank = index + 1;
|
||||
const columnIndex = Math.floor(index / 7);
|
||||
|
||||
if (columnIndex < 2) {
|
||||
const scoreRow = createScoreRow(player, rank);
|
||||
columns[columnIndex].appendChild(scoreRow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create a score row element
|
||||
function createScoreRow(player, rank) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'score-row';
|
||||
row.dataset.id = player.id;
|
||||
|
||||
// Highlight leader (position 1)
|
||||
if (rank === 1) {
|
||||
row.classList.add('leader');
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<span class="player-rank">${rank}</span>
|
||||
<span class="player-name">${player.name}</span>
|
||||
<span class="player-score">${player.score}</span>
|
||||
`;
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
// Prevent screen sleep for TV display
|
||||
if ('wakeLock' in navigator) {
|
||||
navigator.wakeLock.request('screen').catch(err => {
|
||||
console.log('Wake lock failed:', err);
|
||||
});
|
||||
}
|
||||
});
|
||||
65
static/script.js
Normal file
65
static/script.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// static/script.js - Input page functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Mobile Input Controls
|
||||
const playerCards = document.querySelectorAll('.player-card');
|
||||
|
||||
playerCards.forEach(card => {
|
||||
const updateBtn = card.querySelector('.update-btn');
|
||||
const nameInput = card.querySelector('.name-input');
|
||||
const scoreInput = card.querySelector('.score-input');
|
||||
const playerId = card.dataset.id;
|
||||
|
||||
// Update player data on button click
|
||||
updateBtn.addEventListener('click', function() {
|
||||
const name = nameInput.value.trim();
|
||||
const score = parseInt(scoreInput.value) || 0;
|
||||
|
||||
updatePlayer(playerId, name, score);
|
||||
});
|
||||
|
||||
// Update on Enter key press
|
||||
[nameInput, scoreInput].forEach(input => {
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
updateBtn.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-select score input content for easy editing
|
||||
scoreInput.addEventListener('focus', function() {
|
||||
this.select();
|
||||
});
|
||||
});
|
||||
|
||||
// API call to update player
|
||||
async function updatePlayer(id, name, score) {
|
||||
try {
|
||||
const response = await fetch('/api/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(id),
|
||||
name: name,
|
||||
score: score
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Visual feedback for successful update
|
||||
const card = document.querySelector(`[data-id="${id}"]`);
|
||||
card.style.background = '#90EE90';
|
||||
setTimeout(() => {
|
||||
card.style.background = '';
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update failed:', error);
|
||||
alert('Failed to update player data');
|
||||
}
|
||||
}
|
||||
});
|
||||
299
static/style.css
Normal file
299
static/style.css
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
/* static/style.css */
|
||||
|
||||
/* Font Face Declaration */
|
||||
@font-face {
|
||||
font-family: 'PlinkoFont';
|
||||
src: url('pricedown-b1.ttf') format('truetype');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* static/style.css */
|
||||
|
||||
/* Price Is Right Color Palette */
|
||||
:root {
|
||||
--pir-blue: #0066CC;
|
||||
--pir-yellow: #FFD700;
|
||||
--pir-red: #FF4444;
|
||||
--pir-green: #00AA44;
|
||||
--pir-orange: #FF8800;
|
||||
--pir-white: #FFFFFF;
|
||||
--pir-light-blue: #66AAFF;
|
||||
--gradient-bg: linear-gradient(135deg, var(--pir-blue), var(--pir-light-blue));
|
||||
}
|
||||
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--pir-white);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Input Page Styles - Mobile Optimized */
|
||||
.input-page {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
background: var(--gradient-bg);
|
||||
}
|
||||
|
||||
/* Input Page Styles - Mobile Optimized */
|
||||
.input-page {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.input-page .container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.input-page h1 {
|
||||
text-align: center;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--pir-yellow);
|
||||
text-shadow: 3px 3px 6px rgba(0,0,0,0.5);
|
||||
margin-bottom: 30px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.input-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.player-card {
|
||||
background: var(--pir-white);
|
||||
color: var(--pir-blue);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
border: 4px solid var(--pir-yellow);
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.player-card label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.name-input, .score-input {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
font-size: 1.1rem;
|
||||
border: 3px solid var(--pir-blue);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.score-input {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
width: 100%;
|
||||
background: var(--pir-red);
|
||||
color: var(--pir-white);
|
||||
border: none;
|
||||
padding: 15px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.update-btn:hover {
|
||||
background: #CC2222;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-board-btn {
|
||||
display: inline-block;
|
||||
background: var(--pir-green);
|
||||
color: var(--pir-white);
|
||||
text-decoration: none;
|
||||
padding: 20px 40px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-board-btn:hover {
|
||||
background: #008833;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Scoreboard Page Styles - TV Optimized */
|
||||
.scoreboard-page {
|
||||
height: 100vh;
|
||||
padding: 40px;
|
||||
font-family: 'PlinkoFont', 'Arial Black', Arial, sans-serif;
|
||||
background: linear-gradient(135deg,
|
||||
var(--pir-blue) 0%,
|
||||
var(--pir-light-blue) 50%,
|
||||
#4488DD 100%);
|
||||
}
|
||||
|
||||
.scoreboard-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.scoreboard-page .title {
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
color: var(--pir-yellow);
|
||||
text-shadow: 4px 4px 0px var(--pir-red),
|
||||
8px 8px 16px rgba(0,0,0,0.7);
|
||||
margin-bottom: 40px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.scoreboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
flex-grow: 1;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 20px;
|
||||
background: var(--pir-white);
|
||||
color: var(--pir-blue);
|
||||
padding: 25px;
|
||||
border-radius: 15px;
|
||||
border: 6px solid var(--pir-yellow);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.4);
|
||||
align-items: center;
|
||||
transition: all 0.5s ease;
|
||||
min-height: 80px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.score-row:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 12px 24px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.score-row.leader {
|
||||
border-color: var(--pir-orange);
|
||||
background: linear-gradient(135deg, var(--pir-white), #FFF8DC);
|
||||
box-shadow: 0 12px 24px rgba(255, 136, 0, 0.4);
|
||||
}
|
||||
|
||||
.player-rank {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
background: var(--pir-red);
|
||||
color: var(--pir-white);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--pir-red);
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.input-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.input-page h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.scoreboard-page .title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
padding: 20px;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.player-rank {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
35
templates/input.html
Normal file
35
templates/input.html
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<!-- templates/input.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Plinko Score Input</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="input-page">
|
||||
<div class="container">
|
||||
<h1>Fig Fam Fantasy Plinko</h1>
|
||||
<div class="input-grid">
|
||||
{% for player in players %}
|
||||
<div class="player-card" data-id="{{ player.id }}">
|
||||
<label>Player {{ player.id }}</label>
|
||||
<input type="text"
|
||||
class="name-input"
|
||||
value="{{ player.name }}"
|
||||
placeholder="Player Name">
|
||||
<input type="number"
|
||||
class="score-input"
|
||||
value="{{ player.score }}"
|
||||
placeholder="0">
|
||||
<button class="update-btn">Update</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/scoreboard" class="view-board-btn">View Scoreboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
36
templates/scoreboard.html
Normal file
36
templates/scoreboard.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!-- templates/scoreboard.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Plinko Scoreboard</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="scoreboard-page">
|
||||
<div class="scoreboard-container">
|
||||
<h1 class="title">Fig Fam Fantasy Plinko</h1>
|
||||
<div class="scoreboard-grid">
|
||||
<div class="column">
|
||||
{% for player in players[:7] %}
|
||||
<div class="score-row" data-id="{{ player.id }}">
|
||||
<span class="player-rank">{{ loop.index }}</span>
|
||||
<span class="player-name">{{ player.name }}</span>
|
||||
<span class="player-score">{{ player.score }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="column">
|
||||
{% for player in players[7:14] %}
|
||||
<div class="score-row" data-id="{{ player.id }}">
|
||||
<span class="player-rank">{{ loop.index + 7 }}</span>
|
||||
<span class="player-name">{{ player.name }}</span>
|
||||
<span class="player-score">{{ player.score }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/scoreboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue