Add per-page history panel with collapsible diffs
This commit is contained in:
+142
-5
@@ -15,15 +15,33 @@ const { title, frontmatter, id } = Astro.props;
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{title}</h1>
|
||||
{id && (
|
||||
<button
|
||||
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"
|
||||
id="edit-entry-btn"
|
||||
>Modifica</button>
|
||||
<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
|
||||
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"
|
||||
id="edit-entry-btn"
|
||||
>Modifica</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<BreadCrumbs current={frontmatter.fname} />
|
||||
</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">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -38,4 +56,123 @@ const { title, frontmatter, id } = Astro.props;
|
||||
const slug = (e.currentTarget as HTMLElement).dataset.slug;
|
||||
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user