249 lines
12 KiB
HTML
249 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Web Application Architecture</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js" integrity="sha384-ZZ1pncU3bQe8y31yfZdMFdSpttDoPmOZg2wguVK9almUodir1PghgT0eY7Mrty8H" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js" integrity="sha384-en/ztfPSRkGfME4KIm05joYXynqzUgbsG5nMrj/xEFAHXkeZfO3yMK8QQ+mP7p1/" crossorigin="anonymous"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
background: #020617;
|
|
min-height: 100vh;
|
|
padding: 2rem;
|
|
color: white;
|
|
}
|
|
.container { max-width: 1200px; margin: 0 auto; }
|
|
.header { margin-bottom: 2rem; }
|
|
.header-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
|
|
.pulse-dot {
|
|
width: 12px; height: 12px; background: #22d3ee;
|
|
border-radius: 50%; animation: pulse 2s infinite;
|
|
}
|
|
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.025em; }
|
|
.subtitle { color: #94a3b8; font-size: 0.875rem; margin-left: 1.75rem; }
|
|
.diagram-container {
|
|
background: rgba(15, 23, 42, 0.5);
|
|
border-radius: 1rem; border: 1px solid #1e293b;
|
|
padding: 1.5rem; overflow-x: auto;
|
|
}
|
|
svg { width: 100%; min-width: 800px; display: block; }
|
|
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-top: 2rem; }
|
|
.card { background: rgba(15, 23, 42, 0.5); border-radius: 0.75rem; border: 1px solid #1e293b; padding: 1.25rem; }
|
|
.card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
.card-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
.card-dot.cyan { background: #22d3ee; }
|
|
.card-dot.emerald { background: #34d399; }
|
|
.card-dot.violet { background: #a78bfa; }
|
|
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
|
.card ul { list-style: none; color: #94a3b8; font-size: 0.75rem; }
|
|
.card li { margin-bottom: 0.375rem; }
|
|
.footer { text-align: center; margin-top: 1.5rem; color: #475569; font-size: 0.75rem; }
|
|
.toolbar { display: flex; gap: 0.5rem; margin-left: auto; flex-shrink: 0; align-items: center; }
|
|
.toolbar-toggle {
|
|
background: transparent; border: none; color: #475569; cursor: pointer;
|
|
font-size: 1.25rem; line-height: 1; padding: 0.25rem 0.5rem;
|
|
border-radius: 0.375rem; transition: color 0.2s, background 0.2s;
|
|
}
|
|
.toolbar-toggle:hover { color: #94a3b8; background: rgba(30, 41, 59, 0.5); }
|
|
.toolbar-actions { display: none; gap: 0.5rem; }
|
|
.toolbar.expanded .toolbar-actions { display: flex; }
|
|
.toolbar-actions button {
|
|
background: rgba(30, 41, 59, 0.8); border: 1px solid #334155; color: #94a3b8;
|
|
padding: 0.375rem 0.75rem; border-radius: 0.375rem; font-family: inherit;
|
|
font-size: 0.75rem; cursor: pointer; transition: all 0.2s; white-space: nowrap;
|
|
}
|
|
.toolbar-actions button:hover { background: rgba(51, 65, 85, 0.8); color: white; border-color: #475569; }
|
|
@media print {
|
|
body { background: #020617; padding: 1rem; }
|
|
.toolbar { display: none !important; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container" id="report-container">
|
|
<div class="header">
|
|
<div class="header-row">
|
|
<div class="pulse-dot"></div>
|
|
<h1>Web Application Architecture</h1>
|
|
<div class="toolbar">
|
|
<div class="toolbar-actions">
|
|
<button onclick="copyAsImage(this)">📋 Copy</button>
|
|
<button onclick="downloadPNG(this)">🖼️ PNG</button>
|
|
<button onclick="downloadPDF(this)">📄 PDF</button>
|
|
</div>
|
|
<button class="toolbar-toggle" onclick="this.parentElement.classList.toggle('expanded')" title="Export options" aria-label="Export options">⋯</button>
|
|
</div>
|
|
</div>
|
|
<p class="subtitle">React + Node.js + PostgreSQL stack</p>
|
|
</div>
|
|
|
|
<div class="diagram-container">
|
|
<svg viewBox="0 0 900 400">
|
|
<defs>
|
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|
<polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
|
|
</marker>
|
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
|
|
</pattern>
|
|
</defs>
|
|
|
|
<rect width="100%" height="100%" fill="url(#grid)" />
|
|
|
|
<!-- Arrows (drawn first for z-order) -->
|
|
<line x1="130" y1="200" x2="248" y2="200" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
|
<text x="189" y="190" fill="#94a3b8" font-size="8" text-anchor="middle">HTTPS</text>
|
|
|
|
<line x1="400" y1="200" x2="498" y2="200" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
|
<text x="449" y="190" fill="#94a3b8" font-size="8" text-anchor="middle">REST API</text>
|
|
|
|
<line x1="650" y1="200" x2="748" y2="200" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrowhead)"/>
|
|
<text x="699" y="190" fill="#94a3b8" font-size="8" text-anchor="middle">SQL</text>
|
|
|
|
<!-- Users -->
|
|
<rect x="30" y="160" width="100" height="80" rx="6" fill="#0f172a"/>
|
|
<rect x="30" y="160" width="100" height="80" rx="6" fill="rgba(30, 41, 59, 0.5)" stroke="#94a3b8" stroke-width="1.5"/>
|
|
<text x="80" y="195" fill="white" font-size="12" font-weight="600" text-anchor="middle">Users</text>
|
|
<text x="80" y="215" fill="#94a3b8" font-size="9" text-anchor="middle">Browser</text>
|
|
|
|
<!-- Frontend -->
|
|
<rect x="250" y="140" width="150" height="120" rx="6" fill="#0f172a"/>
|
|
<rect x="250" y="140" width="150" height="120" rx="6" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
|
<text x="325" y="175" fill="white" font-size="12" font-weight="600" text-anchor="middle">Frontend</text>
|
|
<text x="325" y="195" fill="#94a3b8" font-size="9" text-anchor="middle">React</text>
|
|
<text x="325" y="210" fill="#94a3b8" font-size="9" text-anchor="middle">TypeScript</text>
|
|
<text x="325" y="225" fill="#94a3b8" font-size="9" text-anchor="middle">Tailwind CSS</text>
|
|
<text x="325" y="248" fill="#22d3ee" font-size="8" text-anchor="middle">:3000</text>
|
|
|
|
<!-- Backend -->
|
|
<rect x="500" y="140" width="150" height="120" rx="6" fill="#0f172a"/>
|
|
<rect x="500" y="140" width="150" height="120" rx="6" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1.5"/>
|
|
<text x="575" y="175" fill="white" font-size="12" font-weight="600" text-anchor="middle">Backend</text>
|
|
<text x="575" y="195" fill="#94a3b8" font-size="9" text-anchor="middle">Node.js</text>
|
|
<text x="575" y="210" fill="#94a3b8" font-size="9" text-anchor="middle">Express</text>
|
|
<text x="575" y="225" fill="#94a3b8" font-size="9" text-anchor="middle">REST API</text>
|
|
<text x="575" y="248" fill="#34d399" font-size="8" text-anchor="middle">:8080</text>
|
|
|
|
<!-- Database -->
|
|
<rect x="750" y="150" width="120" height="100" rx="6" fill="#0f172a"/>
|
|
<rect x="750" y="150" width="120" height="100" rx="6" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
|
<text x="810" y="190" fill="white" font-size="12" font-weight="600" text-anchor="middle">PostgreSQL</text>
|
|
<text x="810" y="210" fill="#94a3b8" font-size="9" text-anchor="middle">Database</text>
|
|
<text x="810" y="238" fill="#a78bfa" font-size="8" text-anchor="middle">:5432</text>
|
|
|
|
<!-- Legend -->
|
|
<text x="30" y="350" fill="white" font-size="10" font-weight="600">Legend</text>
|
|
<rect x="30" y="362" width="16" height="10" rx="2" fill="rgba(8, 51, 68, 0.4)" stroke="#22d3ee" stroke-width="1"/>
|
|
<text x="52" y="370" fill="#94a3b8" font-size="8">Frontend</text>
|
|
<rect x="110" y="362" width="16" height="10" rx="2" fill="rgba(6, 78, 59, 0.4)" stroke="#34d399" stroke-width="1"/>
|
|
<text x="132" y="370" fill="#94a3b8" font-size="8">Backend</text>
|
|
<rect x="190" y="362" width="16" height="10" rx="2" fill="rgba(76, 29, 149, 0.4)" stroke="#a78bfa" stroke-width="1"/>
|
|
<text x="212" y="370" fill="#94a3b8" font-size="8">Database</text>
|
|
</svg>
|
|
</div>
|
|
|
|
<div class="cards">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-dot cyan"></div>
|
|
<h3>Frontend Stack</h3>
|
|
</div>
|
|
<ul>
|
|
<li>• React 18 with hooks</li>
|
|
<li>• TypeScript for type safety</li>
|
|
<li>• Tailwind CSS styling</li>
|
|
<li>• Vite build tool</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-dot emerald"></div>
|
|
<h3>Backend Stack</h3>
|
|
</div>
|
|
<ul>
|
|
<li>• Node.js runtime</li>
|
|
<li>• Express framework</li>
|
|
<li>• JWT authentication</li>
|
|
<li>• REST API design</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-dot violet"></div>
|
|
<h3>Data Layer</h3>
|
|
</div>
|
|
<ul>
|
|
<li>• PostgreSQL database</li>
|
|
<li>• Prisma ORM</li>
|
|
<li>• Redis caching</li>
|
|
<li>• S3 file storage</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="footer">Web Application Architecture • Three-tier design</p>
|
|
</div>
|
|
|
|
<script>
|
|
async function copyAsImage(btn) {
|
|
const orig = btn.textContent;
|
|
try {
|
|
const el = document.getElementById('report-container');
|
|
const r = el.getBoundingClientRect();
|
|
const pad = 32;
|
|
const canvas = await html2canvas(document.body, { backgroundColor: '#020617', scale: 2, useCORS: true, ignoreElements: (e) => e.classList && e.classList.contains('toolbar'), x: r.left + window.scrollX - pad, y: r.top + window.scrollY - pad, width: r.width + pad * 2, height: r.height + pad * 2 });
|
|
const blob = await new Promise(r => canvas.toBlob(r, 'image/png'));
|
|
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|
btn.textContent = '✓ Copied!';
|
|
} catch (e) {
|
|
btn.textContent = '✗ Failed';
|
|
}
|
|
setTimeout(() => btn.textContent = orig, 2000);
|
|
}
|
|
async function downloadPNG(btn) {
|
|
const orig = btn.textContent;
|
|
btn.textContent = '⏳ ...';
|
|
try {
|
|
const el = document.getElementById('report-container');
|
|
const r = el.getBoundingClientRect();
|
|
const pad = 32;
|
|
const canvas = await html2canvas(document.body, { backgroundColor: '#020617', scale: 2, useCORS: true, ignoreElements: (e) => e.classList && e.classList.contains('toolbar'), x: r.left + window.scrollX - pad, y: r.top + window.scrollY - pad, width: r.width + pad * 2, height: r.height + pad * 2 });
|
|
const link = document.createElement('a');
|
|
link.download = 'web-app.png';
|
|
link.href = canvas.toDataURL('image/png');
|
|
link.click();
|
|
btn.textContent = '✓ Done!';
|
|
} catch (e) {
|
|
btn.textContent = '✗ Failed';
|
|
}
|
|
setTimeout(() => btn.textContent = orig, 2000);
|
|
}
|
|
async function downloadPDF(btn) {
|
|
const orig = btn.textContent;
|
|
btn.textContent = '⏳ ...';
|
|
try {
|
|
const el = document.getElementById('report-container');
|
|
const r = el.getBoundingClientRect();
|
|
const pad = 32;
|
|
const canvas = await html2canvas(document.body, { backgroundColor: '#020617', scale: 2, useCORS: true, ignoreElements: (e) => e.classList && e.classList.contains('toolbar'), x: r.left + window.scrollX - pad, y: r.top + window.scrollY - pad, width: r.width + pad * 2, height: r.height + pad * 2 });
|
|
const imgData = canvas.toDataURL('image/png');
|
|
const { jsPDF } = window.jspdf;
|
|
const orientation = canvas.width > canvas.height ? 'landscape' : 'portrait';
|
|
const pdf = new jsPDF({ orientation, unit: 'px', format: [canvas.width, canvas.height], hotfixes: ['px_scaling'] });
|
|
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
|
|
pdf.save('web-app.pdf');
|
|
btn.textContent = '✓ Done!';
|
|
} catch (e) {
|
|
btn.textContent = '✗ Failed';
|
|
}
|
|
setTimeout(() => btn.textContent = orig, 2000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|