WikiLog: collapsible per-commit diff on click

This commit is contained in:
Davide Scaini
2026-05-11 14:56:57 +02:00
parent 5341988e8d
commit b78af43122
+85 -7
View File
@@ -6,29 +6,107 @@ import { SITE_TITLE } from '../../consts';
<Base title={`WikiLog — ${SITE_TITLE}`} description="Ultime modifiche al wiki"> <Base title={`WikiLog — ${SITE_TITLE}`} description="Ultime modifiche al wiki">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<h1 class="text-3xl font-bold mb-8" style="color: var(--text-primary)">WikiLog</h1> <h1 class="text-3xl font-bold mb-8" style="color: var(--text-primary)">WikiLog</h1>
<div id="log-list" class="space-y-2"> <div id="log-list" class="space-y-1">
<p class="text-sm" style="color: var(--text-4)">Caricamento…</p> <p class="text-sm" style="color: var(--text-4)">Caricamento…</p>
</div> </div>
</div> </div>
</Base> </Base>
<script> <script>
function esc(s: string) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderDiff(text: string): string {
return text.split('\n').map(line => {
if (line.startsWith('diff ') || line.startsWith('index ') || line.startsWith('new file') || line.startsWith('deleted file') || line.startsWith('similarity') || line.startsWith('rename')) {
return `<span style="color:var(--text-5)">${esc(line)}</span>`;
}
if (line.startsWith('--- ') || line.startsWith('+++ ')) {
return `<span style="color:var(--text-3)">${esc(line)}</span>`;
}
if (line.startsWith('@@')) {
return `<span style="color:var(--accent);opacity:0.8">${esc(line)}</span>`;
}
if (line.startsWith('+')) {
return `<span style="color:#4ade80;background:rgba(74,222,128,0.07);display:block">${esc(line)}</span>`;
}
if (line.startsWith('-')) {
return `<span style="color:#f87171;background:rgba(248,113,113,0.07);display:block">${esc(line)}</span>`;
}
return `<span style="color:var(--text-4)">${esc(line)}</span>`;
}).join('\n');
}
fetch('/api/log', { credentials: 'include' }) fetch('/api/log', { credentials: 'include' })
.then(r => r.json()) .then(r => r.json())
.then(({ log }) => { .then(({ log }) => {
const el = document.getElementById('log-list'); const el = document.getElementById('log-list');
if (!el) return; if (!el) return;
if (!log?.length) { el.innerHTML = '<p class="text-sm" style="color:var(--text-4)">Nessuna modifica ancora.</p>'; return; } if (!log?.length) {
el.innerHTML = '<p class="text-sm" style="color:var(--text-4)">Nessuna modifica ancora.</p>';
return;
}
el.innerHTML = log.map((e: any) => { el.innerHTML = log.map((e: any) => {
const [author, rest] = e.message.includes(': ') ? e.message.split(': ', 2) : ['', e.message]; const [author, rest] = e.message.includes(': ') ? e.message.split(': ', 2) : ['', e.message];
return ` return `
<div class="flex items-baseline gap-3 px-3 py-2 rounded-lg text-sm" style="background:var(--bg-card)"> <div class="rounded-lg overflow-hidden" style="background:var(--bg-card)">
<span class="font-mono text-xs shrink-0" style="color:var(--text-5)">${e.hash}</span> <button class="log-row w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors hover:brightness-110" data-hash="${e.hash}" aria-expanded="false">
<span class="shrink-0" style="color:var(--accent)">${author || e.author}</span> <span class="font-mono text-xs shrink-0" style="color:var(--text-5)">${e.hash}</span>
<span class="flex-1 truncate" style="color:var(--text-2)">${rest || e.message}</span> <span class="shrink-0 font-medium" style="color:var(--accent)">${author || e.author}</span>
<span class="shrink-0 text-xs" style="color:var(--text-5)">${e.date}</span> <span class="flex-1 min-w-0 truncate" style="color:var(--text-2)">${rest || e.message}</span>
<span class="shrink-0 text-xs" style="color:var(--text-5)">${e.date}</span>
<span class="log-chevron shrink-0 text-xs transition-transform" style="color:var(--text-5)">▶</span>
</button>
<div class="log-diff hidden" data-hash="${e.hash}">
<pre class="text-xs leading-5 px-3 pb-3 overflow-x-auto" style="font-family:'JetBrains Mono',monospace;border-top:1px solid var(--border);padding-top:0.75rem;margin:0"></pre>
</div>
</div>`; </div>`;
}).join(''); }).join('');
el.addEventListener('click', async (ev) => {
const btn = (ev.target as Element).closest<HTMLButtonElement>('.log-row');
if (!btn) return;
const hash = btn.dataset.hash!;
const diffDiv = el.querySelector<HTMLElement>(`.log-diff[data-hash="${hash}"]`);
const pre = diffDiv?.querySelector('pre');
const chevron = btn.querySelector<HTMLElement>('.log-chevron');
if (!diffDiv || !pre) return;
const isOpen = btn.getAttribute('aria-expanded') === 'true';
if (isOpen) {
diffDiv.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
if (chevron) chevron.style.transform = '';
return;
}
btn.setAttribute('aria-expanded', 'true');
if (chevron) chevron.style.transform = 'rotate(90deg)';
diffDiv.classList.remove('hidden');
if (pre.dataset.loaded) return;
pre.textContent = 'Caricamento diff…';
pre.style.color = 'var(--text-5)';
try {
const r = await fetch(`/api/diff/${hash}`, { credentials: 'include' });
if (!r.ok) throw new Error(String(r.status));
const { diff } = await r.json();
pre.dataset.loaded = '1';
if (!diff.trim()) {
pre.textContent = '(nessuna modifica ai file)';
pre.style.color = 'var(--text-5)';
} else {
pre.innerHTML = renderDiff(diff);
pre.style.color = '';
}
} catch {
pre.textContent = 'Errore nel caricamento della diff.';
pre.style.color = '#f87171';
}
});
}) })
.catch(() => { .catch(() => {
const el = document.getElementById('log-list'); const el = document.getElementById('log-list');