|
<!DOCTYPE html> |
|
<html lang="en"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>{% block title %}TTS Arena{% endblock %}</title> |
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
|
{% block extra_head %}{% endblock %} |
|
<style> |
|
:root { |
|
--primary-color: #5046e5; |
|
--secondary-color: #f0f0f0; |
|
--text-color: #333; |
|
--light-gray: #f5f5f5; |
|
--border-color: #e0e0e0; |
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
--radius: 8px; |
|
--success-color: #10b981; |
|
--info-color: #3b82f6; |
|
--warning-color: #f59e0b; |
|
--error-color: #ef4444; |
|
} |
|
|
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
font-family: 'Inter', sans-serif; |
|
} |
|
|
|
body { |
|
color: var(--text-color); |
|
display: flex; |
|
min-height: 100vh; |
|
height: 100vh; |
|
overflow: hidden; |
|
} |
|
|
|
a { |
|
color: var(--primary-color); |
|
} |
|
|
|
.sidebar { |
|
width: 240px; |
|
background-color: var(--light-gray); |
|
padding: 24px 16px; |
|
border-right: 1px solid var(--border-color); |
|
display: flex; |
|
flex-direction: column; |
|
height: 100vh; |
|
z-index: 1000; |
|
transition: transform 0.3s ease-in-out; |
|
flex-shrink: 0; |
|
} |
|
|
|
.logo { |
|
font-size: 24px; |
|
font-weight: 700; |
|
margin-bottom: 32px; |
|
color: var(--primary-color); |
|
} |
|
|
|
.nav-item { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
margin-bottom: 8px; |
|
border-radius: var(--radius); |
|
cursor: pointer; |
|
transition: background-color 0.2s; |
|
color: var(--text-color); |
|
text-decoration: none; |
|
} |
|
|
|
.nav-item.active { |
|
background-color: rgba(80, 70, 229, 0.1); |
|
color: var(--primary-color); |
|
font-weight: 500; |
|
} |
|
|
|
.nav-item:hover:not(.active) { |
|
background-color: rgba(0, 0, 0, 0.05); |
|
} |
|
|
|
.nav-item svg { |
|
margin-right: 12px; |
|
} |
|
|
|
.main-content { |
|
flex: 1; |
|
padding: 32px; |
|
width: 100%; |
|
margin: 0 auto; |
|
overflow-y: auto; |
|
height: 100vh; |
|
} |
|
|
|
.main-content-inner { |
|
max-width: 1200px; |
|
width: 100%; |
|
margin: 0 auto; |
|
} |
|
|
|
.tabs { |
|
display: flex; |
|
border-bottom: 1px solid var(--border-color); |
|
margin-bottom: 24px; |
|
} |
|
|
|
.tab { |
|
padding: 12px 24px; |
|
cursor: pointer; |
|
position: relative; |
|
font-weight: 500; |
|
} |
|
|
|
.tab.active { |
|
color: var(--primary-color); |
|
} |
|
|
|
.tab.active::after { |
|
content: ''; |
|
position: absolute; |
|
bottom: -1px; |
|
left: 0; |
|
width: 100%; |
|
height: 2px; |
|
background-color: var(--primary-color); |
|
} |
|
|
|
.input-container { |
|
display: flex; |
|
margin-bottom: 24px; |
|
align-items: center; |
|
} |
|
|
|
.text-input { |
|
flex: 1; |
|
padding: 12px 16px; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
font-family: 'Inter', sans-serif; |
|
font-size: 1em; |
|
outline: none; |
|
transition: border-color 0.2s; |
|
} |
|
|
|
.text-input:focus { |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.btn { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border: none; |
|
border-radius: var(--radius); |
|
padding: 12px 24px; |
|
margin-left: 12px; |
|
cursor: pointer; |
|
font-weight: 500; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.btn:hover { |
|
background-color: #4038c7; |
|
} |
|
|
|
.icon-btn { |
|
background-color: white; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
width: 42px; |
|
height: 42px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin-left: 12px; |
|
cursor: pointer; |
|
transition: background-color 0.2s; |
|
} |
|
|
|
.icon-btn:hover { |
|
background-color: var(--light-gray); |
|
} |
|
|
|
.players-container { |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.players-row { |
|
display: flex; |
|
gap: 24px; |
|
margin-bottom: 24px; |
|
} |
|
|
|
.player { |
|
flex: 1; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
padding: 16px; |
|
box-shadow: var(--shadow); |
|
} |
|
|
|
.player-label { |
|
font-weight: 600; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.audio-player { |
|
width: 100%; |
|
margin-bottom: 16px; |
|
} |
|
|
|
.vote-btn { |
|
width: 100%; |
|
padding: 12px; |
|
background-color: white; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
font-weight: 500; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
position: relative; |
|
} |
|
|
|
.vote-btn:hover { |
|
background-color: var(--light-gray); |
|
border-color: #ccc; |
|
} |
|
|
|
.vote-btn.selected { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.shortcut-key { |
|
position: absolute; |
|
right: 12px; |
|
top: 50%; |
|
transform: translateY(-50%); |
|
background-color: var(--light-gray); |
|
color: var(--text-color); |
|
border: 1px solid var(--border-color); |
|
border-radius: 4px; |
|
padding: 2px 6px; |
|
font-size: 12px; |
|
font-weight: 600; |
|
} |
|
|
|
.vote-btn.selected .shortcut-key { |
|
background-color: rgba(255, 255, 255, 0.2); |
|
color: white; |
|
border-color: transparent; |
|
} |
|
|
|
.user-auth { |
|
margin-top: auto; |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
border-top: 1px solid var(--border-color); |
|
cursor: pointer; |
|
position: relative; |
|
} |
|
|
|
.user-avatar { |
|
width: 32px; |
|
height: 32px; |
|
border-radius: 50%; |
|
background-color: var(--primary-color); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
color: white; |
|
font-weight: 600; |
|
margin-right: 12px; |
|
} |
|
|
|
.user-name { |
|
font-weight: 500; |
|
flex: 1; |
|
} |
|
|
|
.user-dropdown { |
|
position: absolute; |
|
bottom: 100%; |
|
left: 0; |
|
right: 0; |
|
margin: 0 16px; |
|
background-color: white; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
box-shadow: var(--shadow); |
|
z-index: 1000; |
|
display: none; |
|
overflow: hidden; |
|
margin-bottom: 8px; |
|
} |
|
|
|
.user-dropdown.active { |
|
display: block; |
|
} |
|
|
|
.dropdown-item { |
|
padding: 12px 16px; |
|
display: flex; |
|
align-items: center; |
|
transition: background-color 0.2s; |
|
text-decoration: none; |
|
color: var(--text-color); |
|
} |
|
|
|
.dropdown-item:hover { |
|
background-color: var(--light-gray); |
|
} |
|
|
|
.dropdown-item svg { |
|
margin-right: 12px; |
|
} |
|
|
|
.dropdown-divider { |
|
height: 1px; |
|
background-color: var(--border-color); |
|
margin: 4px 0; |
|
} |
|
|
|
.user-auth-arrow { |
|
transition: transform 0.2s; |
|
} |
|
|
|
.user-auth.active .user-auth-arrow { |
|
transform: rotate(180deg); |
|
} |
|
|
|
.login-link { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
border-top: 1px solid var(--border-color); |
|
text-decoration: none; |
|
color: var(--text-color); |
|
} |
|
|
|
.login-link:hover { |
|
background-color: var(--light-gray); |
|
} |
|
|
|
.login-link img { |
|
width: 24px; |
|
height: 24px; |
|
margin-right: 12px; |
|
} |
|
|
|
.discord-link { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
border-top: 1px solid var(--border-color); |
|
text-decoration: none; |
|
color: var(--text-color); |
|
} |
|
|
|
.discord-link:hover { |
|
background-color: var(--light-gray); |
|
color: #5865F2; |
|
} |
|
|
|
.discord-link svg { |
|
margin-right: 12px; |
|
} |
|
|
|
.sidebar-footer { |
|
margin-top: auto; |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.mobile-header { |
|
display: none; |
|
align-items: center; |
|
justify-content: space-between; |
|
padding: 16px; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.hamburger-menu { |
|
width: 24px; |
|
height: 24px; |
|
cursor: pointer; |
|
} |
|
|
|
.current-page { |
|
font-weight: 600; |
|
font-size: 18px; |
|
} |
|
|
|
.backdrop { |
|
display: none; |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.5); |
|
-webkit-backdrop-filter: blur(3px); |
|
backdrop-filter: blur(3px); |
|
z-index: 999; |
|
opacity: 0; |
|
transition: opacity 0.3s ease-in-out; |
|
} |
|
|
|
.backdrop.active { |
|
display: block; |
|
opacity: 1; |
|
} |
|
|
|
.close-sidebar { |
|
position: absolute; |
|
top: 16px; |
|
right: 16px; |
|
width: 24px; |
|
height: 24px; |
|
cursor: pointer; |
|
display: none; |
|
} |
|
|
|
|
|
.toast-container { |
|
position: fixed; |
|
bottom: 24px; |
|
right: 24px; |
|
z-index: 9999; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
max-width: 350px; |
|
} |
|
|
|
.toast { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 16px; |
|
border-radius: 8px; |
|
background-color: white; |
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); |
|
animation: slideIn 0.3s ease-out forwards; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.toast.slide-out { |
|
animation: slideOut 0.3s ease-in forwards; |
|
} |
|
|
|
.toast-icon { |
|
margin-right: 10px; |
|
flex-shrink: 0; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.toast-content { |
|
flex: 1; |
|
font-size: 14px; |
|
font-weight: 500; |
|
line-height: 1.4; |
|
} |
|
|
|
.toast-close { |
|
margin-left: 10px; |
|
cursor: pointer; |
|
opacity: 0.5; |
|
transition: opacity 0.2s; |
|
flex-shrink: 0; |
|
border-radius: 50%; |
|
width: 20px; |
|
height: 20px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
|
|
.toast-close:hover { |
|
opacity: 1; |
|
background-color: rgba(0, 0, 0, 0.05); |
|
} |
|
|
|
.toast-progress { |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
height: 2px; |
|
width: 100%; |
|
transform-origin: left; |
|
} |
|
|
|
.toast.info { |
|
border-left-color: var(--info-color); |
|
} |
|
|
|
.toast.info .toast-icon { |
|
color: var(--info-color); |
|
} |
|
|
|
.toast.info .toast-progress { |
|
background-color: var(--info-color); |
|
} |
|
|
|
.toast.success .toast-icon { |
|
color: var(--success-color); |
|
} |
|
|
|
.toast.success .toast-progress { |
|
background-color: var(--success-color); |
|
} |
|
|
|
.toast.warning .toast-icon { |
|
color: var(--warning-color); |
|
} |
|
|
|
.toast.warning .toast-progress { |
|
background-color: var(--warning-color); |
|
} |
|
|
|
.toast.error .toast-icon { |
|
color: var(--error-color); |
|
} |
|
|
|
.toast.error .toast-progress { |
|
background-color: var(--error-color); |
|
} |
|
|
|
@keyframes slideIn { |
|
from { |
|
transform: translateX(100%); |
|
opacity: 0; |
|
} |
|
|
|
to { |
|
transform: translateX(0); |
|
opacity: 1; |
|
} |
|
} |
|
|
|
@keyframes slideOut { |
|
from { |
|
transform: translateX(0); |
|
opacity: 1; |
|
} |
|
|
|
to { |
|
transform: translateX(100%); |
|
opacity: 0; |
|
} |
|
} |
|
|
|
@keyframes shrink { |
|
from { |
|
transform: scaleX(1); |
|
} |
|
to { |
|
transform: scaleX(0); |
|
} |
|
} |
|
|
|
@media (max-width: 768px) { |
|
body { |
|
flex-direction: column; |
|
} |
|
|
|
.mobile-header { |
|
display: flex; |
|
flex-shrink: 0; |
|
} |
|
|
|
.sidebar { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 280px; |
|
border-right: 1px solid var(--border-color); |
|
padding: 24px 16px; |
|
height: 100vh; |
|
transform: translateX(-100%); |
|
} |
|
|
|
.sidebar.active { |
|
transform: translateX(0); |
|
} |
|
|
|
.close-sidebar { |
|
display: block; |
|
} |
|
|
|
.logo { |
|
display: block; |
|
} |
|
|
|
.players-container { |
|
flex-direction: column; |
|
} |
|
|
|
.main-content { |
|
height: calc(100vh - 57px); |
|
overflow-y: auto; |
|
} |
|
|
|
.toast-container { |
|
bottom: auto; |
|
top: 16px; |
|
right: 16px; |
|
left: 16px; |
|
max-width: none; |
|
} |
|
|
|
@keyframes slideIn { |
|
from { |
|
transform: translateY(-100%); |
|
opacity: 0; |
|
} |
|
|
|
to { |
|
transform: translateY(0); |
|
opacity: 1; |
|
} |
|
} |
|
|
|
@keyframes slideOut { |
|
from { |
|
transform: translateY(0); |
|
opacity: 1; |
|
} |
|
|
|
to { |
|
transform: translateY(-100%); |
|
opacity: 0; |
|
} |
|
} |
|
} |
|
|
|
::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: var(--light-gray); |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: rgba(120, 120, 120, 0.5); |
|
border-radius: 4px; |
|
transition: background 0.2s ease; |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: rgba(100, 100, 100, 0.7); |
|
} |
|
|
|
|
|
* { |
|
scrollbar-width: thin; |
|
scrollbar-color: rgba(120, 120, 120, 0.5) var(--light-gray); |
|
} |
|
|
|
|
|
::-webkit-scrollbar-corner { |
|
background: var(--light-gray); |
|
} |
|
|
|
|
|
html { |
|
scroll-behavior: smooth; |
|
} |
|
|
|
|
|
@media (prefers-color-scheme: dark) { |
|
:root { |
|
--primary-color: #6c63ff; |
|
--secondary-color: #2d2b38; |
|
--text-color: #e0e0e0; |
|
--light-gray: #1e1e24; |
|
--border-color: #3a3a45; |
|
--shadow: 0 2px 8px rgba(0, 0, 0, 0.3); |
|
--success-color: #10b981; |
|
--info-color: #60a5fa; |
|
--warning-color: #f59e0b; |
|
--error-color: #ef4444; |
|
} |
|
|
|
body { |
|
background-color: #121218; |
|
color: var(--text-color); |
|
} |
|
|
|
.sidebar { |
|
background-color: var(--light-gray); |
|
border-right-color: var(--border-color); |
|
} |
|
|
|
.nav-item.active { |
|
background-color: rgba(108, 99, 255, 0.2); |
|
} |
|
|
|
.nav-item:hover:not(.active) { |
|
background-color: rgba(255, 255, 255, 0.05); |
|
} |
|
|
|
.text-input, |
|
.select-input, |
|
.textarea { |
|
background-color: var(--light-gray); |
|
color: var(--text-color); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.card { |
|
background-color: var(--light-gray); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.tab.active::after { |
|
background-color: var(--primary-color); |
|
} |
|
|
|
|
|
.vote-btn { |
|
background-color: var(--light-gray); |
|
color: var(--text-color); |
|
border-color: var(--border-color); |
|
border-radius: var(--radius); |
|
} |
|
|
|
.vote-btn:hover { |
|
background-color: rgba(255, 255, 255, 0.1); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.vote-btn.selected { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.shortcut-key { |
|
background-color: rgba(255, 255, 255, 0.1); |
|
color: var(--text-color); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.vote-btn.selected .shortcut-key { |
|
background-color: rgba(255, 255, 255, 0.2); |
|
color: white; |
|
border-color: transparent; |
|
} |
|
|
|
|
|
.vote-btn:disabled, |
|
.vote-btn.loading { |
|
background-color: var(--light-gray); |
|
border-radius: var(--radius); |
|
} |
|
|
|
.vote-loader { |
|
background-color: var(--light-gray); |
|
border-radius: var(--radius); |
|
} |
|
|
|
.vote-spinner { |
|
border-color: rgba(108, 99, 255, 0.3); |
|
border-top-color: var(--primary-color); |
|
} |
|
|
|
.toast { |
|
background-color: var(--light-gray); |
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
.toast-close:hover { |
|
background-color: rgba(255, 255, 255, 0.1); |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: var(--secondary-color); |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: rgba(180, 180, 180, 0.5); |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: rgba(200, 200, 200, 0.7); |
|
} |
|
|
|
* { |
|
scrollbar-color: rgba(180, 180, 180, 0.5) var(--secondary-color); |
|
} |
|
|
|
::-webkit-scrollbar-corner { |
|
background: var(--secondary-color); |
|
} |
|
|
|
|
|
.loading-overlay { |
|
background-color: rgba(18, 18, 24, 0.8); |
|
} |
|
|
|
|
|
.loader-spinner { |
|
border-color: rgba(108, 99, 255, 0.2); |
|
border-top-color: var(--primary-color); |
|
} |
|
|
|
|
|
.user-dropdown { |
|
background-color: var(--light-gray); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.dropdown-item { |
|
color: var(--text-color); |
|
} |
|
|
|
.dropdown-item:hover { |
|
background-color: rgba(108, 99, 255, 0.1); |
|
} |
|
|
|
.dropdown-divider { |
|
background-color: var(--border-color); |
|
} |
|
|
|
.user-avatar { |
|
background-color: var(--primary-color); |
|
} |
|
} |
|
|
|
|
|
.loading-overlay { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(255, 255, 255, 0.8); |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 9999; |
|
opacity: 0; |
|
visibility: hidden; |
|
transition: opacity 0.3s ease, visibility 0.3s ease; |
|
} |
|
|
|
.loading-overlay.active { |
|
opacity: 1; |
|
visibility: visible; |
|
} |
|
|
|
.loader-spinner { |
|
width: 50px; |
|
height: 50px; |
|
border: 3px solid rgba(80, 70, 229, 0.3); |
|
border-radius: 50%; |
|
border-top-color: var(--primary-color); |
|
animation: spin 1s ease-in-out infinite; |
|
} |
|
|
|
@keyframes spin { |
|
to { |
|
transform: rotate(360deg); |
|
} |
|
} |
|
|
|
|
|
.login-tip-overlay { |
|
position: absolute; |
|
background-color: white; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--radius); |
|
box-shadow: var(--shadow); |
|
padding: 16px; |
|
z-index: 1000; |
|
width: 280px; |
|
display: none; |
|
} |
|
|
|
.login-tip-overlay.show { |
|
display: block; |
|
} |
|
|
|
.login-tip-content { |
|
font-size: 14px; |
|
margin-bottom: 12px; |
|
} |
|
|
|
.login-tip-actions { |
|
display: flex; |
|
justify-content: space-between; |
|
} |
|
|
|
.login-tip-close { |
|
font-size: 13px; |
|
color: var(--text-color); |
|
opacity: 0.7; |
|
cursor: pointer; |
|
background: none; |
|
border: none; |
|
padding: 0; |
|
} |
|
|
|
.login-now-btn { |
|
font-size: 13px; |
|
background-color: var(--primary-color); |
|
color: white; |
|
border: none; |
|
border-radius: 4px; |
|
padding: 6px 12px; |
|
cursor: pointer; |
|
text-decoration: none; |
|
} |
|
|
|
.login-tip-overlay[data-popper-placement^='top'] .login-tip-caret { |
|
position: absolute; |
|
bottom: -8px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
width: 16px; |
|
height: 8px; |
|
overflow: hidden; |
|
} |
|
|
|
.login-tip-overlay[data-popper-placement^='top'] .login-tip-caret:after { |
|
content: ''; |
|
position: absolute; |
|
width: 12px; |
|
height: 12px; |
|
background: white; |
|
border-right: 1px solid var(--border-color); |
|
border-bottom: 1px solid var(--border-color); |
|
top: -6px; |
|
left: 2px; |
|
transform: rotate(45deg); |
|
} |
|
|
|
@media (prefers-color-scheme: dark) { |
|
.login-tip-overlay { |
|
background-color: var(--light-gray); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.login-tip-overlay[data-popper-placement^='top'] .login-tip-caret:after { |
|
background: var(--light-gray); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.login-tip-close { |
|
color: var(--text-color); |
|
} |
|
} |
|
|
|
|
|
.login-banner { |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
width: 85%; |
|
max-width: 320px; |
|
background-color: white; |
|
color: var(--text-color); |
|
border-radius: var(--radius); |
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
|
padding: 20px; |
|
display: none; |
|
z-index: 9998; |
|
text-align: center; |
|
border: 1px solid var(--border-color); |
|
} |
|
|
|
.login-banner-content { |
|
margin-bottom: 16px; |
|
font-size: 15px; |
|
font-weight: 500; |
|
} |
|
|
|
.login-banner-actions { |
|
display: flex; |
|
flex-direction: row; |
|
justify-content: space-between; |
|
gap: 12px; |
|
align-items: center; |
|
margin-top: 20px; |
|
} |
|
|
|
.login-banner-close { |
|
background: none; |
|
border: 1px solid var(--border-color); |
|
color: var(--text-color); |
|
font-size: 14px; |
|
cursor: pointer; |
|
padding: 10px 16px; |
|
border-radius: var(--radius); |
|
flex: 1; |
|
font-weight: 500; |
|
} |
|
|
|
.login-banner-btn { |
|
background-color: var(--primary-color); |
|
color: white; |
|
border: none; |
|
border-radius: var(--radius); |
|
padding: 10px 16px; |
|
cursor: pointer; |
|
font-weight: 500; |
|
text-decoration: none; |
|
flex: 1; |
|
text-align: center; |
|
} |
|
|
|
@media (prefers-color-scheme: dark) { |
|
.login-banner { |
|
background-color: var(--light-gray); |
|
border-color: var(--border-color); |
|
} |
|
|
|
.login-banner-close { |
|
border-color: var(--border-color); |
|
background-color: rgba(255, 255, 255, 0.05); |
|
} |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
|
|
<div id="loading-overlay" class="loading-overlay"> |
|
<div class="loader-spinner"></div> |
|
</div> |
|
|
|
<div class="mobile-header"> |
|
<div class="hamburger-menu" onclick="toggleSidebar()"> |
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
<path d="M4 6H20M4 12H20M4 18H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> |
|
</svg> |
|
</div> |
|
<div class="current-page">{% block current_page %}Arena{% endblock %}</div> |
|
</div> |
|
|
|
<div class="backdrop" onclick="toggleSidebar()"></div> |
|
|
|
<div class="sidebar"> |
|
<div class="close-sidebar" onclick="toggleSidebar()"> |
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> |
|
</svg> |
|
</div> |
|
<div class="logo">TTS Arena</div> |
|
<nav> |
|
<a href="{{ url_for('arena') }}" class="nav-item {% if request.path == '/' %}active{% endif %}"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dices"><rect width="12" height="12" x="2" y="10" rx="2" ry="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/><path d="M6 18h.01"/><path d="M10 14h.01"/><path d="M15 6h.01"/><path d="M18 9h.01"/></svg> |
|
Arena |
|
</a> |
|
<a href="{{ url_for('leaderboard') }}" class="nav-item {% if request.path == '/leaderboard' %}active{% endif %}"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trophy"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg> |
|
Leaderboard |
|
</a> |
|
<a href="{{ url_for('about') }}" class="nav-item {% if request.path == '/about' %}active{% endif %}"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> |
|
About |
|
</a> |
|
|
|
|
|
{% if g.is_admin %} |
|
<a href="{{ url_for('admin.index') }}" class="nav-item {% if '/admin' in request.path %}active{% endif %}"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/></svg> |
|
Admin Panel |
|
</a> |
|
{% endif %} |
|
</nav> |
|
|
|
<div class="sidebar-footer"> |
|
{# <a href="https://discord.gg/HB8fMR6GTr" target="_blank" rel="noopener noreferrer" class="discord-link">#} |
|
{# <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 127.14 96.36" fill="currentColor">#} |
|
{# <path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>#} |
|
{# </svg>#} |
|
{# Join our Discord#} |
|
{# </a>#} |
|
|
|
{% if current_user.is_authenticated %} |
|
<div class="user-auth" onclick="toggleUserDropdown(event)"> |
|
<div class="user-name">{{ current_user.username }}</div> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="user-auth-arrow"> |
|
<polyline points="6 9 12 15 18 9"></polyline> |
|
</svg> |
|
|
|
<div class="user-dropdown"> |
|
<a href="{{ url_for('auth.logout') }}" class="dropdown-item"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> |
|
<polyline points="16 17 21 12 16 7"></polyline> |
|
<line x1="21" y1="12" x2="9" y2="12"></line> |
|
</svg> |
|
Logout |
|
</a> |
|
</div> |
|
</div> |
|
{% else %} |
|
<a href="{{ url_for('auth.login', next=request.path) }}" class="login-link"> |
|
<img src="{{ url_for('static', filename='huggingface.svg') }}" alt="Hugging Face"> |
|
Login |
|
</a> |
|
|
|
<div id="login-tip-overlay" class="login-tip-overlay"> |
|
<div class="login-tip-content"> |
|
Log in to track your votes, see personalized leaderboards, and more! |
|
</div> |
|
<div class="login-tip-actions"> |
|
<button class="login-tip-close" onclick="dismissLoginTip()">Don't show again</button> |
|
<a href="{{ url_for('auth.login', next=request.path) }}" class="login-now-btn">Login now</a> |
|
</div> |
|
<div class="login-tip-caret"></div> |
|
</div> |
|
{% endif %} |
|
</div> |
|
</div> |
|
|
|
<div class="main-content"> |
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
{% if messages %} |
|
<div class="flash-messages"> |
|
{% for category, message in messages %} |
|
<script> |
|
document.addEventListener('DOMContentLoaded', function () { |
|
openToast('{{ message }}', '{{ category }}'); |
|
}); |
|
</script> |
|
{% endfor %} |
|
</div> |
|
{% endif %} |
|
{% endwith %} |
|
|
|
<div class="main-content-inner"> |
|
{% block content %}{% endblock %} |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="toast-container" id="toast-container"></div> |
|
|
|
{% if not current_user.is_authenticated %} |
|
|
|
<div id="login-banner" class="login-banner"> |
|
<div class="login-banner-content"> |
|
Log in to track your votes and see personalized leaderboards! |
|
</div> |
|
<div class="login-banner-actions"> |
|
<button class="login-banner-close" onclick="dismissLoginTip()">No thanks</button> |
|
<a href="{{ url_for('auth.login', next=request.path) }}" class="login-banner-btn">Login</a> |
|
</div> |
|
</div> |
|
{% endif %} |
|
|
|
{% block extra_scripts %}{% endblock %} |
|
<script src="https://unpkg.com/@popperjs/core@2"></script> |
|
<script> |
|
function toggleSidebar() { |
|
const sidebar = document.querySelector('.sidebar'); |
|
const backdrop = document.querySelector('.backdrop'); |
|
sidebar.classList.toggle('active'); |
|
backdrop.classList.toggle('active'); |
|
} |
|
|
|
function toggleUserDropdown(event) { |
|
event.stopPropagation(); |
|
const userAuth = document.querySelector('.user-auth'); |
|
const userDropdown = document.querySelector('.user-dropdown'); |
|
userAuth.classList.toggle('active'); |
|
userDropdown.classList.toggle('active'); |
|
} |
|
|
|
|
|
function isLoginTipDismissed() { |
|
try { |
|
return localStorage.getItem('login_tip_dismissed') === 'true'; |
|
} catch (error) { |
|
|
|
console.warn('localStorage access failed:', error); |
|
return false; |
|
} |
|
} |
|
|
|
|
|
function dismissLoginTip() { |
|
try { |
|
|
|
localStorage.setItem('login_tip_dismissed', 'true'); |
|
|
|
|
|
const loginTip = document.getElementById('login-tip-overlay'); |
|
const loginBanner = document.getElementById('login-banner'); |
|
const backdrop = document.querySelector('.login-backdrop'); |
|
|
|
if (loginTip) { |
|
loginTip.classList.remove('show'); |
|
} |
|
|
|
if (loginBanner) { |
|
loginBanner.style.display = 'none'; |
|
} |
|
|
|
if (backdrop) { |
|
backdrop.style.display = 'none'; |
|
} |
|
} catch (error) { |
|
console.warn('localStorage write failed:', error); |
|
|
|
const loginTip = document.getElementById('login-tip-overlay'); |
|
const loginBanner = document.getElementById('login-banner'); |
|
const backdrop = document.querySelector('.login-backdrop'); |
|
|
|
if (loginTip) { |
|
loginTip.classList.remove('show'); |
|
} |
|
|
|
if (loginBanner) { |
|
loginBanner.style.display = 'none'; |
|
} |
|
|
|
if (backdrop) { |
|
backdrop.style.display = 'none'; |
|
} |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
|
const loginTipOverlay = document.getElementById('login-tip-overlay'); |
|
const loginBanner = document.getElementById('login-banner'); |
|
const loginLink = document.querySelector('.login-link'); |
|
|
|
if (loginLink && !isLoginTipDismissed()) { |
|
|
|
if (window.innerWidth <= 768) { |
|
|
|
const backdrop = document.createElement('div'); |
|
backdrop.className = 'login-backdrop'; |
|
backdrop.style.position = 'fixed'; |
|
backdrop.style.top = '0'; |
|
backdrop.style.left = '0'; |
|
backdrop.style.width = '100%'; |
|
backdrop.style.height = '100%'; |
|
backdrop.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; |
|
backdrop.style.zIndex = '9997'; |
|
backdrop.style.display = 'none'; |
|
document.body.appendChild(backdrop); |
|
|
|
|
|
if (loginBanner) { |
|
loginBanner.style.display = 'block'; |
|
backdrop.style.display = 'block'; |
|
|
|
|
|
backdrop.addEventListener('click', function() { |
|
dismissLoginTip(); |
|
}); |
|
} |
|
} else { |
|
|
|
if (loginTipOverlay) { |
|
|
|
const popperInstance = Popper.createPopper(loginLink, loginTipOverlay, { |
|
placement: 'top', |
|
modifiers: [ |
|
{ |
|
name: 'offset', |
|
options: { |
|
offset: [0, 10], |
|
}, |
|
}, |
|
], |
|
}); |
|
|
|
loginTipOverlay.classList.add('show'); |
|
popperInstance.update(); |
|
} |
|
} |
|
} |
|
|
|
|
|
window.addEventListener('resize', function() { |
|
if (isLoginTipDismissed()) return; |
|
|
|
const backdrop = document.querySelector('.login-backdrop'); |
|
|
|
if (window.innerWidth <= 768) { |
|
|
|
if (loginTipOverlay) { |
|
loginTipOverlay.classList.remove('show'); |
|
} |
|
if (loginBanner && backdrop) { |
|
loginBanner.style.display = 'block'; |
|
backdrop.style.display = 'block'; |
|
} |
|
} else { |
|
|
|
if (loginBanner && backdrop) { |
|
loginBanner.style.display = 'none'; |
|
backdrop.style.display = 'none'; |
|
} |
|
if (loginTipOverlay && loginLink) { |
|
const popperInstance = Popper.createPopper(loginLink, loginTipOverlay, { |
|
placement: 'top', |
|
modifiers: [ |
|
{ |
|
name: 'offset', |
|
options: { |
|
offset: [0, 10], |
|
}, |
|
}, |
|
], |
|
}); |
|
|
|
loginTipOverlay.classList.add('show'); |
|
popperInstance.update(); |
|
} |
|
} |
|
}); |
|
|
|
|
|
const loadingOverlay = document.getElementById('loading-overlay'); |
|
loadingOverlay.classList.remove('active'); |
|
|
|
|
|
const originalFetch = window.fetch; |
|
window.fetch = async function (url, options) { |
|
try { |
|
const response = await originalFetch(url, options); |
|
|
|
|
|
if (response.status === 403) { |
|
const data = await response.clone().json(); |
|
if (data && (data.error === "Turnstile verification required" || data.error === "Turnstile verification expired")) { |
|
|
|
window.location.href = "/turnstile?redirect_url=" + encodeURIComponent(window.location.href); |
|
return new Response(JSON.stringify({ redirecting: true }), { |
|
status: 200, |
|
headers: { 'Content-Type': 'application/json' } |
|
}); |
|
} |
|
} |
|
|
|
return response; |
|
} catch (error) { |
|
return Promise.reject(error); |
|
} |
|
}; |
|
}); |
|
|
|
|
|
document.addEventListener('click', function (event) { |
|
const userDropdown = document.querySelector('.user-dropdown'); |
|
const userAuth = document.querySelector('.user-auth'); |
|
if (userDropdown && userAuth && userDropdown.classList.contains('active') && !userAuth.contains(event.target)) { |
|
userAuth.classList.remove('active'); |
|
userDropdown.classList.remove('active'); |
|
} |
|
}); |
|
|
|
|
|
function openToast(message, type = 'info', duration = 5000) { |
|
const toastContainer = document.getElementById('toast-container'); |
|
const toast = document.createElement('div'); |
|
toast.className = `toast ${type}`; |
|
|
|
|
|
let iconSvg = ''; |
|
if (type === 'info') { |
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>'; |
|
} else if (type === 'success') { |
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'; |
|
} else if (type === 'warning') { |
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12" y2="17"/></svg>'; |
|
} else if (type === 'error') { |
|
iconSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'; |
|
} |
|
|
|
toast.innerHTML = ` |
|
<div class="toast-icon">${iconSvg}</div> |
|
<div class="toast-content">${message}</div> |
|
<div class="toast-close" onclick="closeToast(this.parentNode)"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
|
<line x1="18" y1="6" x2="6" y2="18"></line> |
|
<line x1="6" y1="6" x2="18" y2="18"></line> |
|
</svg> |
|
</div> |
|
<div class="toast-progress"></div> |
|
`; |
|
|
|
toastContainer.appendChild(toast); |
|
|
|
|
|
const progressBar = toast.querySelector('.toast-progress'); |
|
progressBar.style.animation = `shrink ${duration / 1000}s linear forwards`; |
|
progressBar.style.transformOrigin = 'left'; |
|
progressBar.style.transform = 'scaleX(1)'; |
|
|
|
|
|
const timeoutId = setTimeout(() => { |
|
closeToast(toast); |
|
}, duration); |
|
|
|
|
|
toast.dataset.timeoutId = timeoutId; |
|
|
|
return toast; |
|
} |
|
|
|
function closeToast(toast) { |
|
|
|
if (toast.dataset.timeoutId) { |
|
clearTimeout(parseInt(toast.dataset.timeoutId)); |
|
} |
|
|
|
|
|
toast.classList.add('slide-out'); |
|
|
|
|
|
setTimeout(() => { |
|
if (toast.parentNode) { |
|
toast.parentNode.removeChild(toast); |
|
} |
|
}, 300); |
|
} |
|
</script> |
|
</body> |
|
|
|
</html> |