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