// API Base URL const API_BASE = '/api'; // State let currentUser = null; let authToken = null; let allCollections = []; let allTags = []; // Initialize document.addEventListener('DOMContentLoaded', () => { checkAuth(); // loadAllModels, loadCollections, and loadTags will be called after auth check succeeds }); // Auth Functions function checkAuth() { authToken = localStorage.getItem('authToken'); if (authToken) { fetch(`${API_BASE}/auth/me`, { headers: { 'Authorization': `Bearer ${authToken}` } }) .then(res => res.json()) .then(data => { if (data.user) { currentUser = data.user; // Apply user's theme preference if (currentUser.theme && typeof setTheme === 'function') { setTheme(currentUser.theme); } updateUIForAuth(); } else { logout(); } }) .catch(() => logout()); } } function updateUIForAuth() { document.getElementById('loginBtn').style.display = 'none'; document.getElementById('registerBtn').style.display = 'none'; document.getElementById('userMenu').style.display = 'flex'; document.getElementById('username').textContent = currentUser.username; document.getElementById('uploadBtn').style.display = 'block'; document.getElementById('createCollectionBtn').style.display = 'block'; // Load user's data after authentication loadAllModels(); loadCollections(); loadTags(); } async function handleLogin(event) { event.preventDefault(); const username = document.getElementById('loginUsername').value; const password = document.getElementById('loginPassword').value; try { const response = await fetch(`${API_BASE}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (response.ok) { authToken = data.token; currentUser = data.user; localStorage.setItem('authToken', authToken); closeModal('loginModal'); updateUIForAuth(); } else { showNotification(data.error || 'Login failed', 'error'); } } catch (error) { showNotification('Login failed: ' + error.message, 'error'); } } async function handleRegister(event) { event.preventDefault(); const username = document.getElementById('registerUsername').value; const email = document.getElementById('registerEmail').value; const password = document.getElementById('registerPassword').value; try { const response = await fetch(`${API_BASE}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password }) }); const data = await response.json(); if (response.ok) { authToken = data.token; currentUser = data.user; localStorage.setItem('authToken', authToken); closeModal('registerModal'); updateUIForAuth(); showNotification('Registration successful!', 'success'); } else { showNotification(data.error || 'Registration failed', 'error'); } } catch (error) { showNotification('Registration failed: ' + error.message, 'error'); } } function logout() { authToken = null; currentUser = null; localStorage.removeItem('authToken'); document.getElementById('loginBtn').style.display = 'block'; document.getElementById('registerBtn').style.display = 'block'; document.getElementById('userMenu').style.display = 'none'; document.getElementById('uploadBtn').style.display = 'none'; document.getElementById('createCollectionBtn').style.display = 'none'; // Clear models display const grid = document.getElementById('modelsGrid'); grid.innerHTML = '

Please log in to view models

'; // Clear sidebar sections document.getElementById('printQueueSection').style.display = 'none'; document.getElementById('exportSection').style.display = 'none'; } // Models Functions async function loadAllModels(search = '', tag = '', collection = '') { const grid = document.getElementById('modelsGrid'); grid.innerHTML = '

Loading models...

'; try { let url = `${API_BASE}/models?`; if (search) url += `search=${encodeURIComponent(search)}&`; if (tag) url += `tag=${encodeURIComponent(tag)}&`; if (collection) url += `collection=${encodeURIComponent(collection)}&`; const response = await fetch(url); const data = await response.json(); if (data.models && data.models.length > 0) { displayModels(data.models); } else { grid.innerHTML = '

No models found

'; } } catch (error) { grid.innerHTML = '

Error loading models

