mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: change theme
This commit is contained in:
parent
aca521b54f
commit
9426b73940
17 changed files with 326 additions and 21 deletions
|
|
@ -102,9 +102,12 @@ nav#main-nav {
|
|||
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--text);
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
|
|
@ -112,6 +115,7 @@ nav#main-nav {
|
|||
}
|
||||
|
||||
.brand .brand-dot {
|
||||
-webkit-text-fill-color: initial;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
|
@ -160,12 +164,94 @@ nav#main-nav {
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-user a {
|
||||
.nav-user > a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-profile-link span { color: var(--text); }
|
||||
|
||||
#theme-toggle {
|
||||
border-radius: 999px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ---------- Avatars ---------- */
|
||||
|
||||
.avatar {
|
||||
border-radius: 999px;
|
||||
object-fit: cover;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar-sm { width: 1.7rem; height: 1.7rem; }
|
||||
.avatar-lg { width: 4.5rem; height: 4.5rem; font-size: 1.4rem; }
|
||||
|
||||
.avatar-initials {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ---------- Profile page ---------- */
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.stat-tile {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
color: #fff;
|
||||
|
|
@ -675,4 +761,6 @@ a.back-link:hover { color: var(--accent); }
|
|||
}
|
||||
.setlist-row .row-note-input { flex-basis: 100%; order: 4; }
|
||||
.setlist-row .row-actions { flex-basis: 100%; order: 5; justify-content: flex-end; }
|
||||
|
||||
.profile-header { flex-direction: column; text-align: center; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Détail du concert — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Historique des concerts — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Répertoire — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
function currentTheme() {
|
||||
const attr = document.documentElement.getAttribute('data-theme');
|
||||
if (attr === 'light' || attr === 'dark') return attr;
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('octane-theme', theme);
|
||||
updateThemeToggleIcon();
|
||||
}
|
||||
|
||||
function updateThemeToggleIcon() {
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (!btn) return;
|
||||
btn.textContent = currentTheme() === 'dark' ? '☀️' : '🌙';
|
||||
}
|
||||
|
||||
async function initNav(activePage) {
|
||||
const me = await api.get('/api/users/me');
|
||||
const nav = document.getElementById('main-nav');
|
||||
|
|
@ -11,7 +29,11 @@ async function initNav(activePage) {
|
|||
<a href="/history.html" class="${activePage === 'history' ? 'active' : ''}">Historique</a>
|
||||
</div>
|
||||
<div class="nav-user">
|
||||
<span>${escapeHtml(me.name)}${me.isAdmin ? ' <span class="badge">admin</span>' : ''}</span>
|
||||
<button type="button" class="icon-btn secondary" id="theme-toggle" title="Changer de thème" aria-label="Changer de thème"></button>
|
||||
<a class="nav-profile-link" href="/profile.html">
|
||||
${avatarHtml(me, 'avatar-sm')}
|
||||
<span>${escapeHtml(me.name)}${me.isAdmin ? ' <span class="badge">admin</span>' : ''}</span>
|
||||
</a>
|
||||
<a href="/auth/logout">Se déconnecter</a>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -21,5 +43,10 @@ async function initNav(activePage) {
|
|||
toggle.addEventListener('click', () => links.classList.toggle('open'));
|
||||
links.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => links.classList.remove('open')));
|
||||
|
||||
updateThemeToggleIcon();
|
||||
document.getElementById('theme-toggle').addEventListener('click', () => {
|
||||
applyTheme(currentTheme() === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
|
||||
return me;
|
||||
}
|
||||
|
|
|
|||
56
public/js/profile.js
Normal file
56
public/js/profile.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('error').innerHTML = `<div class="error-banner">${escapeHtml(message)}</div>`;
|
||||
}
|
||||
|
||||
function statTile(value, label) {
|
||||
return `
|
||||
<div class="stat-tile">
|
||||
<div class="stat-value">${value}</div>
|
||||
<div class="stat-label">${escapeHtml(label)}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
await initNav(null);
|
||||
try {
|
||||
const profile = await api.get('/api/users/me/profile');
|
||||
const container = document.getElementById('content');
|
||||
container.innerHTML = `
|
||||
<div class="profile-header">
|
||||
${avatarHtml(profile, 'avatar-lg')}
|
||||
<div>
|
||||
<h1>${escapeHtml(profile.name)}${profile.isAdmin ? ' <span class="badge">admin</span>' : ''}</h1>
|
||||
${profile.username ? `<div class="card-subtitle">@${escapeHtml(profile.username)}</div>` : ''}
|
||||
${profile.email ? `<div class="card-subtitle">${escapeHtml(profile.email)}</div>` : ''}
|
||||
<div class="card-subtitle">Membre depuis ${formatDate(profile.createdAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${profile.groups && profile.groups.length ? `
|
||||
<h2>Groupes Authentik</h2>
|
||||
<div class="tag-list">
|
||||
${profile.groups.map((g) => `<span class="tag">${escapeHtml(g)}</span>`).join('')}
|
||||
</div>` : ''}
|
||||
|
||||
<h2>Votre activité</h2>
|
||||
<div class="stat-grid">
|
||||
${statTile(profile.stats.songsAdded, 'Morceaux ajoutés')}
|
||||
${statTile(profile.stats.suggestionsProposed, 'Suggestions proposées')}
|
||||
${statTile(profile.stats.votesCast, 'Votes exprimés')}
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<p class="empty">Identité gérée par Authentik — pour changer votre nom, email ou mot de passe, rendez-vous sur votre compte Authentik.</p>
|
||||
<a href="/auth/logout"><button type="button" class="danger">Se déconnecter</button></a>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
})();
|
||||
|
|
@ -39,3 +39,21 @@ function youtubeThumbnailUrl(url) {
|
|||
const videoId = youtubeVideoId(url);
|
||||
return videoId ? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg` : null;
|
||||
}
|
||||
|
||||
function initials(name) {
|
||||
return String(name || '?')
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0].toUpperCase())
|
||||
.join('') || '?';
|
||||
}
|
||||
|
||||
function avatarHtml(user, extraClass) {
|
||||
const cls = `avatar${extraClass ? ` ${extraClass}` : ''}`;
|
||||
if (user.avatarUrl) {
|
||||
return `<img class="${cls}" src="${escapeHtml(user.avatarUrl)}" alt="${escapeHtml(user.name)}" loading="lazy">`;
|
||||
}
|
||||
return `<span class="${cls} avatar-initials">${escapeHtml(initials(user.name))}</span>`;
|
||||
}
|
||||
|
|
|
|||
10
public/js/theme-init.js
Normal file
10
public/js/theme-init.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
(function () {
|
||||
try {
|
||||
var saved = localStorage.getItem('octane-theme');
|
||||
if (saved === 'light' || saved === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
}
|
||||
} catch (e) {
|
||||
/* localStorage unavailable (private mode etc.) — fall back to OS theme */
|
||||
}
|
||||
})();
|
||||
24
public/profile.html
Normal file
24
public/profile.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mon profil — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<nav id="main-nav"></nav>
|
||||
</header>
|
||||
<main>
|
||||
<div id="error"></div>
|
||||
<div id="content">Chargement…</div>
|
||||
</main>
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/render.js"></script>
|
||||
<script src="/js/nav.js"></script>
|
||||
<script src="/js/profile.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Prochain concert — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Suggestions — Octane</title>
|
||||
<script src="/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue