Files
eurovision2026/index.html
T
2026-05-16 18:56:22 +00:00

757 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eurovision 2026 — Vienna Scorecard</title>
<style>
:root {
--bg: #0a0a1a;
--card: #141428;
--card-hover: #1a1a36;
--accent: #7c5cfc;
--accent2: #fc5c8c;
--text: #e8e8f0;
--muted: #7878a0;
--gold: #ffd700;
--silver: #c0c0c0;
--bronze: #cd7f32;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
.hero {
background: linear-gradient(135deg, #1a0a3e 0%, #0a1a3e 50%, #1a0a2e 100%);
padding: 2rem 1rem;
text-align: center;
border-bottom: 2px solid var(--accent);
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 30% 50%, rgba(124,92,252,0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 50%, rgba(252,92,140,0.1) 0%, transparent 50%);
animation: shimmer 8s ease-in-out infinite alternate;
}
@keyframes shimmer {
0% { transform: translate(-10%, -10%); }
100% { transform: translate(10%, 10%); }
}
.hero h1 {
font-size: 2.5rem;
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
position: relative;
z-index: 1;
}
.hero .subtitle {
color: var(--muted);
font-size: 1.1rem;
margin-top: 0.5rem;
position: relative;
z-index: 1;
}
.hero .venue {
color: var(--accent2);
font-weight: 600;
position: relative;
z-index: 1;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
padding: 1.5rem 1rem;
position: sticky;
top: 0;
background: var(--bg);
z-index: 100;
border-bottom: 1px solid #1e1e3a;
}
.controls button, .controls select {
background: var(--card);
color: var(--text);
border: 1px solid #2a2a4a;
padding: 0.6rem 1rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.controls button:hover, .controls select:hover {
border-color: var(--accent);
background: var(--card-hover);
}
.controls button.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.controls select {
appearance: auto;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 1rem 4rem;
}
.country-card {
background: var(--card);
border-radius: var(--radius);
padding: 1rem 1.2rem;
margin-bottom: 0.75rem;
display: grid;
grid-template-columns: 3rem 1fr auto auto;
gap: 0.8rem;
align-items: center;
transition: all 0.2s;
border: 1px solid transparent;
}
.country-card:hover {
border-color: #2a2a4a;
background: var(--card-hover);
}
.country-card.scored {
border-color: #2a4a2a;
}
.rank-num {
font-size: 1.1rem;
font-weight: 700;
color: var(--muted);
text-align: center;
width: 3rem;
}
.info {
min-width: 0;
}
.country-name {
font-weight: 700;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.country-flag {
font-size: 1.4rem;
}
.odds-badge {
font-size: 0.7rem;
background: #2a2a4a;
color: var(--accent);
padding: 0.15rem 0.5rem;
border-radius: 20px;
font-weight: 600;
white-space: nowrap;
}
.artist-song {
color: var(--muted);
font-size: 0.85rem;
margin-top: 0.15rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.score-input {
width: 5rem;
text-align: center;
font-size: 1.1rem;
padding: 0.4rem;
border-radius: 8px;
border: 1px solid #2a2a4a;
background: #0e0e20;
color: var(--text);
font-weight: 600;
}
.score-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(124,92,252,0.3);
}
.notes-input {
width: 10rem;
padding: 0.4rem;
border-radius: 8px;
border: 1px solid #2a2a4a;
background: #0e0e20;
color: var(--text);
font-size: 0.85rem;
}
.notes-input:focus {
outline: none;
border-color: var(--accent);
}
.notes-input::placeholder {
color: var(--muted);
}
/* Ranking view */
.country-card.rank-1 { border-left: 4px solid var(--gold); }
.country-card.rank-2 { border-left: 4px solid var(--silver); }
.country-card.rank-3 { border-left: 4px solid var(--bronze); }
.rank-1 .rank-num::after { content: '👑'; }
.rank-2 .rank-num::after { content: '🥈'; }
.rank-3 .rank-num::after { content: '🥉'; }
.summary-box {
background: var(--card);
border-radius: var(--radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #2a2a4a;
}
.summary-box h3 {
color: var(--accent);
margin-bottom: 0.5rem;
}
.top3-list {
list-style: none;
padding: 0;
}
.top3-list li {
padding: 0.5rem 0;
border-bottom: 1px solid #1e1e3a;
display: flex;
justify-content: space-between;
align-items: center;
}
.top3-list li:last-child { border-bottom: none; }
.top3-list .place {
font-weight: 700;
margin-right: 0.5rem;
}
.top3-list .score-val {
color: var(--accent);
font-weight: 700;
}
.share-section {
background: var(--card);
border-radius: var(--radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #2a2a4a;
text-align: center;
}
.share-section h3 {
color: var(--accent2);
margin-bottom: 0.75rem;
}
.share-section textarea {
width: 100%;
min-height: 8rem;
background: #0e0e20;
color: var(--text);
border: 1px solid #2a2a4a;
border-radius: 8px;
padding: 0.75rem;
font-family: monospace;
font-size: 0.8rem;
resize: vertical;
margin-bottom: 0.75rem;
}
.share-section textarea:focus {
outline: none;
border-color: var(--accent);
}
.share-section .import-area {
margin-top: 1rem;
text-align: left;
}
.share-section .import-area label {
color: var(--muted);
font-size: 0.85rem;
display: block;
margin-bottom: 0.3rem;
}
.share-btn {
background: var(--accent);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
margin: 0.25rem;
}
.share-btn:hover {
background: #6a4ce0;
transform: translateY(-1px);
}
.share-btn.secondary {
background: var(--card);
border: 1px solid #2a2a4a;
color: var(--text);
}
.share-btn.secondary:hover {
border-color: var(--accent);
}
.share-btn.import-btn {
background: var(--accent2);
}
.share-btn.import-btn:hover {
background: #e04c7c;
}
.toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: var(--accent);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.toast.show {
opacity: 1;
}
.view-toggle {
display: flex;
gap: 0.25rem;
background: var(--card);
border-radius: 8px;
padding: 0.25rem;
border: 1px solid #2a2a4a;
}
.view-toggle button {
border: none !important;
padding: 0.4rem 0.8rem;
border-radius: 6px;
background: transparent !important;
color: var(--muted);
font-size: 0.8rem;
}
.view-toggle button.active {
background: var(--accent) !important;
color: white !important;
}
.hidden { display: none; }
/* Compact view */
.compact .country-card {
grid-template-columns: 2.5rem 1fr 4rem 8rem;
padding: 0.6rem 0.8rem;
margin-bottom: 0.4rem;
}
.compact .country-name { font-size: 0.9rem; }
.compact .artist-song { font-size: 0.75rem; }
.compact .score-input { width: 4rem; font-size: 1rem; }
.compact .notes-input { width: 7rem; font-size: 0.75rem; }
/* Responsive */
@media (max-width: 640px) {
.hero h1 { font-size: 1.8rem; }
.country-card {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.rank-num {
display: none;
}
.score-input, .notes-input {
width: 100%;
}
.controls {
flex-direction: column;
align-items: stretch;
}
}
.reset-link {
color: var(--muted);
font-size: 0.8rem;
cursor: pointer;
text-decoration: underline;
margin-top: 0.5rem;
display: inline-block;
}
.reset-link:hover { color: var(--accent2); }
.scored-count {
color: var(--muted);
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
</style>
</head>
<body>
<div class="hero">
<h1>🎤 Eurovision 2026</h1>
<div class="subtitle">Vienna Grand Final — <span class="venue">Saturday 16 May</span></div>
</div>
<div class="controls" id="controls">
<div class="view-toggle">
<button id="btn-score" class="active" onclick="setView('score')">✏️ Score</button>
<button id="btn-rank" onclick="setView('rank')">🏆 Rankings</button>
<button id="btn-share" onclick="setView('share')">📤 Share</button>
</div>
<select id="sort-select" onchange="render()" title="Sort by">
<option value="number">By running order</option>
<option value="alpha">By country name</option>
<option value="score">By your score</option>
<option value="odds">By bookmaker odds</option>
</select>
<span class="scored-count" id="scored-count">0 / 35 scored</span>
</div>
<div class="container" id="app">
<div id="score-view"></div>
<div id="rank-view" class="hidden"></div>
<div id="share-view" class="hidden">
<div class="share-section">
<h3>📤 Share Your Scores</h3>
<p style="color:var(--muted);margin-bottom:0.75rem;font-size:0.9rem;">Copy this to send to friends, or paste someone else's scores below to load them.</p>
<textarea id="export-area" readonly></textarea>
<div>
<button class="share-btn" onclick="copyScores()">📋 Copy Scores</button>
<button class="share-btn secondary" onclick="generateLink()">🔗 Generate Link</button>
</div>
<div class="import-area">
<label>Paste scores to import:</label>
<textarea id="import-area" placeholder="Paste exported scores here..."></textarea>
<button class="share-btn import-btn" onclick="importScores()">📥 Import Scores</button>
</div>
<div style="margin-top:1rem">
<a class="reset-link" onclick="resetAll()">🗑️ Reset all scores</a>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const COUNTRIES = [
{num:1, code:"AL", name:"Albania", artist:"Alis", song:"Nân", odds:126, chance:"1%"},
{num:2, code:"AM", name:"Armenia", artist:"SIMÓN", song:"Paloma Rumba", odds:251, chance:"<1%"},
{num:3, code:"AU", name:"Australia", artist:"Delta Goodrem", song:"Eclipse", odds:3, chance:"23%"},
{num:4, code:"AT", name:"Austria", artist:"COSMÓ", song:"Tanzschein", odds:501, chance:"<1%"},
{num:5, code:"AZ", name:"Azerbaijan", artist:"JIVA", song:"Just Go", odds:1001, chance:"<1%"},
{num:6, code:"BE", name:"Belgium", artist:"ESSYLA", song:"Dancing on the Ice", odds:501, chance:"<1%"},
{num:7, code:"BG", name:"Bulgaria", artist:"DARA", song:"Bangaranga", odds:10, chance:"7%"},
{num:8, code:"HR", name:"Croatia", artist:"LELEK", song:"Andromeda", odds:126, chance:"1%"},
{num:9, code:"CY", name:"Cyprus", artist:"Antigoni", song:"JALLA", odds:201, chance:"1%"},
{num:10, code:"CZ", name:"Czechia", artist:"Daniel Žižka", song:"Crossroads", odds:81, chance:"1%"},
{num:11, code:"DK", name:"Denmark", artist:"Søren Torpegaard Lund", song:"Før Vi Går Hjem", odds:26, chance:"2%"},
{num:12, code:"EE", name:"Estonia", artist:"Vanilla Ninja", song:"Too Epic To Be True", odds:301, chance:"<1%"},
{num:13, code:"FI", name:"Finland", artist:"Linda Lampenius & Pete Parkkonen", song:"Liekinheitin", odds:2, chance:"40%"},
{num:14, code:"FR", name:"France", artist:"Monroe", song:"Regarde !", odds:81, chance:"1%"},
{num:15, code:"GE", name:"Georgia", artist:"Bzikebi", song:"On Replay", odds:151, chance:"1%"},
{num:16, code:"DE", name:"Germany", artist:"Sarah Engels", song:"Fire", odds:301, chance:"<1%"},
{num:17, code:"GR", name:"Greece", artist:"Akylas", song:"Ferto", odds:13, chance:"6%"},
{num:18, code:"IL", name:"Israel", artist:"Noam Bettan", song:"Michelle", odds:14, chance:"5%"},
{num:19, code:"IT", name:"Italy", artist:"Sal Da Vinci", song:"Per Sempre Sì", odds:51, chance:"2%"},
{num:20, code:"LV", name:"Latvia", artist:"Atvara", song:"Ēnā", odds:201, chance:"1%"},
{num:21, code:"LT", name:"Lithuania", artist:"Lion Ceccah", song:"Sólo Quiero Más", odds:301, chance:"<1%"},
{num:22, code:"LU", name:"Luxembourg", artist:"Eva Marija", song:"Mother Nature", odds:500, chance:"<1%"},
{num:23, code:"MT", name:"Malta", artist:"AIDAN", song:"Bella", odds:101, chance:"1%"},
{num:24, code:"MD", name:"Moldova", artist:"Satoshi", song:"Viva, Moldova!", odds:81, chance:"1%"},
{num:25, code:"ME", name:"Montenegro", artist:"Tamara Živković", song:"Nova Zora", odds:500, chance:"<1%"},
{num:26, code:"NO", name:"Norway", artist:"JONAS LOVV", song:"Ya Ya Ya", odds:251, chance:"<1%"},
{num:27, code:"PL", name:"Poland", artist:"ALICJA", song:"Pray", odds:251, chance:"<1%"},
{num:28, code:"PT", name:"Portugal", artist:"Bandidos do Cante", song:"Rosa", odds:201, chance:"1%"},
{num:29, code:"RO", name:"Romania", artist:"Alexandra Căpitănescu", song:"Choke Me", odds:20, chance:"4%"},
{num:30, code:"SM", name:"San Marino", artist:"SENHIT", song:"Superstar", odds:401, chance:"<1%"},
{num:31, code:"RS", name:"Serbia", artist:"LAVINA", song:"Kraj Mene", odds:251, chance:"<1%"},
{num:32, code:"SE", name:"Sweden", artist:"Felicia", song:"My System", odds:51, chance:"1%"},
{num:33, code:"CH", name:"Switzerland", artist:"Gio Pane", song:"Alice", odds:201, chance:"1%"},
{num:34, code:"UA", name:"Ukraine", artist:"Leléka", song:"Ridnym", odds:101, chance:"1%"},
{num:35, code:"GB", name:"United Kingdom", artist:"Look Mum No Computer", song:"Eins, Zwei, Drei", odds:301, chance:"<1%"}
];
const FLAGS = {
AL:'🇦🇱',AM:'🇦🇲',AU:'🇦🇺',AT:'🇦🇹',AZ:'🇦🇿',BE:'🇧🇪',BG:'🇧🇬',HR:'🇭🇷',CY:'🇨🇾',
CZ:'🇨🇿',DK:'🇩🇰',EE:'🇪🇪',FI:'🇫🇮',FR:'🇫🇷',GE:'🇬🇪',DE:'🇩🇪',GR:'🇬🇷',IL:'🇮🇱',
IT:'🇮🇹',LV:'🇱🇻',LT:'🇱🇹',LU:'🇱🇺',MT:'🇲🇹',MD:'🇲🇩',ME:'🇲🇪',NO:'🇳🇴',PL:'🇵🇱',
PT:'🇵🇹',RO:'🇷🇴',SM:'🇸🇲',RS:'🇷🇸',SE:'🇸🇪',CH:'🇨🇭',UA:'🇺🇦',GB:'🇬🇧'
};
let scores = {};
let currentView = 'score';
function loadScores() {
try {
const saved = localStorage.getItem('eurovision2026_scores');
if (saved) scores = JSON.parse(saved);
} catch(e) {}
const params = new URLSearchParams(window.location.search);
const data = params.get('scores');
if (data) {
try {
const parsed = JSON.parse(atob(decodeURIComponent(data)));
scores = parsed;
saveScores();
} catch(e) {}
}
}
function saveScores() {
localStorage.setItem('eurovision2026_scores', JSON.stringify(scores));
}
function getSorted() {
const sort = document.getElementById('sort-select').value;
let list = [...COUNTRIES];
switch(sort) {
case 'alpha': list.sort((a,b) => a.name.localeCompare(b.name)); break;
case 'score': list.sort((a,b) => (scores[b.code]||0) - (scores[a.code]||0)); break;
case 'odds': list.sort((a,b) => a.odds - b.odds); break;
default: list.sort((a,b) => a.num - b.num);
}
return list;
}
function setView(view) {
currentView = view;
document.getElementById('score-view').classList.toggle('hidden', view !== 'score');
document.getElementById('rank-view').classList.toggle('hidden', view !== 'rank');
document.getElementById('share-view').classList.toggle('hidden', view !== 'share');
document.getElementById('btn-score').classList.toggle('active', view === 'score');
document.getElementById('btn-rank').classList.toggle('active', view === 'rank');
document.getElementById('btn-share').classList.toggle('active', view === 'share');
if (view === 'share') generateExport();
render();
}
function render() {
const list = getSorted();
const scoredCount = Object.keys(scores).filter(k => scores[k] !== '' && scores[k] !== null).length;
document.getElementById('scored-count').textContent = `${scoredCount} / ${COUNTRIES.length} scored`;
const scoreView = document.getElementById('score-view');
scoreView.innerHTML = list.map((c, i) => `
<div class="country-card ${scores[c.code] !== '' && scores[c.code] !== null && scores[c.code] !== undefined ? 'scored' : ''}">
<div class="rank-num">${c.num}</div>
<div class="info">
<div class="country-name">
<span class="country-flag">${FLAGS[c.code]||''}</span>
${c.name}
<span class="odds-badge" title="Win chance: ${c.chance}">🎲 ${c.odds === 1 ? 'EVEN' : c.odds}</span>
</div>
<div class="artist-song">${c.artist}${c.song}</div>
</div>
<input type="number" class="score-input" placeholder="Pts"
value="${scores[c.code] !== undefined && scores[c.code] !== '' ? scores[c.code] : ''}"
onchange="updateScore('${c.code}', this.value)" title="Your score" min="0" max="12">
<input type="text" class="notes-input" placeholder="Notes..."
value="${scores[c.code + '_n'] || ''}"
onchange="updateNote('${c.code}', this.value)" title="Your notes">
</div>
`).join('');
renderRankings();
}
function renderRankings() {
const ranked = COUNTRIES
.filter(c => scores[c.code] !== '' && scores[c.code] !== null && scores[c.code] !== undefined && scores[c.code] > 0)
.sort((a,b) => (scores[b.code]||0) - (scores[a.code]||0));
const rankView = document.getElementById('rank-view');
if (!ranked.length) {
rankView.innerHTML = '<div class="summary-box"><p style="color:var(--muted);text-align:center">Score some countries first to see rankings!</p></div>';
return;
}
let html = '<div class="summary-box"><h3>🏆 Your Rankings</h3><ul class="top3-list">';
ranked.forEach((c, i) => {
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : '';
html += `<li class="${i<3 ? 'rank-' + (i+1) : ''}">
<span><span class="place">${medal} #${i+1}</span> ${FLAGS[c.code]||''} ${c.name}</span>
<span class="score-val">${scores[c.code]} pts</span>
</li>`;
});
html += '</ul></div>';
rankView.innerHTML = html;
}
function updateScore(code, value) {
if (value === '' || value === null) {
delete scores[code];
} else {
scores[code] = parseInt(value) || 0;
}
saveScores();
render();
}
function updateNote(code, value) {
if (value) {
scores[code + '_n'] = value;
} else {
delete scores[code + '_n'];
}
saveScores();
}
function generateExport() {
const data = {};
COUNTRIES.forEach(c => {
if (scores[c.code] !== '' && scores[c.code] !== null && scores[c.code] !== undefined) {
data[c.name] = {
score: scores[c.code],
...(scores[c.code + '_n'] ? {note: scores[c.code + '_n']} : {})
};
}
});
const json = JSON.stringify(data, null, 2);
document.getElementById('export-area').value =
`🎤 Eurovision 2026 Scores\n${'═'.repeat(40)}\n\n${json}\n\n${'═'.repeat(40)}\n🔗 Generated by Eurovision 2026 Scorecard`;
}
function copyScores() {
const area = document.getElementById('export-area');
navigator.clipboard.writeText(area.value).then(() => showToast('📋 Copied to clipboard!'));
}
function generateLink() {
const data = btoa(encodeURIComponent(JSON.stringify(scores)));
const url = window.location.origin + window.location.pathname + '?scores=' + encodeURIComponent(data);
document.getElementById('export-area').value = url;
navigator.clipboard.writeText(url).then(() => showToast('🔗 Link copied!'));
}
function importScores() {
const input = document.getElementById('import-area').value.trim();
if (!input) return showToast('⚠️ Paste some scores first!');
const urlMatch = input.match(/scores=([^&\s]+)/);
if (urlMatch) {
try {
scores = JSON.parse(decodeURIComponent(atob(urlMatch[1])));
saveScores();
document.getElementById('import-area').value = '';
render();
showToast('✅ Scores imported!');
setView('score');
return;
} catch(e) {}
}
try {
const jsonMatch = input.match(/\{[\s\S]+\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
Object.entries(parsed).forEach(([name, val]) => {
const country = COUNTRIES.find(c => c.name.toLowerCase() === name.toLowerCase());
if (country) {
scores[country.code] = typeof val === 'object' ? val.score : val;
if (typeof val === 'object' && val.note) {
scores[country.code + '_n'] = val.note;
}
}
});
saveScores();
document.getElementById('import-area').value = '';
render();
showToast('✅ Scores imported!');
setView('score');
} else {
showToast('⚠️ Could not parse scores');
}
} catch(e) {
showToast('⚠️ Invalid format');
}
}
function resetAll() {
if (confirm('Clear all scores and notes?')) {
scores = {};
saveScores();
render();
showToast('🗑️ All cleared');
}
}
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2000);
}
loadScores();
render();
setView('score');
</script>
</body>
</html>