mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
273 lines
15 KiB
HTML
273 lines
15 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Fotbal Club — All Diagrams</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0f1115;--panel:#0f131f;--text:#e8eaf0;--muted:#9aa3b2;--primary:#0b5cff;--border:#212736;--badge:#1b2440;
|
|
}
|
|
html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
|
|
header{position:sticky;top:0;z-index:10;background:#0d1017;border-bottom:1px solid var(--border);padding:12px 16px}
|
|
header h1{margin:0;font-size:16px;font-weight:700}
|
|
.filters{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px}
|
|
.filters input[type="search"], .filters select{background:#0f1420;color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 10px}
|
|
.filters .chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:16px;padding:6px 10px;background:#0f1420;cursor:pointer}
|
|
.filters .chip input{margin:0}
|
|
.filters .sp{flex:1}
|
|
.btn{appearance:none;border:1px solid var(--border);background:#11182a;color:var(--text);padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px}
|
|
.btn.primary{background:var(--primary);border-color:var(--primary);color:#fff}
|
|
.btn.ghost{background:transparent}
|
|
main{padding:16px}
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(640px,1fr));gap:16px}
|
|
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
|
|
.card header{display:flex;align-items:center;gap:8px;justify-content:space-between;background:#0f131f;border-bottom:1px solid var(--border);padding:10px 12px;position:static}
|
|
.title{display:flex;flex-direction:column;gap:4px}
|
|
.title h2{margin:0;font-size:14px}
|
|
.meta{color:var(--muted);font-size:11px}
|
|
.badge{background:var(--badge);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:10px;color:#bcd}
|
|
.diagram-wrap{position:relative;overflow:auto;min-height:320px;background:#fff}
|
|
.diagram{padding:16px;min-height:320px}
|
|
.toolbar{display:flex;gap:8px;align-items:center;padding:8px 12px;border-top:1px solid var(--border);background:#0f131f;flex-wrap:wrap}
|
|
.toolbar .sp{flex:1}
|
|
.diagram svg{max-width:100%;height:auto;background:#fff}
|
|
.diagram svg text{fill:#111827 !important}
|
|
.diagram svg .edgePath path, .diagram svg .flowchart-link{stroke:#334155 !important}
|
|
.diagram svg .node > * { transition: filter .2s ease }
|
|
.diagram svg .node:hover { filter: drop-shadow(0 0 6px var(--primary)) }
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
|
|
<script>
|
|
mermaid.initialize({ startOnLoad:false, securityLevel:'loose', theme:'dark', flowchart:{ curve:'linear', useMaxWidth:true } });
|
|
|
|
async function renderMermaidFile(mmdPath, container){
|
|
try{
|
|
container.innerHTML = '<div style="padding:16px;color:#9aa3b2">Loading '+mmdPath+'…</div>';
|
|
const res = await fetch(mmdPath + '?v=' + Date.now(), { cache: 'no-store' });
|
|
if(!res.ok) throw new Error('Failed to load '+mmdPath+': '+res.status);
|
|
const raw = await res.text();
|
|
const code = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
const id = 'm-'+Math.random().toString(36).slice(2);
|
|
const { svg } = await mermaid.render(id, code);
|
|
container.innerHTML = svg;
|
|
const svgEl = container.querySelector('svg');
|
|
if(svgEl){
|
|
svgEl.style.maxWidth = '100%';
|
|
svgEl.style.width = '100%';
|
|
svgEl.style.height = 'auto';
|
|
svgEl.removeAttribute('width');
|
|
svgEl.removeAttribute('height');
|
|
}
|
|
}catch(e){ container.innerHTML = '<div style="padding:16px;color:#ef4444">Error: '+(e && e.message ? e.message : e)+'</div>'; }
|
|
}
|
|
|
|
function downloadSVGOf(container, filename){
|
|
const svg = container.querySelector('svg');
|
|
if(!svg) return;
|
|
const serializer = new XMLSerializer();
|
|
let source = serializer.serializeToString(svg);
|
|
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
source = '<?xml version="1.0" standalone="no"?>\n'+source;
|
|
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a'); a.href = url; a.download = filename; a.click();
|
|
setTimeout(() => URL.revokeObjectURL(url), 4000);
|
|
}
|
|
|
|
function openSVGInNewTab(container){
|
|
const svg = container.querySelector('svg');
|
|
if(!svg) return;
|
|
const serializer = new XMLSerializer();
|
|
let source = serializer.serializeToString(svg);
|
|
if(!source.match(/^<svg[^>]+xmlns=/)) source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
source = '<?xml version="1.0" standalone="no"?>\n'+source;
|
|
// Inject white background and readable styles for new tab view
|
|
const svgStart = source.indexOf('<svg');
|
|
if(svgStart !== -1){
|
|
const svgTagEnd = source.indexOf('>', svgStart);
|
|
if(svgTagEnd !== -1){
|
|
const inject = '<rect width="100%" height="100%" fill="#ffffff"/><style>text{fill:#111827}.edgePath path,.flowchart-link{stroke:#334155}</style>';
|
|
source = source.slice(0, svgTagEnd+1) + inject + source.slice(svgTagEnd+1);
|
|
}
|
|
}
|
|
const blob = new Blob([source], { type:'image/svg+xml;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
|
}
|
|
|
|
const ALL_DIAGRAMS = [
|
|
// System & DB
|
|
{ id:'system-clean', label:'System Overview', file:'system-overall-clean.mmd', cat:'System', tags:['overview','recommended','big'] },
|
|
{ id:'db-er', label:'Database ER', file:'db-er.mmd', cat:'System', tags:['db'] },
|
|
{ id:'db-models', label:'Database Models', file:'db-models.mmd', cat:'System', tags:['db'] },
|
|
// Backend
|
|
{ id:'be-routes', label:'Backend Routes Overview', file:'backend-routes-overview.mmd', cat:'Backend', tags:['routes'] },
|
|
{ id:'be-packages', label:'Backend Packages', file:'backend-packages.mmd', cat:'Backend', tags:['packages'] },
|
|
{ id:'be-mw', label:'Backend Middleware Pipeline', file:'backend-middleware-pipeline.mmd', cat:'Backend', tags:['middleware'] },
|
|
{ id:'be-jobs', label:'Backend Background Jobs', file:'backend-jobs.mmd', cat:'Backend', tags:['jobs'] },
|
|
{ id:'auth', label:'Auth Flow', file:'auth-flow.mmd', cat:'Backend', tags:['auth','flow'] },
|
|
{ id:'err-flow', label:'Error Tracking Flow', file:'error-tracking-flow.mmd', cat:'Backend', tags:['errors','flow'] },
|
|
// Frontend
|
|
{ id:'fe-everything', label:'Frontend — Everything (Big)', file:'frontend-everything.mmd', cat:'Frontend', tags:['overview','big','recommended'], defaultWires:'faint' },
|
|
{ id:'fe-overall', label:'Frontend — Overall', file:'frontend-overall.mmd', cat:'Frontend', tags:['architecture','recommended'] },
|
|
{ id:'fe-routes', label:'Frontend — Routes', file:'frontend-routes.mmd', cat:'Frontend', tags:['routes'] },
|
|
{ id:'fe-home', label:'Frontend — Homepage', file:'frontend-homepage.mmd', cat:'Frontend', tags:['homepage'] },
|
|
{ id:'fe-api', label:'Frontend — API Map', file:'frontend-api-map.mmd', cat:'Frontend', tags:['api'] },
|
|
// Admin
|
|
{ id:'admin-overall', label:'Admin — Overall', file:'admin-overall.mmd', cat:'Admin', tags:['admin','overview','recommended'], defaultWires:'faint' },
|
|
{ id:'scoreboard', label:'Scoreboard Flow', file:'scoreboard-flow.mmd', cat:'Admin', tags:['scoreboard','flow'] },
|
|
{ id:'newsletter', label:'Newsletter Flow', file:'newsletter-flow.mmd', cat:'Admin', tags:['newsletter','flow'] },
|
|
{ id:'comments', label:'Comments Flow', file:'comments-flow.mmd', cat:'Admin', tags:['comments','flow'] },
|
|
{ id:'gallery-zonerama', label:'Gallery (Zonerama) Flow', file:'gallery-zonerama-flow.mmd', cat:'Admin', tags:['gallery','flow'] },
|
|
{ id:'shortlinks', label:'Shortlinks Flow', file:'shortlinks-flow.mmd', cat:'Admin', tags:['shortlinks','flow'] },
|
|
{ id:'upload-flow', label:'Upload Flow', file:'upload-flow.mmd', cat:'Admin', tags:['upload','flow'] },
|
|
];
|
|
|
|
function createCard(d){
|
|
const sec = document.createElement('section');
|
|
sec.className = 'card';
|
|
sec.id = 'card-'+d.id;
|
|
sec.dataset.cat = d.cat;
|
|
sec.dataset.tags = (d.tags||[]).join(',');
|
|
// No default wires styling; keep full visibility
|
|
|
|
const h = document.createElement('header');
|
|
const title = document.createElement('div'); title.className = 'title';
|
|
const h2 = document.createElement('h2'); h2.textContent = d.label; title.appendChild(h2);
|
|
const meta = document.createElement('div'); meta.className='meta'; meta.textContent = d.file + ' • ' + d.cat; title.appendChild(meta);
|
|
const right = document.createElement('div');
|
|
const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = d.cat; right.appendChild(badge);
|
|
h.appendChild(title); h.appendChild(right);
|
|
|
|
const wrap = document.createElement('div'); wrap.className='diagram-wrap';
|
|
const diag = document.createElement('div'); diag.className='diagram'; diag.dataset.file = d.file; wrap.appendChild(diag);
|
|
|
|
const tb = document.createElement('div'); tb.className='toolbar';
|
|
tb.innerHTML = `
|
|
<label style="display:inline-flex;align-items:center;gap:8px"><input type="checkbox" class="fit" checked> Fit width</label>
|
|
<label style="display:inline-flex;align-items:center;gap:6px">Zoom <input class="zoom" type="range" min="50" max="300" value="100" style="width:140px"></label>
|
|
<a class="btn ghost src" href="${d.file}" target="_blank">Source</a>
|
|
<span class="sp"></span>
|
|
<button class="btn open">Open SVG in new tab</button>
|
|
<button class="btn refresh">Refresh</button>
|
|
<button class="btn primary download">Download SVG</button>`;
|
|
|
|
sec.appendChild(h); sec.appendChild(wrap); sec.appendChild(tb);
|
|
return sec;
|
|
}
|
|
|
|
function applyFitZoomFor(card){
|
|
const container = card.querySelector('.diagram');
|
|
const svg = container?.querySelector('svg');
|
|
if(!svg) return;
|
|
const fit = card.querySelector('.fit');
|
|
const zoom = card.querySelector('.zoom');
|
|
if(fit && fit.checked){
|
|
svg.style.width='100%'; svg.style.height='auto';
|
|
svg.style.transformOrigin = '';
|
|
svg.style.transform = '';
|
|
} else {
|
|
svg.style.width=''; svg.style.height='';
|
|
const z = Math.max(50, Math.min(300, parseInt(zoom?.value || '100', 10)));
|
|
svg.style.transformOrigin = 'top left';
|
|
svg.style.transform = 'scale('+(z/100)+')';
|
|
}
|
|
}
|
|
|
|
function wireCardControls(card, file){
|
|
const diag = card.querySelector('.diagram');
|
|
card.dataset.file = file;
|
|
const fit = card.querySelector('.fit');
|
|
const openBtn = card.querySelector('.open');
|
|
const refresh = card.querySelector('.refresh');
|
|
const download = card.querySelector('.download');
|
|
const zoom = card.querySelector('.zoom');
|
|
fit.addEventListener('change', () => applyFitZoomFor(card));
|
|
zoom.addEventListener('input', () => applyFitZoomFor(card));
|
|
openBtn.addEventListener('click', () => openSVGInNewTab(diag));
|
|
refresh.addEventListener('click', async () => { diag.dataset.rendered=''; await renderMermaidFile(file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); });
|
|
download.addEventListener('click', () => downloadSVGOf(diag, (file.replace('.mmd','')||'diagram')+'.svg'));
|
|
}
|
|
|
|
let observer;
|
|
function setupObserver(){
|
|
if(observer) observer.disconnect();
|
|
observer = new IntersectionObserver(entries => {
|
|
entries.forEach(async e => {
|
|
if(e.isIntersecting){
|
|
const card = e.target; const diag = card.querySelector('.diagram');
|
|
if(diag && !diag.dataset.rendered){
|
|
await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card);
|
|
}
|
|
}
|
|
});
|
|
}, { root:null, rootMargin:'200px', threshold:0 });
|
|
document.querySelectorAll('.card').forEach(c => observer.observe(c));
|
|
}
|
|
|
|
function buildGrid(){
|
|
const grid = document.getElementById('grid');
|
|
grid.innerHTML='';
|
|
for(const d of ALL_DIAGRAMS){ const card = createCard(d); grid.appendChild(card); wireCardControls(card, d.file); }
|
|
setupObserver();
|
|
}
|
|
|
|
function applyFilters(){
|
|
const q = (document.getElementById('search').value || '').toLowerCase();
|
|
const cats = Array.from(document.querySelectorAll('input[name="cat-filter"]:checked')).map(i=>i.value);
|
|
document.querySelectorAll('.card').forEach(card => {
|
|
const label = card.querySelector('h2').textContent.toLowerCase();
|
|
const file = card.dataset.file || '';
|
|
const cat = card.dataset.cat;
|
|
const okCat = cats.length===0 || cats.includes(cat);
|
|
const okText = !q || label.includes(q) || file.toLowerCase().includes(q) || (card.dataset.tags||'').toLowerCase().includes(q);
|
|
card.style.display = (okCat && okText) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
function globalAction(action){
|
|
if(action==='refresh-visible'){
|
|
document.querySelectorAll('.card').forEach(async card => {
|
|
if(card.offsetParent!==null){
|
|
const diag = card.querySelector('.diagram');
|
|
if(diag){ diag.dataset.rendered=''; await renderMermaidFile(diag.dataset.file, diag); diag.dataset.rendered='1'; applyFitZoomFor(card); }
|
|
}
|
|
});
|
|
}
|
|
if(action==='expand-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight=''); }
|
|
if(action==='collapse-all'){ document.querySelectorAll('.diagram-wrap').forEach(w => w.style.maxHeight='200px'); }
|
|
}
|
|
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
buildGrid();
|
|
document.querySelectorAll('input[name="cat-filter"]').forEach(i=> i.addEventListener('change', applyFilters));
|
|
document.getElementById('search').addEventListener('input', applyFilters);
|
|
document.getElementById('refreshVisible').addEventListener('click', () => globalAction('refresh-visible'));
|
|
document.getElementById('expandAll').addEventListener('click', () => globalAction('expand-all'));
|
|
document.getElementById('collapseAll').addEventListener('click', () => globalAction('collapse-all'));
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Fotbal Club — Unified Diagrams</h1>
|
|
<div class="filters">
|
|
<input id="search" type="search" placeholder="Search diagrams, files, tags…" />
|
|
<label class="chip"><input type="checkbox" name="cat-filter" value="System" /> System</label>
|
|
<label class="chip"><input type="checkbox" name="cat-filter" value="Backend" /> Backend</label>
|
|
<label class="chip"><input type="checkbox" name="cat-filter" value="Frontend" /> Frontend</label>
|
|
<label class="chip"><input type="checkbox" name="cat-filter" value="Admin" /> Admin</label>
|
|
<span class="sp"></span>
|
|
<button class="btn" id="refreshVisible">Refresh visible</button>
|
|
<button class="btn ghost" id="expandAll">Expand all</button>
|
|
<button class="btn ghost" id="collapseAll">Collapse all</button>
|
|
</div>
|
|
</header>
|
|
<main>
|
|
<div id="grid" class="grid"></div>
|
|
</main>
|
|
</body>
|
|
</html>
|