Add per-page history panel with collapsible diffs

This commit is contained in:
Davide Scaini
2026-05-21 22:42:06 +02:00
parent 64d16dbbdf
commit e6b9ba56b1
+138 -1
View File
@@ -15,15 +15,33 @@ const { title, frontmatter, id } = Astro.props;
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{title}</h1> <h1 class="text-3xl font-bold" style="color: var(--text-primary)">{title}</h1>
{id && ( {id && (
<div class="flex items-center gap-2 mt-1 shrink-0">
<button
id="history-btn"
data-slug={id}
class="text-xs text-zinc-500 hover:text-white transition-colors px-2 py-1 rounded border border-zinc-700 hover:border-zinc-500"
>Cronologia</button>
<button <button
data-slug={id} data-slug={id}
class="shrink-0 text-xs text-zinc-500 hover:text-white transition-colors px-2 py-1 rounded border border-zinc-700 hover:border-zinc-500 mt-1" class="text-xs text-zinc-500 hover:text-white transition-colors px-2 py-1 rounded border border-zinc-700 hover:border-zinc-500"
id="edit-entry-btn" id="edit-entry-btn"
>Modifica</button> >Modifica</button>
</div>
)} )}
</div> </div>
<BreadCrumbs current={frontmatter.fname} /> <BreadCrumbs current={frontmatter.fname} />
</div> </div>
{id && (
<div id="page-history" class="hidden mb-6 rounded-lg overflow-hidden" style="background:var(--bg-card)">
<div class="px-3 py-2 border-b flex items-center justify-between" style="border-color:var(--border)">
<span class="text-sm font-medium" style="color:var(--text-3)">Cronologia modifiche</span>
<button id="history-close" class="text-xs hover:text-white transition-colors" style="color:var(--text-5)">✕</button>
</div>
<div id="history-list" class="p-2 space-y-1">
<p class="text-sm px-2 py-1" style="color:var(--text-4)">Caricamento…</p>
</div>
</div>
)}
<div class="prose-wiki"> <div class="prose-wiki">
<slot /> <slot />
</div> </div>
@@ -38,4 +56,123 @@ const { title, frontmatter, id } = Astro.props;
const slug = (e.currentTarget as HTMLElement).dataset.slug; const slug = (e.currentTarget as HTMLElement).dataset.slug;
window.dispatchEvent(new CustomEvent('open-editor', { detail: { slug, apiBase: '/pages' } })); window.dispatchEvent(new CustomEvent('open-editor', { detail: { slug, apiBase: '/pages' } }));
}); });
const historyBtn = document.getElementById('history-btn') as HTMLElement | null;
const historyPanel = document.getElementById('page-history') as HTMLElement | null;
const historyList = document.getElementById('history-list') as HTMLElement | null;
const historyClose = document.getElementById('history-close') as HTMLElement | null;
if (historyBtn && historyPanel && historyList) {
let loaded = false;
const slug = historyBtn.dataset.slug!;
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');
}
historyBtn.addEventListener('click', async () => {
if (!historyPanel.classList.contains('hidden')) {
historyPanel.classList.add('hidden');
return;
}
historyPanel.classList.remove('hidden');
if (loaded) return;
loaded = true;
try {
const r = await fetch(`/api/history/${slug}`, { credentials: 'include' });
if (!r.ok) throw new Error(String(r.status));
const { log } = await r.json();
if (!log?.length) {
historyList.innerHTML = '<p class="text-sm px-2 py-1" style="color:var(--text-4)">Nessuna modifica registrata.</p>';
return;
}
historyList.innerHTML = log.map((e: any) => {
const [author, rest] = e.message.includes(': ') ? e.message.split(': ', 2) : ['', e.message];
return `
<div class="rounded overflow-hidden mb-1">
<button class="hist-row w-full flex items-center gap-3 px-2 py-1.5 text-sm text-left transition-colors hover:brightness-110 rounded" data-hash="${e.hash}" aria-expanded="false" style="background:var(--bg-elevated)">
<span class="font-mono text-xs shrink-0" style="color:var(--text-5)">${e.hash}</span>
<span class="shrink-0 font-medium text-xs" style="color:var(--accent)">${esc(author || e.author)}</span>
<span class="flex-1 min-w-0 truncate text-xs" style="color:var(--text-2)">${esc(rest || e.message)}</span>
<span class="shrink-0 text-xs" style="color:var(--text-5)">${e.date}</span>
<span class="hist-chevron shrink-0 text-xs transition-transform" style="color:var(--text-5)">▶</span>
</button>
<div class="hist-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;background:var(--bg-elevated)"></pre>
</div>
</div>`;
}).join('');
historyList.addEventListener('click', async (ev) => {
const btn = (ev.target as Element).closest<HTMLButtonElement>('.hist-row');
if (!btn) return;
const hash = btn.dataset.hash!;
const diffDiv = historyList.querySelector<HTMLElement>(`.hist-diff[data-hash="${hash}"]`);
const pre = diffDiv?.querySelector('pre');
const chevron = btn.querySelector<HTMLElement>('.hist-chevron');
if (!diffDiv || !pre) return;
if (btn.getAttribute('aria-expanded') === 'true') {
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 diffUrl = `/api/diff/${hash}?file=pages/${encodeURIComponent(slug)}.md`;
const dr = await fetch(diffUrl, { credentials: 'include' });
if (!dr.ok) throw new Error(String(dr.status));
const { diff } = await dr.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 {
historyList.innerHTML = '<p class="text-sm px-2 py-1" style="color:#f87171">Errore nel caricamento.</p>';
}
});
historyClose?.addEventListener('click', () => {
historyPanel.classList.add('hidden');
});
}
</script> </script>