frontend/templates/content/repo.html
{% extends "index.html" %}
{% block main_content %}
<div class="jj-repo-info">
<h1>{{ page.title }}</h1>
<div class="stats">
<div class="item">
<svg height="16" width="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path>
</svg>
<span><strong>{{ page.extra.commits }}</strong> commits</span>
</div>
<div class="item">
<svg height="16" width="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5a.75.75 0 0 1 1.5 0Z"></path>
</svg>
<span>Updated <strong>{{ page.extra.last_updated }}</strong></span>
</div>
</div>
<div class="actions">
<button onclick="copyToClipboard('git://hennzau.fr/{{ page.title | safe }}.git', this)">Clone</button>
<button onclick="copyToClipboard('jj@hennzau.fr/{{ page.title | safe }}.git', this)">Edit</button>
</div>
</div>
<script>
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;
button.textContent = '✓';
setTimeout(() => {
button.textContent = originalText;
}, 200);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
</script>
<div class="explorer" id="explorer">
</div>
<script>
let treeData = null;
let currentPath = [];
async function loadTree() {
const response = await fetch(`/jj/{{ page.title | safe }}.json`);
treeData = await response.json();
handleHash();
}
function handleHash() {
const hash = window.location.hash.slice(1);
if (!hash) {
currentPath = [];
renderExplorer();
return;
}
const node = findNode(treeData, hash);
if (node?.type === 'file') {
currentPath = hash.split('/').slice(0, -1);
renderExplorer(node.content, node.name);
} else if (node?.type === 'dir') {
currentPath = hash.split('/').filter(Boolean);
renderExplorer();
} else {
renderExplorer();
}
}
function findNode(node, path) {
if (node.path === path) return node;
if (node.children) {
for (const child of node.children) {
const found = findNode(child, path);
if (found) return found;
}
}
return null;
}
function getNodeAt(pathStack) {
let node = treeData;
for (const name of pathStack) {
node = node.children?.find(c => c.name === name && c.type === 'dir');
if (!node) return treeData;
}
return node;
}
function renderExplorer(fileContent = null, fileName = null) {
const container = document.getElementById('explorer');
container.innerHTML = '';
const crumb = document.createElement('div');
crumb.className = 'breadcrumb';
let crumbHtml = `<a data-crumb="-1">root</a>`;
currentPath.forEach((name, i) => {
crumbHtml += ` / <a data-crumb="${i}">${name}</a>`;
});
crumb.innerHTML = crumbHtml;
crumb.querySelectorAll('a[data-crumb]').forEach(a => {
a.addEventListener('click', () => {
const idx = parseInt(a.dataset.crumb);
currentPath = idx === -1 ? [] : currentPath.slice(0, idx + 1);
window.location.hash = currentPath.join('/');
});
});
container.appendChild(crumb);
const currentNode = getNodeAt(currentPath);
const table = document.createElement('table');
table.className = 'table';
if (currentPath.length > 0) {
const tr = document.createElement('tr');
tr.className = "dir";
tr.innerHTML = `<td><strong>..</strong></td>`;
tr.addEventListener('click', () => {
currentPath.pop();
window.location.hash = currentPath.join('/');
});
table.appendChild(tr);
}
const dirs = currentNode.children?.filter(c => c.type === 'dir') || [];
const files = currentNode.children?.filter(c => c.type === 'file') || [];
dirs.forEach(child => {
const tr = document.createElement('tr');
tr.className = "dir";
tr.innerHTML = `<td><strong>${child.name}/</strong></td>`;
tr.addEventListener('click', () => {
window.location.hash = [...currentPath, child.name].join('/');
});
table.appendChild(tr);
});
files.forEach(child => {
const tr = document.createElement('tr');
tr.className = "file";
tr.innerHTML = `<td>${child.name}</td>`;
tr.addEventListener('click', () => {
window.location.hash = child.path;
});
table.appendChild(tr);
});
container.appendChild(table);
if (fileContent !== null) {
const viewer = document.createElement('div');
viewer.className = 'viewer';
viewer.innerHTML = `
<div class="header">${fileName}</div>
<pre class="line-numbers"><code>${escapeHtml(fileContent)}</code></pre>
`;
container.appendChild(viewer);
hljs.highlightElement(viewer.querySelector('code'));
hljs.lineNumbersBlock(viewer.querySelector('code'))
}
}
function escapeHtml(str) {
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
window.addEventListener('hashchange', handleHash);
loadTree();
</script>
{% endblock %}