diff --git a/internal/web/assets/app.js b/internal/web/assets/app.js index 601d88a..453726d 100644 --- a/internal/web/assets/app.js +++ b/internal/web/assets/app.js @@ -4,16 +4,96 @@ // ---- Shared helpers ---- -// Convert ISO 3166-1 alpha-2 country code → emoji flag (mirrors TUI countryFlag). +// Map OpenF1 3-letter country codes → ISO 3166-1 alpha-2 for flag emoji. +const CODE3_TO_2 = { + AUS:'AU', AUT:'AT', AZE:'AZ', BRN:'BH', BEL:'BE', BRA:'BR', CHN:'CN', + NED:'NL', FRA:'FR', GER:'DE', HUN:'HU', ITA:'IT', JPN:'JP', MON:'MC', + MEX:'MX', QAT:'QA', KSA:'SA', SGP:'SG', ESP:'ES', UAE:'AE', GBR:'GB', + USA:'US', CAN:'CA', RSA:'ZA', +}; + +// Convert country code (2- or 3-letter) → emoji flag. function flagEmoji(code) { - if (!code || code.length !== 2) return '🏁'; - const offset = 0x1F1E6 - 65; // Regional Indicator A offset + if (!code) return '🏁'; + let iso2 = code.toUpperCase(); + if (iso2.length === 3) iso2 = CODE3_TO_2[iso2] || ''; + if (iso2.length !== 2) return '🏁'; + const offset = 0x1F1E6 - 65; return String.fromCodePoint( - code.toUpperCase().charCodeAt(0) + offset, - code.toUpperCase().charCodeAt(1) + offset + iso2.charCodeAt(0) + offset, + iso2.charCodeAt(1) + offset ); } +// Per-country accent colours for GP cards. +// Keys cover both ISO 3166-1 alpha-2 AND the 3-letter codes OpenF1 sometimes returns. +const COUNTRY_COLORS = { + // 2-letter ISO + AU: '#C0392B', AT: '#ED2939', AZ: '#0092BC', BH: '#CE1126', + BE: '#F0B400', BR: '#009C3B', CN: '#DE2910', NL: '#FF6600', + FR: '#002395', DE: '#CC0000', HU: '#3A6B35', IT: '#009246', + JP: '#BC002D', MC: '#CE1126', MX: '#006847', QA: '#8D1B3D', + SA: '#006C35', SG: '#EF3340', ES: '#C60B1E', AE: '#009A44', + GB: '#012169', US: '#B22234', CA: '#FF0000', + // 3-letter OpenF1 codes + AUS: '#C0392B', // Australia + AUT: '#ED2939', // Austria + AZE: '#0092BC', // Azerbaijan / Baku + BRN: '#CE1126', // Bahrain + BEL: '#F0B400', // Belgium + BRA: '#009C3B', // Brazil + CHN: '#DE2910', // China + NED: '#FF6600', // Netherlands + FRA: '#002395', // France + GER: '#CC0000', // Germany + HUN: '#3A6B35', // Hungary + ITA: '#009246', // Italy + JPN: '#BC002D', // Japan + MON: '#CE1126', // Monaco + MEX: '#006847', // Mexico + QAT: '#8D1B3D', // Qatar + KSA: '#006C35', // Saudi Arabia + SGP: '#EF3340', // Singapore + ESP: '#C60B1E', // Spain + UAE: '#009A44', // Abu Dhabi + GBR: '#012169', // Great Britain + USA: '#B22234', // USA + CAN: '#FF0000', // Canada + RSA: '#007A4D', // South Africa +}; + +// Return the accent hex for a country code (2- or 3-letter), or null. +function countryAccent(code) { + return COUNTRY_COLORS[code?.toUpperCase()] || null; +} + +// Build the full inline `style` string for a meeting card so the background +// gradient is applied directly (CSS vars containing gradient strings don't work +// reliably across all browsers when used in background-image). +function meetingCardStyle(code) { + const accent = countryAccent(code) || '#2a2a44'; + // Parse the hex to RGB for use in rgba() + const r = parseInt(accent.slice(1, 3), 16); + const g = parseInt(accent.slice(3, 5), 16); + const b = parseInt(accent.slice(5, 7), 16); + return [ + `--country-accent:${accent}`, + `--country-r:${r}`, + `--country-g:${g}`, + `--country-b:${b}`, + `background:linear-gradient(145deg, rgba(${r},${g},${b},0.18) 0%, rgba(${r},${g},${b},0.07) 55%, var(--surface-1) 100%)`, + ].join(';'); +} + +// Tinted gradient string (kept for backward compat, not used on cards now). +function countryGradient(code) { + const accent = countryAccent(code) || '#2a2a44'; + const r = parseInt(accent.slice(1, 3), 16); + const g = parseInt(accent.slice(3, 5), 16); + const b = parseInt(accent.slice(5, 7), 16); + return `linear-gradient(145deg, rgba(${r},${g},${b},0.18) 0%, rgba(${r},${g},${b},0.07) 55%, transparent 100%)`; +} + // Current F1 season year derived from today's date. function currentSeason() { return new Date().getFullYear(); @@ -230,7 +310,14 @@ function homePage() { }, flagEmoji, + countryAccent, + meetingCardStyle, isPast(m) { return new Date(m.date_start).getTime() < Date.now(); }, + roundNum(m) { + // Find 1-based index within the meetings list + const idx = this.meetings.indexOf(m); + return idx >= 0 ? String(idx + 1).padStart(2, '0') : '—'; + }, fmtDate(d) { return d ? new Date(d).toLocaleDateString('en-GB', {day:'numeric',month:'short'}) : ''; }, destroy() { clearInterval(this._countdownTimer); }, diff --git a/internal/web/assets/index.html b/internal/web/assets/index.html index 0706f50..531e3c3 100644 --- a/internal/web/assets/index.html +++ b/internal/web/assets/index.html @@ -85,13 +85,22 @@
diff --git a/internal/web/assets/style.css b/internal/web/assets/style.css index 39ff96b..611b73f 100644 --- a/internal/web/assets/style.css +++ b/internal/web/assets/style.css @@ -321,50 +321,126 @@ a { color: inherit; text-decoration: none; } } .meetings-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(175px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 0.75rem; padding: 0 1.5rem 2rem; } + +/* ---- Meeting card ---- */ .meeting-card { - display: block; - background: var(--surface-1); + display: flex; + flex-direction: column; + justify-content: space-between; + /* background is set directly via inline style by meetingCardStyle() */ border: 1px solid var(--surface-3); - border-radius: 8px; - padding: 0.75rem 1rem; + border-radius: 10px; + padding: 0; cursor: pointer; - transition: background 0.15s, border-color 0.15s, transform 0.15s; + transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s; position: relative; overflow: hidden; + min-height: 140px; + text-decoration: none; } .meeting-card:hover { - background: var(--surface-2); - border-color: var(--text-3); - transform: translateY(-2px); + transform: translateY(-3px); + border-color: rgba(var(--country-r,100), var(--country-g,100), var(--country-b,150), 0.55); + box-shadow: 0 6px 22px rgba(0,0,0,0.4), + 0 0 0 1px rgba(var(--country-r,100), var(--country-g,100), var(--country-b,150), 0.2); } -.meeting-card.past { opacity: 0.5; } +.meeting-card.past { + opacity: 0.45; + filter: grayscale(0.4); +} +.meeting-card.meeting-next { + border-color: rgba(var(--country-r,225), var(--country-g,6), var(--country-b,0), 0.55); + box-shadow: 0 0 0 1px rgba(var(--country-r,225), var(--country-g,6), var(--country-b,0), 0.2), + 0 4px 20px rgba(0,0,0,0.35); +} + +/* Top accent stripe */ .meeting-card-accent { position: absolute; top: 0; left: 0; right: 0; height: 3px; - background: var(--surface-3); - transition: background 0.2s; -} -.meeting-card:not(.past) .meeting-card-accent { - background: linear-gradient(90deg, var(--f1-red), #ff4422); + background: var(--country-accent, var(--surface-3)); + transition: opacity 0.2s; } +.meeting-card.past .meeting-card-accent { opacity: 0.35; } .meeting-card.meeting-next .meeting-card-accent { - background: linear-gradient(90deg, var(--f1-red), #ff8844, var(--f1-red)); + background: linear-gradient(90deg, + var(--country-accent, var(--f1-red)) 0%, + rgba(var(--country-r,225), var(--country-g,6), var(--country-b,0), 0.5) 50%, + var(--country-accent, var(--f1-red)) 100%); background-size: 200% 100%; animation: shimmer-red 2s linear infinite; } -.meeting-card.meeting-next { - border-color: rgba(225,6,0,0.35); - box-shadow: 0 0 0 1px rgba(225,6,0,0.12), 0 4px 16px rgba(225,6,0,0.1); + +/* Card body: country code + flag */ +.meeting-card-body { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1rem 0.85rem 0.25rem; +} + +/* Large bold country code — colored with the accent */ +.meeting-country-code { + font-size: 1.75rem; + font-weight: 900; + letter-spacing: -0.03em; + line-height: 1; + text-transform: uppercase; + color: var(--country-accent, var(--text-1)); + /* soften very dark accents a little so they're legible */ + filter: brightness(1.4); + text-shadow: 0 2px 10px rgba(0,0,0,0.5); +} +.meeting-card.past .meeting-country-code { + color: var(--text-3); + filter: none; +} + +/* Small flag emoji — top-right corner */ +.meeting-card-flag { + font-size: 1.25rem; + line-height: 1; + filter: drop-shadow(0 1px 3px rgba(0,0,0,0.4)); +} + +/* Card footer: name, circuit, date */ +.meeting-card-foot { + padding: 0.5rem 0.85rem 0.75rem; +} +.meeting-name { + font-weight: 700; + font-size: 0.82rem; + line-height: 1.3; + color: var(--text-1); + margin-bottom: 0.1rem; +} +.meeting-circuit { + color: var(--text-2); + font-size: 0.73rem; + margin-top: 0.1rem; +} +.meeting-date { + color: var(--text-3); + font-size: 0.7rem; + margin-top: 0.25rem; +} + +/* Round number badge — bottom right */ +.meeting-round { + position: absolute; + bottom: 0.6rem; + right: 0.7rem; + font-size: 0.62rem; + font-weight: 800; + letter-spacing: 0.08em; + color: var(--text-3); + opacity: 0.7; } -.meeting-flag { font-size: 1.5rem; margin: 0.3rem 0 0.3rem; } -.meeting-name { font-weight: 600; font-size: 0.9rem; line-height: 1.3; } -.meeting-circuit { color: var(--text-2); font-size: 0.78rem; margin-top: 0.15rem; } -.meeting-date { color: var(--text-3); font-size: 0.75rem; margin-top: 0.3rem; } /* ---- Session pills ---- */ .session-pills {