Files
bourbonacci/frontend/index.html
derekc f1b82baebd Overhaul nav, fix DB transaction bugs, add admin UI
- Replace nav user area with display name (non-clickable), gear settings
  modal, admin button (admins only), and logout button
- Settings modal handles display name, timezone, and password change
- Add admin.html + admin.js: user table with reset PW, disable/enable,
  login-as (impersonation), and delete; return-to-admin flow in nav
- Add is_admin to UserResponse so frontend can gate the Admin button
- Fix all db.begin() bugs in admin.py and users.py (transaction already
  active from get_current_user query; use commit() directly instead)
- Add email-validator and pin bcrypt==4.0.1 for passlib compatibility
- Add escHtml() to api.js and admin API namespace
- Group nav brand + links in nav-left for left-aligned layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:09:38 -07:00

95 lines
3.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bourbonacci — Infinity Bottle Tracker</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥃</text></svg>" />
<link rel="stylesheet" href="/css/style.css" />
</head>
<body>
<nav>
<div class="nav-left">
<a href="/index.html" class="nav-brand">🥃 Bourbonacci</a>
<div class="nav-links" id="nav-links"></div>
</div>
<div id="nav-user"></div>
</nav>
<main>
<div class="hero">
<h1>The Infinity Bottle</h1>
<p>One pour from every bottle. An ever-evolving blend that grows richer with every addition.</p>
<a href="/login.html" class="btn btn-primary" id="hero-cta">Track Your Bottle</a>
</div>
<div class="section-header">
<h2>Community Bottles</h2>
</div>
<div id="user-cards" class="user-cards">
<div class="empty"><div class="empty-icon"></div><p>Loading...</p></div>
</div>
</main>
<script src="/js/api.js"></script>
<script src="/js/auth.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await Auth.renderNav('home');
if (Auth.isLoggedIn()) {
document.getElementById('hero-cta').textContent = 'Go to My Bottle';
document.getElementById('hero-cta').href = '/dashboard.html';
}
const container = document.getElementById('user-cards');
try {
const stats = await API.public.stats();
if (stats.length === 0) {
container.innerHTML = `<div class="empty"><div class="empty-icon">🥃</div><p>No bottles yet — be the first to register!</p></div>`;
return;
}
container.innerHTML = stats.map(u => {
const maxShots = 25;
const pct = Math.min(100, (u.current_total_shots / maxShots) * 100);
const proof = u.estimated_proof != null ? `${u.estimated_proof}` : '—';
return `
<div class="user-card">
<div class="user-card-name">${escHtml(u.display_name)}</div>
<div class="stats-grid" style="margin-bottom:.75rem">
<div class="stat-box">
<span class="stat-value">${u.total_add_entries}</span>
<span class="stat-label">Bourbons</span>
</div>
<div class="stat-box">
<span class="stat-value">${proof}</span>
<span class="stat-label">Est. Proof</span>
</div>
<div class="stat-box">
<span class="stat-value">${u.current_total_shots}</span>
<span class="stat-label">Shots Left</span>
</div>
</div>
<div class="bottle-bar-wrap">
<div class="bottle-bar" style="width:${pct}%"></div>
</div>
<div class="bottle-label">${u.current_total_shots} shots remaining</div>
</div>`;
}).join('');
} catch (err) {
container.innerHTML = `<div class="alert alert-error">Could not load stats: ${err.message}</div>`;
}
});
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
</body>
</html>