hennzau

21 commits
Updated 2026-04-25 12:39:37
frontend/templates/content
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

window.addEventListener('hashchange', handleHash);
loadTree();
</script>

{% endblock %}