update: change theme

This commit is contained in:
Nathan FONTEYNE 2026-07-08 14:24:23 +02:00
parent aca521b54f
commit 9426b73940
17 changed files with 326 additions and 21 deletions

View file

@ -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
View 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);
}
})();

View file

@ -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
View 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 */
}
})();