296 lines
9.3 KiB
HTML
296 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Dereth Tracker - Login</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #000;
|
|
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
|
|
color: #b0aaa0;
|
|
}
|
|
|
|
.logo-container {
|
|
margin-bottom: 24px;
|
|
}
|
|
.logo-container img {
|
|
display: block;
|
|
width: 400px;
|
|
height: auto;
|
|
}
|
|
|
|
.quote-container {
|
|
height: 44px;
|
|
margin-bottom: 22px;
|
|
text-align: center;
|
|
width: 500px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.quote-text {
|
|
font-size: 0.78rem;
|
|
font-style: italic;
|
|
color: #706858;
|
|
line-height: 1.4;
|
|
opacity: 0;
|
|
transition: opacity 0.8s ease;
|
|
}
|
|
.quote-text.visible { opacity: 1; }
|
|
.quote-attribution {
|
|
display: block;
|
|
font-size: 0.58rem;
|
|
font-style: normal;
|
|
color: #484034;
|
|
margin-top: 4px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.login-card {
|
|
width: 440px;
|
|
background: #0c0c0c;
|
|
border: 1px solid #2a2520;
|
|
border-radius: 4px;
|
|
padding: 24px 28px 20px;
|
|
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
.login-title {
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
font-size: 0.75rem;
|
|
color: #706050;
|
|
text-transform: uppercase;
|
|
letter-spacing: 3px;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.form-field {
|
|
flex: 1;
|
|
}
|
|
.form-field label {
|
|
display: block;
|
|
font-size: 0.7rem;
|
|
color: #585048;
|
|
margin-bottom: 5px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
.form-field input {
|
|
width: 100%;
|
|
padding: 9px 11px;
|
|
font-size: 0.9rem;
|
|
font-family: inherit;
|
|
background: #060606;
|
|
color: #c0b8a8;
|
|
border: 1px solid #2a2520;
|
|
border-radius: 3px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
.form-field input:focus {
|
|
border-color: #8a7a50;
|
|
box-shadow: 0 0 6px rgba(138, 122, 80, 0.15);
|
|
}
|
|
.form-field input::placeholder {
|
|
color: #2a2520;
|
|
}
|
|
|
|
.login-btn {
|
|
width: 100%;
|
|
padding: 10px;
|
|
font-family: inherit;
|
|
font-size: 0.85rem;
|
|
font-weight: bold;
|
|
color: #0c0c0c;
|
|
background: linear-gradient(180deg, #9a8a60 0%, #706040 100%);
|
|
border: 1px solid #5a5038;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
transition: background 0.2s, box-shadow 0.2s;
|
|
}
|
|
.login-btn:hover {
|
|
background: linear-gradient(180deg, #b0a070 0%, #8a7a50 100%);
|
|
box-shadow: 0 2px 8px rgba(138, 122, 80, 0.25);
|
|
}
|
|
.login-btn:active {
|
|
background: linear-gradient(180deg, #706040 0%, #5a5038 100%);
|
|
}
|
|
|
|
.login-error {
|
|
margin-top: 12px;
|
|
padding: 8px;
|
|
text-align: center;
|
|
font-size: 0.8rem;
|
|
color: #cc5555;
|
|
background: rgba(200, 50, 50, 0.06);
|
|
border: 1px solid rgba(200, 50, 50, 0.15);
|
|
border-radius: 3px;
|
|
display: none;
|
|
}
|
|
|
|
.login-footer {
|
|
margin-top: 16px;
|
|
text-align: center;
|
|
font-size: 0.6rem;
|
|
color: #2a2520;
|
|
letter-spacing: 1px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="logo-container">
|
|
<img src="/icons/06001343.png" alt="Asheron's Call">
|
|
</div>
|
|
|
|
<div class="quote-container">
|
|
<div class="quote-text" id="quoteText"></div>
|
|
</div>
|
|
|
|
<div class="login-card">
|
|
<div class="login-title">Dereth Tracker</div>
|
|
|
|
<form id="loginForm" onsubmit="return handleLogin(event)">
|
|
<div class="form-row">
|
|
<div class="form-field">
|
|
<label for="username">Username</label>
|
|
<input type="text" id="username" name="username" autocomplete="username" autofocus required>
|
|
</div>
|
|
<div class="form-field">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" autocomplete="current-password" required>
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="login-btn" id="loginBtn">Enter Dereth</button>
|
|
<div class="login-error" id="loginError"></div>
|
|
</form>
|
|
|
|
<div class="login-footer">Mosswart Enjoyers Club</div>
|
|
</div>
|
|
|
|
<script>
|
|
const alexQuotes = [
|
|
"I don't run late. I glide fashionably through time.",
|
|
"Time is a flat circle\u2014and I'm still trying to find the starting point.",
|
|
"Being on time is like spotting a unicorn: rare, beautiful, and definitely not me.",
|
|
"Time zones are a social construct. I operate on instinct.",
|
|
"Every time I try to be early, time responds with traffic.",
|
|
"Time is an illusion. My ETA is performance art.",
|
|
"I treat deadlines like speed limits: suggestions.",
|
|
"I don't set alarms. I set vague intentions.",
|
|
"If you want me to be on time, lie about when it starts.",
|
|
"Why stress about time when you can just embrace chaos?",
|
|
"My to-do list is a historical document.",
|
|
"Time and I broke up. It was toxic.",
|
|
"I'm not late. I'm temporally creative.",
|
|
"'In five minutes' means something different in my language.",
|
|
"I don't track time. I let it track me\u2014poorly.",
|
|
"Time management is easy. Just avoid doing anything.",
|
|
"Time is fleeting. So is my attention span.",
|
|
"I live in the moment. Just never the right one.",
|
|
"Time doesn't control me. It just heavily inconveniences me.",
|
|
"I'm a time optimist: always wrong, always hopeful.",
|
|
"Punctuality is for people who don't trust spontaneity.",
|
|
"My life is a series of missed trains and strong coffee.",
|
|
"If time is a river, I'm definitely upstream without a paddle.",
|
|
"I don't watch the clock. I avoid eye contact with it.",
|
|
"'ASAP' means 'as soon as procrastination ends.'",
|
|
"Early is suspicious. On time is impressive. Late is expected.",
|
|
"Time management? I prefer time improvisation.",
|
|
"I like to keep time on its toes. Mostly by ignoring it.",
|
|
"Alarms are like plot twists. I didn't see it coming, and I still ignore it.",
|
|
"If being late was a sport, I'd already have missed the medal ceremony.",
|
|
"I don't lose track of time. I just pretend it doesn't exist.",
|
|
"Watches are decorative lies.",
|
|
"Time flies. I miss every flight.",
|
|
"My planner is just a coloring book for stress.",
|
|
"I'm not in a hurry. I'm in denial.",
|
|
"I live in the now, just usually a little bit after everyone else.",
|
|
"My calendar and I are estranged, but we're working on it.",
|
|
"The early bird catches the worm. I order food later.",
|
|
"Time moves fast. I move slower.",
|
|
"I was going to be on time, but then I remembered who I am.",
|
|
"I see '9:00 AM' and read it as 'guideline.'",
|
|
"Every plan is a maybe with extra steps.",
|
|
"I don't have time blindness\u2014I just have time indifference.",
|
|
"I'm not late. Reality is early.",
|
|
"Time and I aren't speaking after what happened last Monday.",
|
|
"Being on time is impressive. Being consistently late is a brand.",
|
|
"I'm in sync with the universe\u2014just in a different dimension.",
|
|
"My sense of time is like my sock drawer: chaotic and mostly missing.",
|
|
"Some people chase time. I let it wander off.",
|
|
"I'm not running late. I'm setting the tone for a relaxed experience.",
|
|
];
|
|
|
|
function shuffle(arr) {
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
const shuffled = shuffle([...alexQuotes]);
|
|
let idx = 0;
|
|
const el = document.getElementById('quoteText');
|
|
|
|
function showQuote() {
|
|
el.classList.remove('visible');
|
|
setTimeout(() => {
|
|
if (idx >= shuffled.length) { idx = 0; shuffle(shuffled); }
|
|
el.innerHTML = '\u201C' + shuffled[idx] + '\u201D'
|
|
+ '<span class="quote-attribution">\u2014 Time according to Alex</span>';
|
|
idx++;
|
|
el.classList.add('visible');
|
|
}, 800);
|
|
}
|
|
|
|
showQuote();
|
|
setInterval(showQuote, 5000);
|
|
|
|
async function handleLogin(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('loginBtn');
|
|
const errDiv = document.getElementById('loginError');
|
|
errDiv.style.display = 'none';
|
|
btn.textContent = 'Authenticating...';
|
|
btn.disabled = true;
|
|
|
|
try {
|
|
const resp = await fetch('/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: document.getElementById('username').value,
|
|
password: document.getElementById('password').value,
|
|
}),
|
|
});
|
|
if (resp.ok) {
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
const data = await resp.json();
|
|
errDiv.textContent = data.detail || 'Login failed';
|
|
errDiv.style.display = 'block';
|
|
} catch (err) {
|
|
errDiv.textContent = 'Connection error';
|
|
errDiv.style.display = 'block';
|
|
}
|
|
btn.textContent = 'Enter Dereth';
|
|
btn.disabled = false;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|