From dd259081c1f41844abd673951d74320168754810 Mon Sep 17 00:00:00 2001 From: Heidi Date: Mon, 22 Jun 2026 22:05:46 +0100 Subject: [PATCH] ai generated solutions to our ai generated problems --- frontend/index.html | 42 +- frontend/src/App.jsx | 1041 +++++++++++++++++++++++++++++++++--------- 2 files changed, 864 insertions(+), 219 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index e158163..1706f13 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -42,11 +42,51 @@ document.documentElement.dataset.theme = theme document.documentElement.style.backgroundColor = bg document.documentElement.style.colorScheme = theme + + const customColor = cookies['tssbot_custom_color'] || localStorage.getItem('tssbot.customColor') + if (customColor && /^#[0-9a-fA-F]{6}$/.test(customColor)) { + document.documentElement.style.setProperty('--color-fury-cyan', customColor) + document.documentElement.style.setProperty('--color-ring', customColor) + + const bigint = parseInt(customColor.replace('#', ''), 16) + const r = (bigint >> 16) & 255 + const g = (bigint >> 8) & 255 + const b = bigint & 255 + document.documentElement.style.setProperty('--color-shadow', 'rgba(' + r + ', ' + g + ', ' + b + ', ' + (theme === 'dark' ? '0.18' : '0.12') + ')') + + const adjust = (val, percent) => percent > 0 + ? Math.max(0, Math.min(255, Math.round(val + (255 - val) * percent))) + : Math.max(0, Math.min(255, Math.round(val * (1 + percent)))) + const formatHex = (nr, ng, nb) => '#' + ((1 << 24) + (nr << 16) + (ng << 8) + nb).toString(16).slice(1) + + const aquaColor = formatHex(adjust(r, 0.2), adjust(g, 0.2), adjust(b, 0.2)) + const violetColor = formatHex(adjust(r, -0.2), adjust(g, -0.2), adjust(b, -0.2)) + + document.documentElement.style.setProperty('--color-fury-aqua', aquaColor) + document.documentElement.style.setProperty('--color-fury-violet', violetColor) + } + document.querySelector('meta[name="theme-color"]')?.setAttribute( 'content', - theme === 'dark' ? '#101211' : '#e82517', + customColor && /^#[0-9a-fA-F]{6}$/.test(customColor) + ? customColor + : (theme === 'dark' ? '#101211' : '#e82517') ) + try { + const rawColors = cookies['tssbot_custom_colors'] || localStorage.getItem('tssbot.customColors') + if (rawColors) { + const colors = JSON.parse(rawColors) + const cssVarMap = { bg: '--color-bg', surface: '--color-surface', text: '--color-text', border: '--color-border', textSoft: '--color-text-soft', textMuted: '--color-text-muted' } + for (const [field, cssVar] of Object.entries(cssVarMap)) { + if (colors[field] && /^#[0-9a-fA-F]{6}$/.test(colors[field])) { + document.documentElement.style.setProperty(cssVar, colors[field]) + if (field === 'bg') document.documentElement.style.backgroundColor = colors[field] + } + } + } + } catch {} + let analyticsPreferences = null const serializedPreferences = cookies['tssbot_analytics_preferences'] try { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0178f3d..55964fb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -45,6 +45,7 @@ const primaryNavItems = [ const utilityNavItems = [ { path: '/viewers', label: 'Viewers' }, { path: '/docs', label: 'Setup' }, + { path: '/settings', label: 'Settings' }, ] const analyticsConsentKey = 'tssbot.analyticsConsent' @@ -198,6 +199,7 @@ function parseRoute(pathname = window.location.pathname) { if (pathname === '/viewers') return { page: 'viewers', teamName: '' } if (pathname === '/privacy') return { page: 'privacy', teamName: '' } if (pathname === '/docs') return { page: 'docs', teamName: '' } + if (pathname === '/settings') return { page: 'settings', teamName: '' } if (pathname === '/tournaments') return { page: 'tournaments-list', teamName: '' } if (pathname.startsWith('/tournaments/')) { const tournamentId = decodeURIComponent(pathname.slice('/tournaments/'.length)) @@ -458,9 +460,9 @@ function teamDetailLooksReal(detail) { const hasRoster = players.length > 0 const hasActivity = Boolean( Number(summary?.player_count || 0) > 0 || - Number(summary?.total_battles || 0) > 0 || - Number(summary?.wins || 0) > 0 || - Number(summary?.points?.total_points || summary?.total_points || 0) > 0, + Number(summary?.total_battles || 0) > 0 || + Number(summary?.wins || 0) > 0 || + Number(summary?.points?.total_points || summary?.total_points || 0) > 0, ) return hasStableId || hasRoster || hasActivity @@ -490,8 +492,8 @@ function normalizeAnalyticsPreferences(value) { function browserPrivacySignalEnabled() { return Boolean( navigator.globalPrivacyControl || - navigator.doNotTrack === '1' || - window.doNotTrack === '1', + navigator.doNotTrack === '1' || + window.doNotTrack === '1', ) } @@ -578,6 +580,110 @@ function persistThemePreference(theme) { return normalized } +const customColorCookie = 'tssbot_custom_color' +const customColorKey = 'tssbot.customColor' + +function hexToRgb(hex) { + try { + const cleanHex = String(hex || '').trim().replace('#', '') + const bigint = parseInt(cleanHex, 16) + const r = (bigint >> 16) & 255 + const g = (bigint >> 8) & 255 + const b = bigint & 255 + return { r, g, b } + } catch { + return { r: 232, g: 37, b: 23 } + } +} + +function adjustColorBrightness(hex, percent) { + try { + const { r, g, b } = hexToRgb(hex) + const adjust = (val) => percent > 0 + ? Math.max(0, Math.min(255, Math.round(val + (255 - val) * percent))) + : Math.max(0, Math.min(255, Math.round(val * (1 + percent)))) + const nr = adjust(r) + const ng = adjust(g) + const nb = adjust(b) + return '#' + ((1 << 24) + (nr << 16) + (ng << 8) + nb).toString(16).slice(1) + } catch { + return hex + } +} + +function storedCustomColorPreference() { + const cookieValue = readCookie(customColorCookie) + if (cookieValue && /^#[0-9a-fA-F]{6}$/.test(cookieValue)) return cookieValue + + try { + const local = window.localStorage.getItem(customColorKey) + if (local && /^#[0-9a-fA-F]{6}$/.test(local)) return local + } catch { } + + return '' +} + +function persistCustomColorPreference(color) { + if (color && /^#[0-9a-fA-F]{6}$/.test(color)) { + writeCookie(customColorCookie, color) + try { + window.localStorage.setItem(customColorKey, color) + } catch { } + return color + } else { + writeCookie(customColorCookie, '') + try { + window.localStorage.removeItem(customColorKey) + } catch { } + return '' + } +} + +const customColorsCookie = 'tssbot_custom_colors' +const customColorsKey = 'tssbot.customColors' + +const customColorsFields = ['bg', 'surface', 'text', 'border', 'textSoft', 'textMuted'] + +function storedCustomColors() { + const parse = (raw) => { + try { + const obj = JSON.parse(raw) + if (obj && typeof obj === 'object') { + const result = {} + for (const field of customColorsFields) { + if (obj[field] && /^#[0-9a-fA-F]{6}$/.test(obj[field])) result[field] = obj[field] + } + return result + } + } catch { } + return {} + } + + const cookieValue = readCookie(customColorsCookie) + if (cookieValue) { + const parsed = parse(cookieValue) + if (Object.keys(parsed).length) return parsed + } + + try { + const local = window.localStorage.getItem(customColorsKey) + if (local) return parse(local) + } catch { } + + return {} +} + +function persistCustomColors(colors) { + const filtered = {} + for (const field of customColorsFields) { + if (colors[field] && /^#[0-9a-fA-F]{6}$/.test(colors[field])) filtered[field] = colors[field] + } + const json = JSON.stringify(filtered) + writeCookie(customColorsCookie, json) + try { window.localStorage.setItem(customColorsKey, json) } catch { } + return filtered +} + function storedAnalyticsPreferences() { const bootPreferences = window.__TSS_BOOT_PREFERENCES__?.analyticsPreferences if (bootPreferences) return normalizeAnalyticsPreferences(bootPreferences) @@ -717,6 +823,7 @@ function routeLabel(route) { if (route.page === 'viewers') return 'viewers' if (route.page === 'privacy') return 'Privacy notice' if (route.page === 'docs') return 'Docs' + if (route.page === 'settings') return 'Settings' if (route.page === 'player') return route.uid ? `Player ${route.uid}` : 'Player' return 'Home' } @@ -739,6 +846,7 @@ function canonicalPathForRoute(route) { if (route.page === 'viewers') return '/viewers' if (route.page === 'privacy') return '/privacy' if (route.page === 'docs') return '/docs' + if (route.page === 'settings') return '/settings' if (route.page === 'player' && route.uid) return `/players/${encodeURIComponent(route.uid)}` return '/' } @@ -855,6 +963,12 @@ function seoForRoute(route, profileDetail = null) { robots: 'noindex, follow', path: '/docs', }, + settings: { + title: "Settings | Toothless' TSS Bot", + description: 'Customize layout and appearance preferences for Toothless TSS Bot, including custom theme accent colors.', + robots: 'noindex, nofollow', + path: '/settings', + }, } return byPage[route.page] || { @@ -1263,6 +1377,8 @@ function AppContent() { const [viewers, setViewers] = useState({ status: 'idle', data: null, error: null, updatedAt: null }) const [analyticsPreferences, setAnalyticsPreferences] = useState(() => storedAnalyticsPreferences()) const [theme, setTheme] = useState(() => storedThemePreference()) + const [customColor, setCustomColor] = useState(() => storedCustomColorPreference()) + const [customColors, setCustomColors] = useState(() => storedCustomColors()) const [showFloatingNav, setShowFloatingNav] = useState(() => window.scrollY > 40) const [themeTogglePositions, setThemeTogglePositions] = useState(() => { const position = defaultThemeTogglePosition() @@ -1302,11 +1418,52 @@ function AppContent() { themeRef.current = theme document.documentElement.dataset.theme = theme document.documentElement.style.colorScheme = theme - document.querySelector('meta[name="theme-color"]')?.setAttribute( - 'content', - theme === 'dark' ? '#101211' : '#e82517', - ) - }, [theme]) + + const metaThemeColor = document.querySelector('meta[name="theme-color"]') + + if (customColor && /^#[0-9a-fA-F]{6}$/.test(customColor)) { + document.documentElement.style.setProperty('--color-fury-cyan', customColor) + document.documentElement.style.setProperty('--color-ring', customColor) + + const { r, g, b } = hexToRgb(customColor) + document.documentElement.style.setProperty('--color-shadow', `rgba(${r}, ${g}, ${b}, ${theme === 'dark' ? 0.18 : 0.12})`) + + const aquaColor = adjustColorBrightness(customColor, 0.2) + const violetColor = adjustColorBrightness(customColor, -0.2) + document.documentElement.style.setProperty('--color-fury-aqua', aquaColor) + document.documentElement.style.setProperty('--color-fury-violet', violetColor) + + metaThemeColor?.setAttribute('content', customColor) + } else { + document.documentElement.style.removeProperty('--color-fury-cyan') + document.documentElement.style.removeProperty('--color-ring') + document.documentElement.style.removeProperty('--color-shadow') + document.documentElement.style.removeProperty('--color-fury-aqua') + document.documentElement.style.removeProperty('--color-fury-violet') + + metaThemeColor?.setAttribute( + 'content', + theme === 'dark' ? '#101211' : '#e82517', + ) + } + + const cssVarMap = { + bg: '--color-bg', + surface: '--color-surface', + text: '--color-text', + border: '--color-border', + textSoft: '--color-text-soft', + textMuted: '--color-text-muted', + } + for (const [field, cssVar] of Object.entries(cssVarMap)) { + const val = customColors[field] + if (val && /^#[0-9a-fA-F]{6}$/.test(val)) { + document.documentElement.style.setProperty(cssVar, val) + } else { + document.documentElement.style.removeProperty(cssVar) + } + } + }, [theme, customColor, customColors]) useEffect(() => () => { window.clearTimeout(themeTransitionTimerRef.current) @@ -1442,7 +1599,7 @@ function AppContent() { headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), keepalive: true, - }).catch(() => {}) + }).catch(() => { }) } sendViewerEvent('page_view') @@ -1908,7 +2065,7 @@ function AppContent() { session_id: analyticsSessionId, }), keepalive: true, - }).catch(() => {}) + }).catch(() => { }) } } @@ -1925,20 +2082,41 @@ function AppContent() { setTheme(persistThemePreference(nextTheme)) } + function chooseCustomColor(nextColor) { + if (!nextColor) { + setCustomColor('') + persistCustomColorPreference('') + return + } + + setCustomColor(nextColor) + + if (/^#[0-9a-fA-F]{6}$/.test(nextColor)) { + persistCustomColorPreference(nextColor) + } + } + + function chooseCustomColors(nextColors) { + const saved = persistCustomColors(nextColors) + setCustomColors(saved) + } + const activeNavPath = route.page === 'team' ? '/teams' : route.page === 'player' || route.page === 'players' ? '/players' - : route.page === 'battle-logs' || route.page === 'game' - ? '/battle-logs' - : route.page === 'tournaments-list' || route.page === 'tournament' - ? '/tournaments' - : route.page === 'blog' || route.page === 'blog-post' - ? '/blog' - : route.page === 'viewers' - ? '/viewers' - : window.location.pathname + : route.page === 'battle-logs' || route.page === 'game' + ? '/battle-logs' + : route.page === 'tournaments-list' || route.page === 'tournament' + ? '/tournaments' + : route.page === 'blog' || route.page === 'blog-post' + ? '/blog' + : route.page === 'viewers' + ? '/viewers' + : route.page === 'settings' + ? '/settings' + : window.location.pathname const shouldShowFloatingNav = route.page !== 'home' || showFloatingNav useEffect(() => { @@ -2092,6 +2270,18 @@ function AppContent() { {route.page === 'game' ? : null} {route.page === 'tournaments-list' ? : null} {route.page === 'tournament' ? : null} + {route.page === 'settings' ? ( + + ) : null}