document.addEventListener('DOMContentLoaded', () => {
// Tabs
const tabUpload = document.getElementById('tab-upload');
const tabUrl = document.getElementById('tab-url');
const tabZip = document.getElementById('tab-zip');
const panelUpload = document.getElementById('panel-upload');
const panelUrl = document.getElementById('panel-url');
const panelZip = document.getElementById('panel-zip');
function showPanel(name) {
[panelUpload, panelUrl, panelZip].forEach(p => p && p.classList.add('hidden'));
[tabUpload, tabUrl, tabZip].forEach(t => {
if (!t) return;
t.classList.remove('bg-cyan-500/20', 'text-cyan-400', 'border-cyan-500/30');
t.classList.add('bg-slate-800', 'border-white/10', 'text-slate-400');
});
const activeTab = name === 'upload' ? tabUpload : name === 'url' ? tabUrl : tabZip;
const activePanel = name === 'upload' ? panelUpload : name === 'url' ? panelUrl : panelZip;
if (activeTab) {
activeTab.classList.remove('bg-slate-800', 'border-white/10', 'text-slate-400');
activeTab.classList.add('bg-cyan-500/20', 'text-cyan-400', 'border-cyan-500/30');
}
if (activePanel) activePanel.classList.remove('hidden');
}
if (tabUpload) tabUpload.addEventListener('click', () => showPanel('upload'));
if (tabUrl) tabUrl.addEventListener('click', () => showPanel('url'));
if (tabZip) tabZip.addEventListener('click', () => showPanel('zip'));
// License template selector
const licenseSelect = document.getElementById('license-select');
const licenseInput = document.getElementById('license');
if (licenseSelect && licenseInput) {
licenseSelect.addEventListener('change', () => {
if (licenseSelect.value === 'custom') {
licenseInput.classList.remove('hidden');
licenseInput.focus();
} else {
licenseInput.classList.add('hidden');
licenseInput.value = licenseSelect.value;
}
});
}
// ====== NORMAL UPLOAD ======
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const fileNameDisplay = document.getElementById('file-name');
const uploadForm = document.getElementById('upload-form');
const submitBtn = document.getElementById('submit-btn');
const btnText = document.getElementById('btn-text');
const btnIcon = document.getElementById('btn-icon');
const tagsInput = document.getElementById('tags');
const tagsSuggestions = document.getElementById('tags-suggestions');
const partsList = document.getElementById('parts-list');
const imagesDropZone = document.getElementById('images-drop-zone');
const imagesInput = document.getElementById('images-input');
const imagesNameDisplay = document.getElementById('images-name');
let selectedFiles = [];
let selectedImages = [];
let allTags = [];
loadExistingTags();
async function loadExistingTags() {
try {
allTags = await apiGet('/models/tags');
} catch (e) {
console.error('Could not load tags', e);
}
}
// Tag autocomplete
if (tagsInput && tagsSuggestions) {
tagsInput.addEventListener('input', () => {
const val = tagsInput.value.toLowerCase();
const lastTag = val.split(',').pop().trim();
if (!lastTag || lastTag.length < 1) {
tagsSuggestions.style.display = 'none';
return;
}
const matches = allTags.filter(t => t.name.includes(lastTag) && !val.includes(t.name)).slice(0, 5);
if (matches.length === 0) {
tagsSuggestions.style.display = 'none';
return;
}
tagsSuggestions.innerHTML = matches.map(t =>
`
${t.name}
`
).join('');
tagsSuggestions.style.display = 'block';
});
document.addEventListener('click', (e) => {
if (!tagsInput.contains(e.target) && !tagsSuggestions.contains(e.target)) {
tagsSuggestions.style.display = 'none';
}
});
}
window.selectTag = function(name) {
if (!tagsInput) return;
const parts = tagsInput.value.split(',');
parts.pop();
parts.push(name);
tagsInput.value = parts.join(', ') + ', ';
if (tagsSuggestions) tagsSuggestions.style.display = 'none';
tagsInput.focus();
};
// 3D Files drop zone
if (dropZone && fileInput) {
dropZone.addEventListener('click', (e) => {
if (e.target !== fileInput) fileInput.click();
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('drop-active'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('drop-active'), false);
});
dropZone.addEventListener('drop', (e) => {
const files = Array.from(e.dataTransfer.files).filter(f =>
f.name.toLowerCase().endsWith('.stl') || f.name.toLowerCase().endsWith('.3mf')
);
if (files.length) handleFiles(files);
}, false);
fileInput.addEventListener('change', () => {
if (fileInput.files.length) handleFiles(Array.from(fileInput.files));
});
}
function handleFiles(files) {
selectedFiles = files;
renderPartsList();
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
if (fileNameDisplay) {
fileNameDisplay.innerHTML = `${files.length} archivo(s) (${(totalSize / 1024).toFixed(1)} KB)`;
}
showToast(`${files.length} archivo(s) 3D seleccionado(s)`, 'success');
}
function renderPartsList() {
if (!partsList) return;
if (selectedFiles.length === 0) {
partsList.innerHTML = '';
return;
}
partsList.innerHTML = selectedFiles.map((f, i) => `
${i + 1}
${f.name}
${(f.size / 1024).toFixed(1)} KB
`).join('');
}
// Images drop zone
if (imagesDropZone && imagesInput) {
imagesDropZone.addEventListener('click', (e) => {
if (e.target !== imagesInput) imagesInput.click();
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
imagesDropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
['dragenter', 'dragover'].forEach(eventName => {
imagesDropZone.addEventListener(eventName, () => imagesDropZone.classList.add('drop-active'), false);
});
['dragleave', 'drop'].forEach(eventName => {
imagesDropZone.addEventListener(eventName, () => imagesDropZone.classList.remove('drop-active'), false);
});
imagesDropZone.addEventListener('drop', (e) => {
const files = Array.from(e.dataTransfer.files).filter(f =>
f.name.toLowerCase().endsWith('.jpg') || f.name.toLowerCase().endsWith('.jpeg') || f.name.toLowerCase().endsWith('.png')
);
if (files.length) handleImages(files);
}, false);
imagesInput.addEventListener('change', () => {
if (imagesInput.files.length) handleImages(Array.from(imagesInput.files));
});
}
function handleImages(files) {
selectedImages = files;
const totalSize = files.reduce((sum, f) => sum + f.size, 0);
if (imagesNameDisplay) {
imagesNameDisplay.innerHTML = `${files.length} imagen(es) (${(totalSize / 1024).toFixed(1)} KB)`;
}
showToast(`${files.length} imagen(es) seleccionada(s)`, 'success');
}
if (uploadForm) {
uploadForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedFiles.length) {
showToast('Selecciona al menos un archivo 3D', 'error');
dropZone.scrollIntoView({ behavior: 'smooth', block: 'center' });
dropZone.classList.add('animate-pulse');
setTimeout(() => dropZone.classList.remove('animate-pulse'), 1000);
return;
}
const titleEl = document.getElementById('title');
if (!titleEl || !titleEl.value.trim()) {
showToast('El titulo es obligatorio', 'error');
titleEl.focus();
return;
}
const formData = new FormData();
selectedFiles.forEach(f => formData.append('files', f));
selectedImages.forEach(f => formData.append('images', f));
formData.append('title', titleEl.value.trim());
const descEl = document.getElementById('description');
const authorEl = document.getElementById('author');
const licenseEl = document.getElementById('license');
const tagsEl = document.getElementById('tags');
const catEl = document.getElementById('category');
if (descEl) formData.append('description', descEl.value);
if (authorEl) formData.append('author', authorEl.value);
if (licenseEl) formData.append('license', licenseEl.value);
if (tagsEl) formData.append('tags', tagsEl.value);
if (catEl) formData.append('category', catEl.value);
// Collect part names
const partNameInputs = document.querySelectorAll('.part-name-input');
const partNames = {};
partNameInputs.forEach(input => {
const idx = input.getAttribute('data-idx');
if (input.value.trim()) {
partNames[idx] = input.value.trim();
}
});
if (Object.keys(partNames).length > 0) {
formData.append('part_names', JSON.stringify(partNames));
}
setLoading(true);
try {
const result = await apiPostForm('/models/', formData);
showToast('Modelo subido correctamente', 'success');
window.location.href = '/model/' + result.id;
} catch (err) {
console.error(err);
if (err.message.includes('already exists')) {
showToast('Este archivo ya existe en el repositorio', 'error');
} else {
showToast('Error al subir: ' + err.message, 'error');
}
setLoading(false);
}
});
}
// ====== IMPORT URL ======
const urlForm = document.getElementById('url-form');
if (urlForm) {
urlForm.addEventListener('submit', async (e) => {
e.preventDefault();
const urlEl = document.getElementById('url-input');
const titleEl = document.getElementById('url-title');
if (!urlEl || !urlEl.value.trim() || !titleEl || !titleEl.value.trim()) {
showToast('URL y titulo son obligatorios', 'error');
return;
}
const formData = new FormData();
formData.append('url', urlEl.value.trim());
formData.append('title', titleEl.value.trim());
const desc = document.getElementById('url-description');
const author = document.getElementById('url-author');
const license = document.getElementById('url-license');
const tags = document.getElementById('url-tags');
const cat = document.getElementById('url-category');
if (desc) formData.append('description', desc.value);
if (author) formData.append('author', author.value);
if (license) formData.append('license', license.value);
if (tags) formData.append('tags', tags.value);
if (cat) formData.append('category', cat.value);
const btn = urlForm.querySelector('button[type="submit"]');
const orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = 'Importando...';
try {
const result = await apiPostForm('/models/import-url', formData);
showToast('Modelo importado correctamente', 'success');
window.location.href = '/model/' + result.id;
} catch (err) {
showToast('Error: ' + err.message, 'error');
btn.disabled = false;
btn.innerHTML = orig;
}
});
}
// ====== BULK ZIP ======
const zipDropZone = document.getElementById('zip-drop-zone');
const zipInput = document.getElementById('zip-input');
const zipNameDisplay = document.getElementById('zip-name');
const zipForm = document.getElementById('zip-form');
let selectedZip = null;
if (zipDropZone && zipInput) {
zipDropZone.addEventListener('click', (e) => {
if (e.target !== zipInput) zipInput.click();
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
zipDropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false);
});
['dragenter', 'dragover'].forEach(eventName => {
zipDropZone.addEventListener(eventName, () => zipDropZone.classList.add('drop-active'), false);
});
['dragleave', 'drop'].forEach(eventName => {
zipDropZone.addEventListener(eventName, () => zipDropZone.classList.remove('drop-active'), false);
});
zipDropZone.addEventListener('drop', (e) => {
const f = Array.from(e.dataTransfer.files).find(fi => fi.name.toLowerCase().endsWith('.zip'));
if (f) handleZip(f);
}, false);
zipInput.addEventListener('change', () => {
if (zipInput.files.length) handleZip(zipInput.files[0]);
});
}
function handleZip(file) {
selectedZip = file;
if (zipNameDisplay) {
zipNameDisplay.innerHTML = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
}
showToast('ZIP seleccionado', 'success');
}
if (zipForm) {
zipForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedZip) {
showToast('Selecciona un archivo ZIP', 'error');
return;
}
const formData = new FormData();
formData.append('zip_file', selectedZip);
const desc = document.getElementById('zip-description');
const author = document.getElementById('zip-author');
const license = document.getElementById('zip-license');
const tags = document.getElementById('zip-tags');
const cat = document.getElementById('zip-category');
if (desc) formData.append('description', desc.value);
if (author) formData.append('author', author.value);
if (license) formData.append('license', license.value);
if (tags) formData.append('tags', tags.value);
if (cat) formData.append('category', cat.value);
const btn = zipForm.querySelector('button[type="submit"]');
const orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = 'Procesando...';
try {
const results = await apiPostForm('/models/bulk-zip', formData);
showToast(`${results.length} modelo(s) importado(s)`, 'success');
setTimeout(() => window.location.href = '/', 1000);
} catch (err) {
showToast('Error: ' + err.message, 'error');
btn.disabled = false;
btn.innerHTML = orig;
}
});
}
function setLoading(v) {
if (!submitBtn) return;
submitBtn.disabled = v;
if (v) {
btnText.textContent = 'Subiendo...';
btnIcon.innerHTML = '';
btnIcon.classList.add('animate-spin');
} else {
btnText.textContent = 'Subir Modelo';
btnIcon.innerHTML = '';
btnIcon.classList.remove('animate-spin');
}
}
});