Toolvana: production-ready media tools platform (CodeIgniter 4)
- 134-tool registry with programmatic SEO (unique titles/H1/descriptions, JSON-LD graphs, sitemap index, canonical 301 enforcement via required filter) - DB-backed job queue (SKIP LOCKED) with drivers: Ffmpeg, Images (GD), Pdf (qpdf/gs/poppler), Youtube (thumbnails), Qr (server-side PNG) - Security: SSRF guard, MIME validation, rate limits, API-key auth, bcrypt admin login, security headers - Admin panel: dashboard, tools/categories/guides CRUD, SEO audit, analytics, job inspector with retry, system health, feature flags - Docker deployment (nginx + web/api FPM pools + scalable workers), PHPUnit suite (19 tests / 1139 assertions), PWA manifest + service worker
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/* URL-tool flow: paste link → instant info → job → poll → download. */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var panel = document.getElementById('tool-panel');
|
||||
if (!panel) return;
|
||||
var slug = panel.dataset.tool;
|
||||
|
||||
var form = document.getElementById('tool-form');
|
||||
var urlInput = document.getElementById('url-input');
|
||||
var infoPanel = document.getElementById('info-panel');
|
||||
var statusFlow = document.getElementById('status-flow');
|
||||
var progressFill = document.getElementById('progress-fill');
|
||||
var resultPanel = document.getElementById('result-panel');
|
||||
var errorBox = document.getElementById('error-box');
|
||||
|
||||
var pollTimer = null, currentJobId = null;
|
||||
|
||||
function track(name) {
|
||||
try {
|
||||
navigator.sendBeacon &&
|
||||
navigator.sendBeacon('/api/events', new Blob([JSON.stringify({ name: name, tool: slug })], { type: 'application/json' }));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function showError(msg) { errorBox.textContent = msg; errorBox.hidden = false; track('tool_failure'); }
|
||||
|
||||
function setStage(stage, progress) {
|
||||
statusFlow.hidden = false;
|
||||
document.querySelectorAll('.stages li').forEach(function (li) {
|
||||
li.classList.toggle('done', li.dataset.stage !== stage);
|
||||
li.classList.toggle('active', li.dataset.stage === stage);
|
||||
});
|
||||
if (typeof progress === 'number') progressFill.style.width = Math.max(6, progress) + '%';
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
hideError();
|
||||
resultPanel.classList.remove('show');
|
||||
|
||||
fetch('/api/jobs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: slug, url: urlInput.value })
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error(res.body.message || res.body.error || 'Request failed.');
|
||||
currentJobId = res.body.job_id;
|
||||
setStage('preparing', 8);
|
||||
poll();
|
||||
})
|
||||
.catch(function (err) { showError(err.message || 'Something went wrong. Please try again.'); });
|
||||
});
|
||||
|
||||
function hideError() { errorBox.hidden = true; }
|
||||
|
||||
function poll() {
|
||||
if (!currentJobId) return;
|
||||
fetch('/api/jobs/' + currentJobId)
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.ok) throw new Error('lost');
|
||||
if (data.status === 'completed') { finish(data); return; }
|
||||
if (data.status === 'failed') { showError(data.message || 'This video could not be processed.'); return; }
|
||||
var stageMap = { preparing: 'preparing', fetching: 'fetching', processing: 'processing', converting: 'converting' };
|
||||
setStage(stageMap[data.stage] || 'processing', data.progress);
|
||||
pollTimer = setTimeout(poll, 1600);
|
||||
})
|
||||
.catch(function () { pollTimer = setTimeout(poll, 3200); });
|
||||
}
|
||||
|
||||
function finish(data) {
|
||||
clearTimeout(pollTimer);
|
||||
setStage('complete', 100);
|
||||
resultPanel.innerHTML =
|
||||
'<div class="result-meta">' +
|
||||
'<span class="badge">Done</span>' +
|
||||
'<strong>' + escapeHtml(data.output_name || 'download') + '</strong>' +
|
||||
'<span>' + humanSize(data.output_size || 0) + '</span>' +
|
||||
'<a class="btn-primary" style="text-decoration:none;padding:.55rem 1.3rem;font-size:.95rem" href="' + data.download_url + '" download>Download</a>' +
|
||||
'</div>';
|
||||
resultPanel.classList.add('show');
|
||||
}
|
||||
|
||||
function humanSize(bytes) {
|
||||
var u = ['B', 'KB', 'MB', 'GB'], i = 0;
|
||||
while (bytes >= 1024 && i < u.length - 1) { bytes /= 1024; i++; }
|
||||
return (Math.round(bytes * 10) / 10) + ' ' + u[i];
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user