secubox-openwrt/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/upload.js
CyberMind-FR 2f7d57dced feat(hexojs): Add content upload wizard and moderation system
- Add upload.js wizard with multi-target publishing (HexoJS, Gitea, Streamlit, MetaBlogizer)
- Add submit.js for user content submission with moderation workflow
- Add moderation RPCD methods: submit_for_review, list_pending, approve_submission, reject_submission
- Update ACL with new moderation permissions
- Add menu entries for Upload and Submit & Moderate views

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 14:11:20 +01:00

428 lines
20 KiB
JavaScript

'use strict';
'require view';
'require ui';
'require rpc';
'require secubox/kiss-theme';
var callUploadHTML = rpc.declare({
object: 'luci.hexojs',
method: 'upload_html',
params: ['base64_data', 'title', 'visibility', 'category', 'tags'],
expect: { '': {} }
});
var callUploadPDF = rpc.declare({
object: 'luci.hexojs',
method: 'upload_pdf',
params: ['base64_data', 'title', 'visibility', 'category'],
expect: { '': {} }
});
var callUploadMD = rpc.declare({
object: 'luci.hexojs',
method: 'upload_article',
params: ['filename', 'content', 'title', 'visibility', 'category', 'tags'],
expect: { '': {} }
});
var callWizardUpload = rpc.declare({
object: 'luci.hexojs',
method: 'wizard_upload',
params: ['base64_data', 'filename', 'title', 'visibility', 'category', 'tags', 'target', 'options'],
expect: { '': {} }
});
var callGiteaUpload = rpc.declare({
object: 'luci.gitea',
method: 'upload_file',
params: ['repo', 'path', 'content', 'message'],
expect: { '': {} }
});
var callStreamlitCreate = rpc.declare({
object: 'luci.streamlit',
method: 'create_app',
params: ['name', 'source_file'],
expect: { '': {} }
});
var callMetablogizerCreate = rpc.declare({
object: 'luci.metablogizer',
method: 'create_entry',
params: ['name', 'source_file', 'category'],
expect: { '': {} }
});
function fileToBase64(file) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() {
var base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
return view.extend({
render: function() {
var self = this;
var content = E('div', { style: 'padding:20px;max-width:800px;margin:0 auto;' }, [
E('h2', { style: 'text-align:center;margin-bottom:30px;' }, '📤 Content Upload Wizard'),
E('div', { class: 'kiss-card', style: 'padding:30px;' }, [
// Step 1: File Selection
E('div', { class: 'wizard-step', id: 'step-file' }, [
E('h3', {}, '1. Select File'),
E('p', { style: 'color:#888;' }, 'Supported: HTML, PDF, Markdown (.md)'),
E('div', { style: 'border:2px dashed #3498db;border-radius:12px;padding:40px;text-align:center;margin:20px 0;' }, [
E('input', {
type: 'file',
id: 'file-input',
accept: '.html,.htm,.pdf,.md,.markdown',
style: 'display:none;'
}),
E('label', { for: 'file-input', style: 'cursor:pointer;display:block;' }, [
E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '📄'),
E('div', { style: 'color:#3498db;font-size:1.2em;' }, 'Click to select file'),
E('div', { id: 'file-name', style: 'margin-top:10px;color:#27ae60;font-weight:bold;' }, '')
])
])
]),
// Step 2: Metadata
E('div', { class: 'wizard-step', id: 'step-meta', style: 'display:none;' }, [
E('h3', {}, '2. Article Details'),
E('div', { style: 'margin:15px 0;' }, [
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Title'),
E('input', {
type: 'text',
id: 'input-title',
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
placeholder: 'Article title...'
})
]),
E('div', { style: 'margin:15px 0;' }, [
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Category'),
E('input', {
type: 'text',
id: 'input-category',
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
placeholder: 'e.g., Blog, Tutorial, News',
value: 'Uploads'
})
]),
E('div', { style: 'margin:15px 0;' }, [
E('label', { style: 'display:block;margin-bottom:5px;' }, 'Tags (comma-separated)'),
E('input', {
type: 'text',
id: 'input-tags',
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;',
placeholder: 'tag1, tag2, tag3'
})
]),
E('div', { style: 'margin:20px 0;' }, [
E('label', { style: 'display:block;margin-bottom:10px;' }, 'Visibility'),
E('div', { style: 'display:flex;gap:20px;' }, [
E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [
E('input', { type: 'radio', name: 'visibility', value: 'public', checked: true }),
E('span', {}, '🌍 Public')
]),
E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [
E('input', { type: 'radio', name: 'visibility', value: 'private' }),
E('span', {}, '🔒 Private (Draft)')
])
])
])
]),
// Step 3: Target Selection
E('div', { class: 'wizard-step', id: 'step-target', style: 'display:none;' }, [
E('h3', {}, '3. Publish To'),
E('div', { style: 'display:grid;grid-template-columns:repeat(2,1fr);gap:15px;margin:20px 0;' }, [
E('div', {
class: 'target-option selected',
'data-target': 'hexojs',
style: 'padding:20px;border:2px solid #3498db;border-radius:12px;cursor:pointer;text-align:center;'
}, [
E('div', { style: 'font-size:2em;' }, '📝'),
E('div', { style: 'font-weight:bold;' }, 'HexoJS Blog'),
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Static blog post')
]),
E('div', {
class: 'target-option',
'data-target': 'gitea',
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
}, [
E('div', { style: 'font-size:2em;' }, '🐙'),
E('div', { style: 'font-weight:bold;' }, 'Gitea'),
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Version control')
]),
E('div', {
class: 'target-option',
'data-target': 'streamlit',
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
}, [
E('div', { style: 'font-size:2em;' }, '📊'),
E('div', { style: 'font-weight:bold;' }, 'Streamlit'),
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Interactive app')
]),
E('div', {
class: 'target-option',
'data-target': 'metablogizer',
style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;'
}, [
E('div', { style: 'font-size:2em;' }, '🔗'),
E('div', { style: 'font-weight:bold;' }, 'MetaBlogizer'),
E('div', { style: 'color:#888;font-size:0.9em;' }, 'Multi-platform')
])
]),
// Gitea options (shown when gitea selected)
E('div', { id: 'gitea-options', style: 'display:none;margin-top:20px;padding:15px;background:#1a1a2e;border-radius:8px;' }, [
E('h4', { style: 'margin-bottom:10px;' }, 'Gitea Repository'),
E('input', {
type: 'text',
id: 'gitea-repo',
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;',
placeholder: 'owner/repo (e.g., admin/my-docs)'
}),
E('input', {
type: 'text',
id: 'gitea-path',
style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;margin-top:10px;',
placeholder: 'path/in/repo (e.g., docs/articles/)'
})
])
]),
// Navigation Buttons
E('div', { style: 'display:flex;justify-content:space-between;margin-top:30px;' }, [
E('button', {
id: 'btn-prev',
class: 'cbi-button',
style: 'display:none;',
click: ui.createHandlerFn(this, 'prevStep')
}, '← Back'),
E('button', {
id: 'btn-next',
class: 'cbi-button cbi-button-action',
style: 'margin-left:auto;',
click: ui.createHandlerFn(this, 'nextStep')
}, 'Next →')
]),
// Progress dots
E('div', { style: 'display:flex;justify-content:center;gap:10px;margin-top:20px;' }, [
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#3498db;' }),
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' }),
E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' })
])
]),
// Result Card
E('div', { id: 'result-card', class: 'kiss-card', style: 'display:none;padding:30px;text-align:center;' }, [
E('div', { style: 'font-size:4em;margin-bottom:20px;' }, '✅'),
E('h3', {}, 'Upload Successful!'),
E('p', { id: 'result-message', style: 'color:#888;' }, ''),
E('div', { style: 'margin-top:20px;' }, [
E('a', {
id: 'result-link',
href: '#',
class: 'cbi-button cbi-button-action',
target: '_blank',
style: 'text-decoration:none;'
}, 'View Article'),
E('button', {
class: 'cbi-button',
style: 'margin-left:10px;',
click: function() { window.location.reload(); }
}, 'Upload Another')
])
])
]);
// Initialize
setTimeout(function() {
self.currentStep = 1;
self.selectedFile = null;
self.selectedTarget = 'hexojs';
var fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
self.selectedFile = e.target.files[0];
document.getElementById('file-name').textContent = self.selectedFile.name;
var title = self.selectedFile.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
document.getElementById('input-title').value = title;
}
});
document.querySelectorAll('.target-option').forEach(function(opt) {
opt.addEventListener('click', function() {
document.querySelectorAll('.target-option').forEach(function(o) {
o.style.borderColor = '#444';
});
this.style.borderColor = '#3498db';
self.selectedTarget = this.dataset.target;
// Show/hide gitea options
var giteaOpts = document.getElementById('gitea-options');
if (giteaOpts) {
giteaOpts.style.display = self.selectedTarget === 'gitea' ? 'block' : 'none';
}
});
});
}, 100);
return KissTheme.wrap([content], 'admin/services/hexojs/upload');
},
nextStep: function() {
var self = this;
if (this.currentStep === 1) {
if (!this.selectedFile) {
ui.addNotification(null, E('p', 'Please select a file'), 'warning');
return;
}
document.getElementById('step-file').style.display = 'none';
document.getElementById('step-meta').style.display = 'block';
document.getElementById('btn-prev').style.display = 'block';
this.currentStep = 2;
this.updateDots();
} else if (this.currentStep === 2) {
var title = document.getElementById('input-title').value;
if (!title) {
ui.addNotification(null, E('p', 'Please enter a title'), 'warning');
return;
}
document.getElementById('step-meta').style.display = 'none';
document.getElementById('step-target').style.display = 'block';
document.getElementById('btn-next').textContent = '📤 Publish';
this.currentStep = 3;
this.updateDots();
} else if (this.currentStep === 3) {
this.doUpload();
}
},
prevStep: function() {
if (this.currentStep === 2) {
document.getElementById('step-meta').style.display = 'none';
document.getElementById('step-file').style.display = 'block';
document.getElementById('btn-prev').style.display = 'none';
this.currentStep = 1;
} else if (this.currentStep === 3) {
document.getElementById('step-target').style.display = 'none';
document.getElementById('step-meta').style.display = 'block';
document.getElementById('btn-next').textContent = 'Next →';
this.currentStep = 2;
}
this.updateDots();
},
updateDots: function() {
var dots = document.querySelectorAll('.step-dot');
var step = this.currentStep;
dots.forEach(function(dot, i) {
dot.style.background = i < step ? '#3498db' : '#444';
});
},
doUpload: function() {
var self = this;
var title = document.getElementById('input-title').value;
var category = document.getElementById('input-category').value || 'Uploads';
var tags = document.getElementById('input-tags').value || '';
var visibility = document.querySelector('input[name="visibility"]:checked').value;
var target = this.selectedTarget;
var ext = this.selectedFile.name.split('.').pop().toLowerCase();
ui.showModal('Uploading...', [
E('p', { class: 'spinning' }, 'Processing ' + this.selectedFile.name + '...')
]);
fileToBase64(this.selectedFile).then(function(base64) {
// Route based on target
if (target === 'hexojs') {
// Default HexoJS upload
if (ext === 'pdf') {
return callUploadPDF(base64, title, visibility, category);
} else if (ext === 'html' || ext === 'htm') {
return callUploadHTML(base64, title, visibility, category, tags);
} else {
return callUploadMD(self.selectedFile.name, atob(base64), title, visibility, category, tags);
}
} else if (target === 'gitea') {
// Gitea upload
var repo = document.getElementById('gitea-repo').value || 'admin/uploads';
var path = document.getElementById('gitea-path').value || 'uploads/';
var fullPath = path.replace(/\/$/, '') + '/' + self.selectedFile.name;
return callGiteaUpload(repo, fullPath, base64, 'Upload: ' + title);
} else if (target === 'streamlit') {
// Create Streamlit app from markdown/python
var appName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
return callStreamlitCreate(appName, base64).then(function(result) {
result.target = 'streamlit';
result.appUrl = '/admin/services/streamlit/apps/' + appName;
return result;
});
} else if (target === 'metablogizer') {
// Create MetaBlogizer entry
var entryName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
return callMetablogizerCreate(entryName, base64, category).then(function(result) {
result.target = 'metablogizer';
return result;
});
} else {
// Fallback: use wizard_upload for unified handling
var options = {};
if (target === 'gitea') {
options.repo = document.getElementById('gitea-repo').value;
options.path = document.getElementById('gitea-path').value;
}
return callWizardUpload(base64, self.selectedFile.name, title, visibility, category, tags, target, JSON.stringify(options));
}
}).then(function(result) {
ui.hideModal();
if (result.success) {
document.querySelector('.kiss-card').style.display = 'none';
document.getElementById('result-card').style.display = 'block';
var message = 'Your content has been uploaded.';
var linkText = 'View';
var linkHref = '#';
if (target === 'hexojs') {
message = 'Your ' + (visibility === 'public' ? 'article' : 'draft') + ' has been created.';
linkText = 'View Article';
if (result.slug) linkHref = '/' + result.slug + '/';
} else if (target === 'gitea') {
message = 'File uploaded to Gitea repository.';
linkText = 'Open Gitea';
linkHref = result.url || '/admin/services/gitea/overview';
} else if (target === 'streamlit') {
message = 'Streamlit app created successfully.';
linkText = 'Open App';
linkHref = result.appUrl || '/admin/services/streamlit/overview';
} else if (target === 'metablogizer') {
message = 'MetaBlogizer entry created.';
linkText = 'View Entry';
linkHref = result.url || '/admin/services/metablogizer/overview';
}
document.getElementById('result-message').textContent = message;
document.getElementById('result-link').textContent = linkText;
document.getElementById('result-link').href = linkHref;
} else {
ui.addNotification(null, E('p', result.error || 'Upload failed'), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', 'Error: ' + err.message), 'error');
});
}
});