'; console.error('Error loading models:', error); } } function displayModels(models) { const grid = document.getElementById('modelsGrid'); grid.innerHTML = ''; models.forEach(model => { const card = document.createElement('div'); card.className = 'model-card'; card.onclick = () => showModelDetails(model.id); const fileIcon = getFileIcon(model.file_type); const fileSize = formatFileSize(model.file_size); const createdDate = new Date(model.created_at).toLocaleDateString(); // Check if thumbnail exists const previewContent = model.preview_image ? `${escapeHtml(model.name)}` : ``; // Check if 3D viewer is available const has3DViewer = ['.stl', '.obj', '.3mf'].includes(model.file_type); card.innerHTML = `
${previewContent} ${has3DViewer ? ` ` : ''}

${escapeHtml(model.name)}

${escapeHtml(model.description || 'No description')}

${fileSize} ${createdDate}
${model.tags && model.tags.length > 0 ? `
${model.tags.map(tag => `${escapeHtml(tag)}`).join('')}
` : ''}
`; grid.appendChild(card); }); } async function showModelDetails(modelId) { try { const response = await fetch(`${API_BASE}/models/${modelId}`); const model = await response.json(); const detailsHtml = `

${escapeHtml(model.name)}

${model.creator ? `

By ${escapeHtml(model.creator)}

` : ''}
${['.stl', '.obj', '.3mf'].includes(model.file_type) ? ` ` : ''} ${model.files && model.files.length > 0 ? 'Download All (ZIP)' : 'Download'} ${currentUser && currentUser.id === model.user_id ? ` ` : ''}

Description

${escapeHtml(model.description || 'No description available')}

${model.notes ? `

Notes

${escapeHtml(model.notes)}

` : ''}

Tags

${model.tags && model.tags.length > 0 ? model.tags.map(tag => `${escapeHtml(tag)}`).join('') : 'No tags'}

File Information

Primary File: ${escapeHtml(model.file_name)}

File Type: ${escapeHtml(model.file_type)}

File Size: ${formatFileSize(model.file_size)}

Uploaded: ${new Date(model.created_at).toLocaleString()}

${model.files && model.files.length > 0 ? `

Additional Files (${model.files.length}):

    ${model.files.map(file => `
  • ${escapeHtml(file.file_name)} (${formatFileSize(file.file_size)})
  • `).join('')}
` : ''}
${model.collection_name ? `

Collection

${escapeHtml(model.collection_name)}

` : ''} ${model.source_url ? ` ` : ''}

Support Status

${model.is_supported ? 'Includes supports' : 'No supports'}

