From 1957a7ced759db689c0f662aa69c31758ec9f1fd Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:00:35 +0200 Subject: [PATCH] Refactor ResultsArticle to handle optional num_files --- src/pages/results.tsx | 517 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 448 insertions(+), 69 deletions(-) diff --git a/src/pages/results.tsx b/src/pages/results.tsx index 3a57c48..4faec61 100644 --- a/src/pages/results.tsx +++ b/src/pages/results.tsx @@ -1,4 +1,4 @@ -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import db from "../db/db"; @@ -7,47 +7,79 @@ import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { DownloadIcon } from "../icons/download"; import { DeleteIcon } from "../icons/delete"; import { EyeIcon } from "../icons/eye"; +import { ShareIcon } from "../icons/share"; import { userService } from "./user"; +import { outputDir } from ".."; +import { sendFileToErugo } from "../helpers/erugo"; + +type JobsWithOptionalNumFiles = Jobs & { + num_files?: number; +}; function ResultsArticle({ job, files, outputPath, }: { - job: Jobs; + job: JobsWithOptionalNumFiles; files: Filename[]; outputPath: string; }) { + const maxFiles = + typeof job.num_files === "number" && Number.isFinite(job.num_files) + ? Number(job.num_files) + : Number(files.length); + + const doneFiles = Number( + files.filter((f) => String(f.status ?? "").toLowerCase() === "done").length, + ); + + const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true; + + const disabledLinkClass = "pointer-events-none opacity-50"; + return (

Results

+

Delete

+

Tar

-
+ + - - - + + + + {files.map((file) => ( - + + ))}
- Converted File Name - - Status - - Actions - Converted File NameStatusActions
{file.output_file_name} {file.status} + + +
+ + {/* Share Modal (hidden by default) */} +
); } +const shareJs = ` +(function () { + const WEBROOT = ${JSON.stringify(WEBROOT)}; + + function getEl(id) { return document.getElementById(id); } + + function getRefs() { + return { + modal: getEl("cxShareModal"), + closeBtn: getEl("cxShareClose"), + cancelBtn: getEl("cxShareCancel"), + submitBtn: getEl("cxShareSubmit"), + statusEl: getEl("cxShareStatus"), + emailEl: getEl("cxShareEmail"), + nameEl: getEl("cxShareName"), + descEl: getEl("cxShareDescription"), + linkBlock: getEl("cxShareLinkBlock"), + linkEl: getEl("cxShareLink"), + copyBtn: getEl("cxShareCopy"), + }; + } + + let currentJobId = null; + let currentFileName = null; + + function openModal(jobId, fileName) { + const r = getRefs(); + if (!r.modal || !r.emailEl || !r.nameEl || !r.descEl || !r.statusEl || !r.linkBlock || !r.linkEl) { + console.warn("[ConvertX] Share modal elements not found (DOM may be mid-refresh)."); + return; + } + + currentJobId = jobId; + currentFileName = fileName; + + r.nameEl.value = fileName || ""; + r.emailEl.value = ""; + r.descEl.value = ""; + + r.linkBlock.classList.add("hidden"); + r.linkEl.value = ""; + r.statusEl.textContent = ""; + + r.modal.classList.remove("hidden"); + r.modal.classList.add("flex"); + r.emailEl.focus(); + } + + function closeModal() { + const r = getRefs(); + if (!r.modal) return; + + r.modal.classList.add("hidden"); + r.modal.classList.remove("flex"); + currentJobId = null; + currentFileName = null; + } + + document.addEventListener("click", (e) => { + const t = e.target; + const btn = t && (t.closest ? t.closest('[data-share="true"]') : null); + if (!btn) return; + + e.preventDefault(); + e.stopPropagation(); + if (e.stopImmediatePropagation) e.stopImmediatePropagation(); + + const jobId = btn.getAttribute("data-job-id"); + const fileName = btn.getAttribute("data-file-name"); + openModal(jobId, fileName); + }, true); + + document.addEventListener("click", (e) => { + const id = e.target && e.target.id; + if (id === "cxShareClose" || id === "cxShareCancel") { + e.preventDefault(); + closeModal(); + } + if (id === "cxShareModal") { + closeModal(); + } + }, true); + + document.addEventListener("keydown", (e) => { + const r = getRefs(); + if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal(); + }); + + document.addEventListener("click", async (e) => { + const t = e.target; + if (!t || t.id !== "cxShareCopy") return; + e.preventDefault(); + + const r = getRefs(); + if (!r.linkEl || !r.statusEl) return; + + try { + await navigator.clipboard.writeText(r.linkEl.value || ""); + r.statusEl.textContent = "Copied."; + } catch (err) { + r.linkEl.focus(); + r.linkEl.select(); + r.statusEl.textContent = "Select + copy (Ctrl/Cmd+C)."; + } + }, true); + + document.addEventListener("click", async (e) => { + const t = e.target; + if (!t || t.id !== "cxShareSubmit") return; + e.preventDefault(); + + const r = getRefs(); + if (!r.submitBtn || !r.statusEl || !r.emailEl || !r.nameEl || !r.descEl || !r.linkBlock || !r.linkEl) return; + + if (!currentJobId || !currentFileName) return; + + r.submitBtn.disabled = true; + r.submitBtn.setAttribute("aria-busy", "true"); + r.statusEl.textContent = "Sending..."; + + try { + const email = (r.emailEl.value || "").trim(); + const shareName = (r.nameEl.value || "").trim(); + const description = (r.descEl.value || "").trim(); + + const payload = { + fileName: currentFileName, + ...(email ? { recipientEmail: email } : {}), + ...(shareName ? { shareName } : {}), + ...(description ? { description } : {}), + }; + + const res = await fetch(\`\${WEBROOT}/share-to-erugo/\${currentJobId}\`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const text = await res.text(); + let json; + try { json = JSON.parse(text); } catch (_) { json = { raw: text }; } + + if (!res.ok) { + console.error("[ConvertX] share-to-erugo error", res.status, json); + r.statusEl.textContent = "Failed. See logs."; + return; + } + + const url = + (json && typeof json === "object" && json.share_url) || + (json && typeof json === "object" && json.share_link) || + (json && typeof json === "object" && json.data && json.data.url) || + (json && typeof json === "object" && json.data && json.data.share && json.data.share.url) || + null; + + if (url && typeof url === "string") { + r.linkEl.value = url; + r.linkBlock.classList.remove("hidden"); + } + + r.statusEl.textContent = email + ? (url ? "Sent. Link also shown below." : "Sent. (No link returned.)") + : (url ? "Created. Copy the link below." : "Created, but no link returned."); + } catch (err) { + console.error(err); + r.statusEl.textContent = "Failed. See logs."; + } finally { + r.submitBtn.disabled = false; + r.submitBtn.removeAttribute("aria-busy"); + } + }, true); + + document.addEventListener("click", (e) => { + const t = e.target; + const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null); + if (!btn) return; + + e.preventDefault(); + try { + if (typeof window.downloadAll === "function") { + window.downloadAll(); + } else { + console.warn("[ConvertX] downloadAll() not found. Ensure results.js is loaded."); + } + } catch (err) { + console.error("[ConvertX] downloadAll() failed", err); + } + }, true); +})(); +`.trim(); + export const results = new Elysia() .use(userService) + .get( + "/results-share.js", + () => + new Response(shareJs, { + headers: { + "content-type": "text/javascript; charset=utf-8", + "cache-control": "no-store", + }, + }), + { auth: true }, + ) .get( "/results/:jobId", async ({ params, set, cookie: { job_id }, user }) => { - if (job_id?.value) { - // Clear the job_id cookie since we are viewing the results - job_id.remove(); - } + if (job_id?.value) job_id.remove(); const job = db .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .as(Jobs) - .get(user.id, params.jobId); + .get(user.id, params.jobId) as JobsWithOptionalNumFiles | null; if (!job) { set.status = 404; - return { - message: "Job not found.", - }; + return { message: "Job not found." }; } const outputPath = `${user.id}/${params.jobId}/`; @@ -160,16 +481,21 @@ export const results = new Elysia() return ( <> -
-
+
+ +
+ + {/* existing file */}