Files
aio_html_generator/static/js/app.js
Mateusz Gruszczyński b6f96085a1 commit
2026-02-26 14:13:04 +01:00

226 lines
7.3 KiB
JavaScript

document.addEventListener("DOMContentLoaded", () => {
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
const modeBtns = document.querySelectorAll(".mode-btn");
const progress = document.getElementById("progress");
const progressBar = progress.querySelector(".progress-bar");
const progressText = document.getElementById("progress-text");
const result = document.getElementById("result");
let currentMode = "basic";
modeBtns.forEach((btn) => {
btn.addEventListener("click", () => {
modeBtns.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
currentMode = btn.dataset.mode || "basic";
});
});
["dragenter", "dragover"].forEach((e) =>
dropZone.addEventListener(e, (ev) => {
ev.preventDefault();
dropZone.classList.add("dragover");
})
);
["dragleave", "drop"].forEach((e) =>
dropZone.addEventListener(e, (ev) => {
ev.preventDefault();
dropZone.classList.remove("dragover");
})
);
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
fileInput.files = e.dataTransfer.files;
processFiles();
});
fileInput.addEventListener("change", processFiles);
dropZone.querySelector("label")?.addEventListener("click", (e) => {
e.preventDefault();
fileInput.click();
});
function processFiles() {
const files = fileInput.files;
if (!files || !files.length) return;
const endpoint =
currentMode === "advanced" ? "/api/upload-advanced" : "/api/upload";
const formData = new FormData();
Array.from(files).forEach((file) => formData.append("files", file));
showProgress(currentMode.toUpperCase(), files.length);
fetch(endpoint, { method: "POST", body: formData })
.then((r) => r.json())
.then((data) => {
hideProgress();
if (data && data.success) showResult(data);
else showError(data?.error || "Unknown error");
})
.catch((e) => {
hideProgress();
showError(e?.message || "Request failed");
});
}
function showProgress(mode, count) {
progress.classList.remove("d-none");
progressText.classList.remove("d-none");
progressText.textContent = `${mode} - ${count} files`;
progressBar.style.width = "45%";
}
function hideProgress() {
progress.classList.add("d-none");
progressText.classList.add("d-none");
}
async function showResult(data) {
const sizeKB = ((data.size || 0) / 1024).toFixed(1);
const rawHtml =
typeof data.aio_html === "string" ? data.aio_html : String(data.aio_html ?? "");
const formattedHtml = await formatHtml(rawHtml);
result.innerHTML = `
<div class="alert alert-success">
<div class="d-flex align-items-start gap-3">
<i class="fa-solid fa-circle-check mt-1 flex-shrink-0"></i>
<div class="flex-grow-1">
<h5 class="mb-1">${escapeHtml(data.message ?? "Done")}</h5>
<div class="mb-2">
<small><code>${escapeHtml(data.original ?? "")}</code> → <strong>${sizeKB}KB</strong></small>
</div>
</div>
<div class="btn-group-vertical btn-group-sm">
<button class="btn btn-success btn-copy-sm" type="button">
<i class="fa-solid fa-copy me-1"></i>Copy
</button>
<a
href="data:text/html;charset=utf-8,${encodeURIComponent(rawHtml)}"
download="aio.html"
class="btn btn-outline-primary btn-sm"
>
<i class="fa-solid fa-download me-1"></i>Save
</a>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="card result-card">
<div class="card-header bg-light">
<h6 class="mb-0"><i class="fa-regular fa-eye me-2"></i>Preview</h6>
</div>
<iframe id="previewFrame" class="preview-iframe" style="height:70vh; min-height:520px;"></iframe>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card result-card">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fa-solid fa-code me-2"></i>Code</h6>
<button class="btn btn-sm btn-success btn-copy-all" type="button">
<i class="fa-solid fa-copy me-1"></i>Copy All
</button>
</div>
<pre class="code-preview mb-0" style="max-height:70vh; min-height:520px; overflow:auto;">
<code class="language-markup">${escapeHtml(formattedHtml)}</code></pre>
</div>
</div>
</div>
<div class="text-center mt-4">
<button class="btn btn-outline-secondary px-4" type="button" id="btn-new">
<i class="fa-solid fa-arrow-rotate-right me-2"></i>New
</button>
</div>
`;
result.classList.remove("d-none");
const frame = result.querySelector("#previewFrame");
if (frame) frame.srcdoc = rawHtml;
if (window.Prism) window.Prism.highlightAllUnder(result);
result.querySelectorAll(".btn-copy-sm, .btn-copy-all").forEach((btn) => {
const originalHtml = btn.innerHTML;
btn.addEventListener("click", () => {
navigator.clipboard
.writeText(rawHtml)
.then(() => {
btn.innerHTML = '<i class="fa-solid fa-check me-1"></i>Copied';
setTimeout(() => (btn.innerHTML = originalHtml), 1200);
})
.catch(() => fallbackCopy(rawHtml, btn, originalHtml));
});
});
result.querySelector("#btn-new")?.addEventListener("click", () => {
location.reload();
});
result.scrollIntoView({ behavior: "smooth" });
}
function fallbackCopy(text, btn, originalHtml) {
try {
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
btn.innerHTML = '<i class="fa-solid fa-check me-1"></i>Copied';
setTimeout(() => (btn.innerHTML = originalHtml), 1200);
} catch {
showError("Clipboard blocked by browser");
}
}
function showError(msg) {
result.innerHTML = `
<div class="alert alert-danger">
<i class="fa-solid fa-circle-xmark me-2"></i>${escapeHtml(msg)}
</div>
<div class="text-center">
<button class="btn btn-outline-primary" type="button" id="btn-try">Try Again</button>
</div>
`;
result.classList.remove("d-none");
result.querySelector("#btn-try")?.addEventListener("click", () => location.reload());
}
});
async function formatHtml(html) {
html = typeof html === "string" ? html : String(html ?? "");
try {
if (window.prettier && window.prettierPlugins?.html) {
return await window.prettier.format(html, {
parser: "html",
plugins: [window.prettierPlugins.html],
printWidth: 100,
tabWidth: 2,
useTabs: false,
});
}
} catch (e) {
console.error(e);
}
return html;
}
function escapeHtml(text) {
text = typeof text === "string" ? text : String(text ?? "");
const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
return text.replace(/[&<>"']/g, (m) => map[m]);
}