`; document.getElementById('modelDetails').innerHTML = detailsHtml; showModal('modelModal'); } catch (error) { showNotification('Error loading model details', 'error'); console.error('Error:', error); } } async function handleUpload(event) { event.preventDefault(); if (!authToken) { showNotification('Please login to upload models', 'error'); return; } const formData = new FormData(); // Handle multiple files const fileInput = document.getElementById('uploadFile'); const files = fileInput.files; if (files.length === 0) { showNotification('Please select at least one file', 'error'); return; } // Append all files for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } formData.append('name', document.getElementById('uploadName').value); formData.append('description', document.getElementById('uploadDescription').value); formData.append('creator', document.getElementById('uploadCreator').value); formData.append('source_url', document.getElementById('uploadSourceUrl').value); formData.append('collection_id', document.getElementById('uploadCollection').value); formData.append('is_supported', document.getElementById('uploadSupported').checked); formData.append('notes', document.getElementById('uploadNotes').value); const tagsInput = document.getElementById('uploadTags').value; if (tagsInput) { const tags = tagsInput.split(',').map(t => t.trim()).filter(t => t); formData.append('tags', JSON.stringify(tags)); } try { const response = await fetch(`${API_BASE}/models`, { method: 'POST', headers: { 'Authorization': `Bearer ${authToken}` }, body: formData }); const data = await response.json(); if (response.ok) { closeModal('uploadModal'); const message = data.fileCount > 1 ? `Model uploaded with ${data.fileCount} files!` : 'Model uploaded successfully!'; showNotification(message, 'success'); loadAllModels(); event.target.reset(); } else { showNotification(data.error || 'Upload failed', 'error'); } } catch (error) { showNotification('Upload failed: ' + error.message, 'error'); } } async function deleteModel(modelId) { if (!confirm('Are you sure you want to delete this model?')) { return; } try { const response = await fetch(`${API_BASE}/models/${modelId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${authToken}` } }); if (response.ok) { closeModal('modelModal'); showNotification('Model deleted successfully', 'success'); loadAllModels(); } else { const data = await response.json(); showNotification(data.error || 'Delete failed', 'error'); } } catch (error) { showNotification('Delete failed: ' + error.message, 'error'); } } // Collections Functions async function loadCollections() { try { const response = await fetch(`${API_BASE}/collections`); const data = await response.json(); allCollections = data.collections; const collectionsList = document.getElementById('collectionsList'); if (data.collections.length > 0) { collectionsList.innerHTML = data.collections.map(collection => `
  • ${escapeHtml(collection.name)} (${collection.model_count})
  • `).join(''); } else { collectionsList.innerHTML = '
  • No collections
  • '; } // Update upload modal collection select const uploadCollectionSelect = document.getElementById('uploadCollection'); uploadCollectionSelect.innerHTML = '' + data.collections.map(c => ``).join(''); } catch (error) { console.error('Error loading collections:', error); } } async function handleCreateCollection(event) { event.preventDefault(); if (!authToken) { showNotification('Please login to create collections', 'error'); return; } const name = document.getElementById('collectionName').value; const description = document.getElementById('collectionDescription').value; try { const response = await fetch(`${API_BASE}/collections`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify({ name, description }) }); const data = await response.json(); if (response.ok) { closeModal('createCollectionModal'); showNotification('Collection created successfully!', 'success'); loadCollections(); event.target.reset(); } else { showNotification(data.error || 'Failed to create collection', 'error'); } } catch (error) { showNotification('Failed to create collection: ' + error.message, 'error'); } } function showCollections() { loadCollections(); showNotification('Showing all collections', 'success'); } // Tags Functions async function loadTags() { try { const response = await fetch(`${API_BASE}/tags`); const data = await response.json(); allTags = data.tags; const tagsList = document.getElementById('tagsList'); if (data.tags.length > 0) { const topTags = data.tags.sort((a, b) => b.model_count - a.model_count).slice(0, 10); tagsList.innerHTML = topTags.map(tag => ` ${escapeHtml(tag.name)} (${tag.model_count}) `).join(''); } else { tagsList.innerHTML = 'No tags'; } } catch (error) { console.error('Error loading tags:', error); } } function showTags() { loadTags(); showNotification('Showing all tags', 'success'); } // Search Function let searchTimeout; function handleSearch() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { const searchTerm = document.getElementById('searchInput').value; loadAllModels(searchTerm); }, 300); } // Modal Functions function showModal(modalId) { document.getElementById(modalId).style.display = 'block'; } function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; } function showLoginModal() { showModal('loginModal'); } function showRegisterModal() { showModal('registerModal'); } function showUploadModal() { if (!authToken) { showNotification('Please login to upload models', 'error'); showLoginModal(); return; } showModal('uploadModal'); } function showCreateCollectionModal() { if (!authToken) { showNotification('Please login to create collections', 'error'); showLoginModal(); return; } showModal('createCollectionModal'); } function showSettingsModal() { if (!authToken) { showNotification('Please login to access settings', 'error'); showLoginModal(); return; } // Populate current email document.getElementById('currentEmailDisplay').value = currentUser.email; // Reset forms document.getElementById('emailTab').querySelector('form').reset(); document.getElementById('passwordTab').querySelector('form').reset(); // Show email tab by default switchSettingsTab('email'); showModal('settingsModal'); } function switchSettingsTab(tab) { // Hide all tabs document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active')); // Show selected tab document.getElementById(tab + 'Tab').classList.add('active'); event.target.classList.add('active'); } function updateEmail(event) { event.preventDefault(); const newEmail = document.getElementById('newEmail').value; const password = document.getElementById('emailConfirmPassword').value; fetch(`${API_BASE}/auth/me/email`, { method: 'PUT', headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ newEmail, password }) }) .then(res => res.json()) .then(data => { if (data.error) { showNotification(data.error, 'error'); } else { showNotification('Email updated successfully', 'success'); currentUser.email = newEmail; document.getElementById('currentEmailDisplay').value = newEmail; event.target.reset(); } }) .catch(err => showNotification('Error updating email', 'error')); } function updatePassword(event) { event.preventDefault(); const currentPassword = document.getElementById('currentPassword').value; const newPassword = document.getElementById('newPassword').value; const confirmNewPassword = document.getElementById('confirmNewPassword').value; if (newPassword !== confirmNewPassword) { showNotification('Passwords do not match', 'error'); return; } fetch(`${API_BASE}/auth/me/password`, { method: 'PUT', headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ currentPassword, newPassword }) }) .then(res => res.json()) .then(data => { if (data.error) { showNotification(data.error, 'error'); } else { showNotification('Password updated successfully', 'success'); event.target.reset(); } }) .catch(err => showNotification('Error updating password', 'error')); } // Close modals when clicking outside window.onclick = function(event) { if (event.target.classList.contains('modal')) { event.target.style.display = 'none'; } } // Utility Functions function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text ? String(text).replace(/[&<>"']/g, m => map[m]) : ''; } function formatFileSize(bytes) { if (!bytes) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } function getFileIcon(fileType) { const iconMap = { '.stl': 'fa-cube', '.obj': 'fa-cubes', '.3mf': 'fa-box', '.gcode': 'fa-code', '.zip': 'fa-file-archive' }; return iconMap[fileType] || 'fa-file'; } function showNotification(message, type) { // Simple console notification for now console.log(`${type.toUpperCase()}: ${message}`); // You can implement a toast notification system here alert(message); } // Edit Model Functions async function showEditModal(modelId) { try { // Fetch model details const response = await fetch(`${API_BASE}/models/${modelId}`); const model = await response.json(); // Populate form fields document.getElementById('editModelId').value = model.id; document.getElementById('editModelName').value = model.name || ''; document.getElementById('editModelDescription').value = model.description || ''; document.getElementById('editModelCreator').value = model.creator || ''; document.getElementById('editModelSourceUrl').value = model.source_url || ''; document.getElementById('editModelNotes').value = model.notes || ''; document.getElementById('editModelSupported').checked = model.is_supported === 1; // Populate tags if (model.tags && model.tags.length > 0) { document.getElementById('editModelTags').value = model.tags.join(', '); } else { document.getElementById('editModelTags').value = ''; } // Populate collection dropdown const collectionSelect = document.getElementById('editModelCollection'); collectionSelect.innerHTML = '' + allCollections.map(c => `` ).join(''); // Close model details modal and show edit modal closeModal('modelModal'); showModal('editModelModal'); } catch (error) { console.error('Error loading model for edit:', error); showNotification('Failed to load model details', 'error'); } } async function handleEditModel(event) { event.preventDefault(); if (!authToken) { showNotification('Please login to edit models', 'error'); return; } const modelId = document.getElementById('editModelId').value; const tagsInput = document.getElementById('editModelTags').value; const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : []; const updateData = { name: document.getElementById('editModelName').value, description: document.getElementById('editModelDescription').value, creator: document.getElementById('editModelCreator').value, source_url: document.getElementById('editModelSourceUrl').value, collection_id: document.getElementById('editModelCollection').value || null, is_supported: document.getElementById('editModelSupported').checked, notes: document.getElementById('editModelNotes').value, tags: tags }; try { const response = await fetch(`${API_BASE}/models/${modelId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, body: JSON.stringify(updateData) }); const data = await response.json(); if (response.ok) { closeModal('editModelModal'); showNotification('Model updated successfully!', 'success'); loadAllModels(); // Refresh the models list } else { showNotification(data.error || 'Failed to update model', 'error'); } } catch (error) { showNotification('Failed to update model: ' + error.message, 'error'); } }