diff --git a/network-poc/frontend/src/pages/index.astro b/network-poc/frontend/src/pages/index.astro
index d08308e..e8bf42d 100644
--- a/network-poc/frontend/src/pages/index.astro
+++ b/network-poc/frontend/src/pages/index.astro
@@ -1341,6 +1341,7 @@ OUTPUT FORMAT:
`${esc(name||'Projekti')} (${entries.length})` +
`` +
`` +
+ `` +
`` +
`` +
`
${tabs}
${panels}`;
@@ -1360,6 +1361,84 @@ OUTPUT FORMAT:
const text = Object.entries(files).map(([n,c]) => '# --- ' + n + ' ---\n' + c).join('\n\n');
navigator.clipboard.writeText(text);
};
+ window.downloadProjectZip = function(id, name) {
+ const files = window._projectFiles[id];
+ if (!files) return;
+ const enc = new TextEncoder();
+ const entries = Object.entries(files);
+ const localParts = [];
+ const centralParts = [];
+ let offset = 0;
+
+ for (const [fname, content] of entries) {
+ const nameBytes = enc.encode(fname);
+ const dataBytes = enc.encode(content);
+ const crc = crc32(dataBytes);
+
+ // Local file header
+ const local = new Uint8Array(30 + nameBytes.length + dataBytes.length);
+ const lv = new DataView(local.buffer);
+ lv.setUint32(0, 0x04034b50, true); // signature
+ lv.setUint16(4, 20, true); // version needed
+ lv.setUint16(8, 8, true); // UTF-8 flag
+ lv.setUint32(14, crc, true); // CRC-32
+ lv.setUint32(18, dataBytes.length, true); // compressed size
+ lv.setUint32(22, dataBytes.length, true); // uncompressed size
+ lv.setUint16(26, nameBytes.length, true); // filename length
+ local.set(nameBytes, 30);
+ local.set(dataBytes, 30 + nameBytes.length);
+ localParts.push(local);
+
+ // Central directory entry
+ const central = new Uint8Array(46 + nameBytes.length);
+ const cv = new DataView(central.buffer);
+ cv.setUint32(0, 0x02014b50, true); // signature
+ cv.setUint16(4, 20, true); // version made by
+ cv.setUint16(6, 20, true); // version needed
+ cv.setUint16(8, 8, true); // UTF-8 flag
+ cv.setUint32(16, crc, true);
+ cv.setUint32(20, dataBytes.length, true);
+ cv.setUint32(24, dataBytes.length, true);
+ cv.setUint16(28, nameBytes.length, true);
+ cv.setUint32(42, offset, true); // local header offset
+ central.set(nameBytes, 46);
+ centralParts.push(central);
+
+ offset += local.length;
+ }
+
+ const centralSize = centralParts.reduce((s, c) => s + c.length, 0);
+ // End of central directory
+ const eocd = new Uint8Array(22);
+ const ev = new DataView(eocd.buffer);
+ ev.setUint32(0, 0x06054b50, true);
+ ev.setUint16(8, entries.length, true);
+ ev.setUint16(10, entries.length, true);
+ ev.setUint32(12, centralSize, true);
+ ev.setUint32(16, offset, true);
+
+ const blob = new Blob([...localParts, ...centralParts, eocd], { type: 'application/zip' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = (name || 'projekti').replace(/[^a-z0-9_-]/gi, '_') + '.zip';
+ a.click();
+ URL.revokeObjectURL(url);
+ };
+
+ // CRC-32 (ZIP-standardin mukainen)
+ const crc32Table = new Uint32Array(256);
+ for (let i = 0; i < 256; i++) {
+ let c = i;
+ for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
+ crc32Table[i] = c;
+ }
+ function crc32(data) {
+ let crc = 0xFFFFFFFF;
+ for (let i = 0; i < data.length; i++) crc = crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
+ return (crc ^ 0xFFFFFFFF) >>> 0;
+ }
+
window.switchProjTab = function(id,i) {
document.querySelectorAll(`.project-tab[data-card="${id}"]`).forEach((t,j) => t.classList.toggle('active', j===i));
document.querySelectorAll(`.proj-panel[data-card="${id}"]`).forEach((p,j) => p.style.display = j===i ? '' : 'none');