UNPKG

collectlie

Version:

TypeScript SDK for Collectlie - flexible data collection platform with custom types, schema validation, and Supabase backend integration

1,655 lines (1,395 loc) 53.5 kB
# Vanilla JavaScript Examples Complete examples for using collectlie with vanilla JavaScript. ## Installation ```bash npm install collectlie ``` Or use via CDN: ```html <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> ``` ## Basic Usage ### ES Modules (Modern Browsers) ```javascript import { Collectlie } from 'collectlie'; const collectlie = new Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); // Submit feedback await sdk.submit({ type: 'issue', title: 'Bug Report', content: 'Found a problem with the login form' }); ``` ### CDN Usage (UMD) ```html <!DOCTYPE html> <html> <head> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> </head> <body> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); // Submit feedback sdk.submit({ type: 'feedback', title: 'Great Product!', content: 'Love the new features' }).then(result => { console.log('Success:', result); }).catch(error => { console.error('Error:', error.message); }); </script> </body> </html> ``` ## Complete Form Example with File Upload ### HTML Structure ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Feedback Form</title> <style> .form-container { max-width: 500px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 8px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; } button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background-color: #6c757d; cursor: not-allowed; } .message { padding: 10px; border-radius: 4px; margin-top: 10px; } .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .file-item { transition: background-color 0.2s; } .file-item:hover { background-color: #f8f9fa; } </style> </head> <body> <div class="form-container"> <h2>Submit Feedback</h2> <form id="feedback-form"> <div class="form-group"> <label for="type">Type:</label> <select id="type" name="type" required> <option value="">Select type...</option> <option value="bug">Bug Report</option> <option value="feature">Feature Request</option> <option value="feedback">General Feedback</option> <option value="rating">Rating</option> </select> </div> <div class="form-group"> <label for="title">Title:</label> <input type="text" id="title" name="title" placeholder="Brief description" required> </div> <div class="form-group"> <label for="content">Content:</label> <textarea id="content" name="content" rows="4" placeholder="Detailed description" required></textarea> </div> <div class="form-group"> <label for="author-email">Email (optional):</label> <input type="email" id="author-email" name="author-email" placeholder="your@email.com"> </div> <div class="form-group"> <label for="author-name">Name (optional):</label> <input type="text" id="author-name" name="author-name" placeholder="Your name"> </div> <div class="form-group"> <label for="priority">Priority:</label> <select id="priority" name="priority"> <option value="low">Low</option> <option value="medium" selected>Medium</option> <option value="high">High</option> </select> </div> <div class="form-group"> <label for="files">Attach Files (optional):</label> <input type="file" id="files" name="files" multiple accept="image/*"> <small>File limits enforced by backend: 1MB each, images only (JPEG, PNG, GIF, WebP).</small> <div id="file-list"></div> </div> <button type="submit" id="submit-btn">Submit Feedback</button> <div id="message"></div> </form> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> // Initialize SDK with file upload configuration const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key', maxFileSize: 1 * 1024 * 1024, // 1MB per file (backend enforced) // Note: No attachment count limit - backend handles validation allowedFileTypes: [ // Allowed file types 'image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/webm', 'application/pdf', 'text/plain' ] }); // Get form elements const form = document.getElementById('feedback-form'); const submitBtn = document.getElementById('submit-btn'); const messageDiv = document.getElementById('message'); const fileInput = document.getElementById('files'); const fileList = document.getElementById('file-list'); // File handling let selectedFiles = []; fileInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); displayFileList(); }); function displayFileList() { if (selectedFiles.length === 0) { fileList.innerHTML = ''; return; } const listHTML = selectedFiles.map((file, index) => ` <div class="file-item" style="display: flex; justify-content: space-between; align-items: center; padding: 5px; border: 1px solid #ddd; margin: 5px 0; border-radius: 4px;"> <div> <strong>${file.name}</strong><br> <small>${file.type} - ${(file.size / 1024 / 1024).toFixed(2)} MB</small> </div> <button type="button" onclick="removeFile(${index})" style="background: #dc3545; color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer;">Remove</button> </div> `).join(''); fileList.innerHTML = ` <h4>Selected Files (${selectedFiles.length}/5):</h4> ${listHTML} `; } // Make removeFile globally accessible window.removeFile = function(index) { selectedFiles.splice(index, 1); // Update file input const dt = new DataTransfer(); selectedFiles.forEach(file => dt.items.add(file)); fileInput.files = dt.files; displayFileList(); }; // Form submission handler form.addEventListener('submit', async (e) => { e.preventDefault(); // Disable submit button submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; messageDiv.innerHTML = ''; try { // Get form data const formData = new FormData(form); // Prepare submission data const authorEmail = formData.get('author-email'); const authorName = formData.get('author-name'); // Prepare attachments const attachments = selectedFiles.map(file => ({ filename: file.name, mimeType: file.type, size: file.size, data: file })); const submissionData = { type: formData.get('type'), title: formData.get('title'), content: formData.get('content'), author: authorEmail || authorName ? { email: authorEmail || undefined, name: authorName || undefined } : undefined, attachments: attachments.length > 0 ? attachments : undefined, priority: formData.get('priority') // Custom field }; // Submit with automatic data gathering const result = await collectlie.submit(submissionData, { browser: true, // Browser info (replaces manual navigator.userAgent) device: true, // Device type and capabilities source: true, // Page URL, title, referrer performance: true, // Page load time and performance errors: true, // Any JavaScript errors environment: true // Connection status, preferences }); // Show success message messageDiv.innerHTML = '<div class="message success">Thank you for your feedback! Your submission has been received.</div>'; // Reset form and files form.reset(); selectedFiles = []; displayFileList(); console.log('Submission successful:', result); } catch (error) { // Show error message let errorMessage = 'An error occurred. Please try again.'; if (error.message.includes('API key')) { errorMessage = 'Configuration error. Please contact support.'; } else if (error.message.includes('timeout')) { errorMessage = 'Request timed out. Please check your connection and try again.'; } else if (error.message.includes('Validation failed')) { errorMessage = 'Please check your input and try again.'; } else if (error.message.includes('File size exceeds')) { errorMessage = 'One or more files are too large. Please use files smaller than 1MB.'; } else if (error.message.includes('File type not allowed')) { errorMessage = 'Please only upload supported file types (images, videos, PDFs, text files).'; } else if (error.message.includes('Too many attachments')) { errorMessage = 'File upload failed. Please try again.'; } messageDiv.innerHTML = `<div class="message error">${errorMessage}</div>`; console.error('Submission error:', error); } finally { // Re-enable submit button submitBtn.disabled = false; submitBtn.textContent = 'Submit Feedback'; } }); </script> </body> </html> ``` ## Rating Widget Example ```html <!DOCTYPE html> <html> <head> <title>Rating Widget</title> <style> .rating-widget { text-align: center; padding: 20px; border: 1px solid #ddd; border-radius: 8px; max-width: 400px; margin: 50px auto; } .stars { font-size: 2em; margin: 20px 0; } .star { color: #ddd; cursor: pointer; transition: color 0.2s; } .star:hover, .star.active { color: #ffd700; } .comment-box { margin: 20px 0; } textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; resize: vertical; } button { background-color: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="rating-widget"> <h3>How was your experience?</h3> <div class="stars" id="stars"> <span class="star" data-rating="1">★</span> <span class="star" data-rating="2">★</span> <span class="star" data-rating="3">★</span> <span class="star" data-rating="4">★</span> <span class="star" data-rating="5">★</span> </div> <div class="comment-box"> <textarea id="comment" placeholder="Tell us more about your experience (optional)" rows="3"></textarea> </div> <button id="submit-rating">Submit Rating</button> <div id="rating-message"></div> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); let selectedRating = 0; // Star selection logic const stars = document.querySelectorAll('.star'); const submitBtn = document.getElementById('submit-rating'); const messageDiv = document.getElementById('rating-message'); const commentBox = document.getElementById('comment'); stars.forEach((star, index) => { star.addEventListener('click', () => { selectedRating = index + 1; updateStars(); }); star.addEventListener('mouseover', () => { highlightStars(index + 1); }); }); document.getElementById('stars').addEventListener('mouseleave', () => { updateStars(); }); function highlightStars(rating) { stars.forEach((star, index) => { if (index < rating) { star.classList.add('active'); } else { star.classList.remove('active'); } }); } function updateStars() { highlightStars(selectedRating); } // Submit rating submitBtn.addEventListener('click', async () => { if (selectedRating === 0) { messageDiv.innerHTML = '<div style="color: red;">Please select a rating</div>'; return; } submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; try { await sdk.submit({ type: 'rating', title: `${selectedRating} Star Rating`, content: commentBox.value || undefined, author: { email: 'user@example.com' // From your authentication system }, metadata: { rating: selectedRating, category: 'overall', page_url: window.location.href, user_agent: navigator.userAgent } }); messageDiv.innerHTML = '<div style="color: green;">Thank you for your rating!</div>'; // Reset form selectedRating = 0; updateStars(); commentBox.value = ''; } catch (error) { messageDiv.innerHTML = '<div style="color: red;">Failed to submit rating. Please try again.</div>'; console.error('Rating submission error:', error); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Submit Rating'; } }); </script> </body> </html> ``` ## Dynamic Form Builder ```html <!DOCTYPE html> <html> <head> <title>Dynamic Feedback Form</title> <style> .container { max-width: 600px; margin: 50px auto; padding: 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, select, textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } .field-controls { margin-top: 10px; } .add-field { background-color: #28a745; margin-right: 10px; } </style> </head> <body> <div class="container"> <h2>Dynamic Feedback Form</h2> <div class="form-group"> <label>Submission Type:</label> <select id="submission-type"> <option value="bug">Bug Report</option> <option value="feature">Feature Request</option> <option value="feedback">General Feedback</option> </select> </div> <div id="dynamic-fields"></div> <div class="field-controls"> <button class="add-field" onclick="addTextField()">Add Text Field</button> <button class="add-field" onclick="addSelectField()">Add Select Field</button> <button class="add-field" onclick="addCheckboxField()">Add Checkbox</button> </div> <br><br> <button onclick="submitForm()">Submit Form</button> <div id="result"></div> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); let fieldCounter = 0; function addTextField() { const container = document.getElementById('dynamic-fields'); const fieldId = `field_${fieldCounter++}`; const fieldDiv = document.createElement('div'); fieldDiv.className = 'form-group'; fieldDiv.innerHTML = ` <label for="${fieldId}">Text Field:</label> <input type="text" id="${fieldId}" name="${fieldId}" placeholder="Enter text"> <button onclick="removeField(this)" style="margin-top: 5px; background-color: #dc3545;">Remove</button> `; container.appendChild(fieldDiv); } function addSelectField() { const container = document.getElementById('dynamic-fields'); const fieldId = `field_${fieldCounter++}`; const fieldDiv = document.createElement('div'); fieldDiv.className = 'form-group'; fieldDiv.innerHTML = ` <label for="${fieldId}">Select Field:</label> <select id="${fieldId}" name="${fieldId}"> <option value="option1">Option 1</option> <option value="option2">Option 2</option> <option value="option3">Option 3</option> </select> <button onclick="removeField(this)" style="margin-top: 5px; background-color: #dc3545;">Remove</button> `; container.appendChild(fieldDiv); } function addCheckboxField() { const container = document.getElementById('dynamic-fields'); const fieldId = `field_${fieldCounter++}`; const fieldDiv = document.createElement('div'); fieldDiv.className = 'form-group'; fieldDiv.innerHTML = ` <label> <input type="checkbox" id="${fieldId}" name="${fieldId}" value="checked"> Checkbox Field </label> <button onclick="removeField(this)" style="margin-top: 5px; background-color: #dc3545;">Remove</button> `; container.appendChild(fieldDiv); } function removeField(button) { button.parentElement.remove(); } async function submitForm() { try { // Collect all form data const type = document.getElementById('submission-type').value; const customFields = {}; // Get all dynamic field values const fields = document.querySelectorAll('#dynamic-fields input, #dynamic-fields select'); fields.forEach(field => { if (field.type === 'checkbox') { customFields[field.name] = field.checked; } else { customFields[field.name] = field.value; } }); // Submit to API const result = await sdk.submit({ type: type, title: `Dynamic ${type} submission`, content: `Form submitted with ${Object.keys(customFields).length} custom fields`, author: { email: 'user@example.com' // From your authentication system }, metadata: customFields }); document.getElementById('result').innerHTML = '<div style="color: green; margin-top: 10px;">Form submitted successfully!</div>'; console.log('Submission result:', result); } catch (error) { document.getElementById('result').innerHTML = '<div style="color: red; margin-top: 10px;">Submission failed: ' + error.message + '</div>'; console.error('Submission error:', error); } } // Add initial fields addTextField(); addSelectField(); </script> </body> </html> ``` ## Environment Variable Usage For production applications, use environment variables: ```javascript // With build tools that support environment variables const sdk = new Collectlie(); // Automatically uses COLLECTLIE_PROJECT_ID and COLLECTLIE_API_KEY // Manual configuration as fallback const collectlie = new Collectlie({ projectId: process.env.COLLECTLIE_PROJECT_ID || 'fallback_project_id', apiKey: process.env.COLLECTLIE_API_KEY || 'proj_fallback_key' }); ``` ## Error Handling Patterns ```javascript async function submitFeedback(data) { try { return await sdk.submit(data); } catch (error) { console.error('Submission failed:', error.message); throw error; } } // Usage try { const result = await submitFeedback({ type: 'issue', title: 'Network Problem', content: 'Intermittent connectivity issues', author: { email: 'admin@company.com', name: 'System Admin' } }); console.log('Success:', result); } catch (error) { console.error('All attempts failed:', error.message); } ``` ## Performance Optimization ```javascript // Initialize SDK once and reuse const collectlie = new Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key', validateSchemas: true // Enable validation }); // Debounce rapid submissions let submitTimeout; function debouncedSubmit(data, delay = 1000) { clearTimeout(submitTimeout); submitTimeout = setTimeout(() => { sdk.submit(data); }, delay); } // Usage in forms document.getElementById('feedback-input').addEventListener('input', (e) => { debouncedSubmit({ type: 'draft', content: e.target.value, author: { email: 'user@example.com' // From your authentication system } }, 2000); // Save draft every 2 seconds }); ``` ## Drag and Drop File Upload Example ```html <!DOCTYPE html> <html> <head> <title>Drag & Drop File Upload</title> <style> .container { max-width: 600px; margin: 50px auto; padding: 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; } .drop-zone { border: 2px dashed #ccc; border-radius: 8px; padding: 40px; text-align: center; transition: all 0.3s ease; cursor: pointer; background: #fafafa; position: relative; } .drop-zone.dragover { border-color: #007bff; background: #e3f2fd; } .file-list { margin-top: 15px; } .file-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 5px; background: white; } .file-remove { background: #dc3545; color: white; border: none; border-radius: 4px; padding: 4px 8px; cursor: pointer; } button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; } </style> </head> <body> <div class="container"> <h2>Bug Report with Drag & Drop Upload</h2> <form id="bug-form"> <div class="form-group"> <label for="title">Bug Title:</label> <input type="text" id="title" name="title" required> </div> <div class="form-group"> <label for="description">Description:</label> <textarea id="description" name="description" rows="4" required></textarea> </div> <div class="form-group"> <label>Attach Screenshots or Files:</label> <div class="drop-zone" id="drop-zone"> 📁 Drag files here or click to browse<br> <small>Backend enforces: 1MB each, images only.</small> <input type="file" id="file-input" multiple accept="image/*,video/*,.pdf,.txt" style="position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer;"> </div> <div class="file-list" id="file-list"></div> </div> <button type="submit">Submit Bug Report</button> <div id="status" style="margin-top: 15px;"></div> </form> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key', maxFileSize: 10 * 1024 * 1024, // Note: No client-side attachment limits }); let selectedFiles = []; // DOM elements const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const fileList = document.getElementById('file-list'); const form = document.getElementById('bug-form'); const status = document.getElementById('status'); // Drag and drop handlers dropZone.addEventListener('dragover', handleDragOver); dropZone.addEventListener('dragleave', handleDragLeave); dropZone.addEventListener('drop', handleDrop); dropZone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFileSelect); form.addEventListener('submit', handleSubmit); function handleDragOver(e) { e.preventDefault(); dropZone.classList.add('dragover'); } function handleDragLeave(e) { e.preventDefault(); dropZone.classList.remove('dragover'); } function handleDrop(e) { e.preventDefault(); dropZone.classList.remove('dragover'); const files = Array.from(e.dataTransfer.files); addFiles(files); } function handleFileSelect(e) { const files = Array.from(e.target.files); addFiles(files); } function addFiles(files) { const validFiles = []; const errors = []; files.forEach(file => { // Check file count if (selectedFiles.length + validFiles.length >= 5) { // Note: No client-side file count limits return; } // Check file size if (file.size > 10 * 1024 * 1024) { errors.push(`${file.name} is too large (max 1MB)`); return; } // Check file type const allowedTypes = ['image/', 'video/', 'application/pdf', 'text/plain']; const isAllowed = allowedTypes.some(type => file.type.startsWith(type)); if (!isAllowed) { errors.push(`${file.name} is not a supported file type`); return; } validFiles.push(file); }); if (errors.length > 0) { showStatus(errors.join('. '), 'error'); } selectedFiles.push(...validFiles); updateFileList(); } function removeFile(index) { selectedFiles.splice(index, 1); updateFileList(); } function updateFileList() { if (selectedFiles.length === 0) { fileList.innerHTML = ''; return; } const html = selectedFiles.map((file, index) => ` <div class="file-item"> <div> <strong>${file.name}</strong><br> <small>${file.type} - ${(file.size / 1024 / 1024).toFixed(2)} MB</small> </div> <button type="button" class="file-remove" onclick="removeFile(${index})"> Remove </button> </div> `).join(''); fileList.innerHTML = ` <h4>Files to Upload (${selectedFiles.length}/5):</h4> ${html} `; } async function handleSubmit(e) { e.preventDefault(); const title = document.getElementById('title').value; const description = document.getElementById('description').value; if (!title || !description) { showStatus('Please fill in all required fields', 'error'); return; } showStatus('Uploading...', 'info'); try { const attachments = selectedFiles.map(file => ({ filename: file.name, mimeType: file.type, size: file.size, data: file })); const result = await sdk.submit({ type: 'bug', title: title, content: description, author: { email: 'user@example.com' // Get from your auth system }, attachments: attachments, metadata: { browser: navigator.userAgent, page_url: window.location.href, timestamp: new Date().toISOString() } }); showStatus(`✅ Bug report submitted successfully! ID: ${result.data.id}`, 'success'); // Reset form form.reset(); selectedFiles = []; updateFileList(); } catch (error) { let errorMessage = '❌ Failed to submit bug report'; if (error.message.includes('file size')) { errorMessage = '❌ One or more files are too large'; } else if (error.message.includes('file type')) { errorMessage = '❌ Unsupported file type detected'; } else if (error.message.includes('too many attachments')) { errorMessage = '❌ Too many files attached'; } showStatus(errorMessage, 'error'); console.error('Submission error:', error); } } function showStatus(message, type) { status.innerHTML = message; status.style.color = type === 'error' ? '#dc3545' : type === 'success' ? '#28a745' : '#007bff'; } // Make removeFile globally accessible window.removeFile = removeFile; </script> </body> </html> ``` ## File Upload Progress Indicator ```html <!DOCTYPE html> <html> <head> <title>File Upload with Progress</title> <style> .container { max-width: 500px; margin: 50px auto; padding: 20px; } .upload-area { border: 2px dashed #ddd; border-radius: 8px; padding: 30px; text-align: center; transition: border-color 0.3s; cursor: pointer; } .upload-area:hover { border-color: #007bff; } .upload-area.uploading { border-color: #28a745; background: #f8fff8; } .progress-bar { width: 100%; height: 20px; background: #f0f0f0; border-radius: 10px; overflow: hidden; margin: 10px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #007bff, #0056b3); width: 0%; transition: width 0.3s; } .file-info { background: #f8f9fa; padding: 10px; border-radius: 4px; margin: 10px 0; text-align: left; } </style> </head> <body> <div class="container"> <h2>Upload Files with Progress</h2> <div class="upload-area" id="upload-area"> <div id="upload-content"> 📎 Click to select files or drag them here<br> <small>Images up to 1MB (backend enforced)</small> </div> <input type="file" id="file-input" multiple accept="image/*,.pdf,.txt" style="display: none;"> <div id="progress-container" style="display: none;"> <div class="progress-bar"> <div class="progress-fill" id="progress-fill"></div> </div> <div id="progress-text">Uploading...</div> </div> </div> <div id="file-info"></div> <div id="result"></div> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); const uploadArea = document.getElementById('upload-area'); const fileInput = document.getElementById('file-input'); const progressContainer = document.getElementById('progress-container'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); const fileInfo = document.getElementById('file-info'); const result = document.getElementById('result'); const uploadContent = document.getElementById('upload-content'); uploadArea.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleFiles); // Drag and drop uploadArea.addEventListener('dragover', e => { e.preventDefault(); uploadArea.style.borderColor = '#007bff'; }); uploadArea.addEventListener('dragleave', e => { e.preventDefault(); uploadArea.style.borderColor = '#ddd'; }); uploadArea.addEventListener('drop', e => { e.preventDefault(); uploadArea.style.borderColor = '#ddd'; const files = Array.from(e.dataTransfer.files); handleFiles({ target: { files } }); }); async function handleFiles(event) { const files = Array.from(event.target.files); if (files.length === 0) return; // Show file info const fileInfoHtml = files.map(file => ` <div class="file-info"> <strong>${file.name}</strong><br> <small>${file.type} - ${(file.size / 1024 / 1024).toFixed(2)} MB</small> </div> `).join(''); fileInfo.innerHTML = fileInfoHtml; // Start upload await uploadFiles(files); } async function uploadFiles(files) { uploadArea.classList.add('uploading'); uploadContent.style.display = 'none'; progressContainer.style.display = 'block'; try { // Simulate upload progress (the SDK handles actual upload internally) let progress = 0; const progressInterval = setInterval(() => { progress += Math.random() * 15; if (progress > 90) progress = 90; progressFill.style.width = progress + '%'; progressText.textContent = `Uploading... ${Math.round(progress)}%`; }, 200); // Prepare attachments const attachments = files.map(file => ({ filename: file.name, mimeType: file.type, size: file.size, data: file })); // Submit with files const response = await sdk.submit({ type: 'feedback', title: 'File Upload Test', content: `Uploading ${files.length} file(s) for review`, attachments: attachments, author: { email: 'user@example.com' } }); // Complete progress clearInterval(progressInterval); progressFill.style.width = '100%'; progressText.textContent = 'Upload complete!'; setTimeout(() => { result.innerHTML = ` <div style="color: green; margin-top: 15px;"> ✅ Successfully uploaded ${files.length} file(s)!<br> <small>Submission ID: ${response.data.id}</small> </div> `; // Reset UI resetUI(); }, 1000); } catch (error) { clearInterval(progressInterval); result.innerHTML = ` <div style="color: red; margin-top: 15px;"> ❌ Upload failed: ${error.message} </div> `; resetUI(); } } function resetUI() { setTimeout(() => { uploadArea.classList.remove('uploading'); uploadContent.style.display = 'block'; progressContainer.style.display = 'none'; progressFill.style.width = '0%'; fileInfo.innerHTML = ''; fileInput.value = ''; }, 2000); } </script> </body> </html> ``` ## File Upload with Real-Time Validation ```html <!DOCTYPE html> <html> <head> <title>Real-Time File Validation</title> <style> .upload-zone { border: 2px dashed #ccc; border-radius: 8px; padding: 20px; text-align: center; transition: all 0.3s ease; } .upload-zone.valid { border-color: #28a745; background: #f8fff8; } .upload-zone.invalid { border-color: #dc3545; background: #fff8f8; } .file-validation { display: flex; justify-content: space-between; align-items: center; padding: 8px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; } .validation-pass { border-color: #28a745; background: #f8fff8; } .validation-fail { border-color: #dc3545; background: #fff8f8; } .validation-icon { font-size: 16px; } </style> </head> <body> <div style="max-width: 600px; margin: 50px auto; padding: 20px;"> <h2>File Upload with Real-Time Validation</h2> <div class="upload-zone" id="upload-zone"> 📁 Click or drag files here<br> <small>Backend enforces: 1MB each, images only.</small> <input type="file" id="file-input" multiple accept="image/*,.pdf,.txt" style="position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer;"> </div> <div id="validation-results" style="margin-top: 20px;"></div> <form id="submission-form" style="margin-top: 20px;"> <input type="text" id="title" placeholder="Title" required style="width: 100%; margin: 10px 0; padding: 8px;"> <textarea id="content" placeholder="Description" required style="width: 100%; margin: 10px 0; padding: 8px;" rows="4"></textarea> <button type="submit" id="submit-btn" disabled>Submit with Files</button> </form> <div id="status" style="margin-top: 15px;"></div> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key', maxFileSize: 10 * 1024 * 1024, // Note: No client-side attachment limits, enableFileValidation: true }); let selectedFiles = []; let validationResults = []; const uploadZone = document.getElementById('upload-zone'); const fileInput = document.getElementById('file-input'); const validationContainer = document.getElementById('validation-results'); const submitBtn = document.getElementById('submit-btn'); const form = document.getElementById('submission-form'); const status = document.getElementById('status'); // File validation configuration const validation = { maxSize: 1 * 1024 * 1024, // 1MB (backend enforced) maxFiles: 5, allowedTypes: [ 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain' ], allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'] }; // Real-time file validation function validateFiles(files) { const results = []; const fileArray = Array.from(files); for (let i = 0; i < fileArray.length; i++) { const file = fileArray[i]; const result = { file: file, valid: true, errors: [] }; // File count validation if (selectedFiles.length + results.filter(r => r.valid).length >= validation.maxFiles) { result.valid = false; result.errors.push(`Exceeds maximum ${validation.maxFiles} files`); } // File size validation if (file.size > validation.maxSize) { result.valid = false; result.errors.push(`File too large (${(file.size / 1024 / 1024).toFixed(1)}MB > ${validation.maxSize / 1024 / 1024}MB)`); } // File type validation if (!validation.allowedTypes.includes(file.type)) { result.valid = false; result.errors.push(`Unsupported file type: ${file.type}`); } // File extension validation const extension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')); if (!validation.allowedExtensions.includes(extension)) { result.valid = false; result.errors.push(`Unsupported file extension: ${extension}`); } // Filename validation if (file.name.length > 255) { result.valid = false; result.errors.push('Filename too long (max 255 characters)'); } // Check for dangerous filenames const dangerousPatterns = [/\.exe$/, /\.bat$/, /\.cmd$/, /\.scr$/, /\.vbs$/]; if (dangerousPatterns.some(pattern => pattern.test(file.name.toLowerCase()))) { result.valid = false; result.errors.push('Potentially dangerous file type'); } results.push(result); } return results; } function displayValidationResults(results) { validationResults = results; const validFiles = results.filter(r => r.valid); const invalidFiles = results.filter(r => !r.valid); // Update upload zone appearance uploadZone.classList.remove('valid', 'invalid'); if (results.length > 0) { uploadZone.classList.add(invalidFiles.length === 0 ? 'valid' : 'invalid'); } // Display validation results const html = results.map((result, index) => { const iconClass = result.valid ? 'validation-pass' : 'validation-fail'; const icon = result.valid ? '✅' : '❌'; return ` <div class="file-validation ${iconClass}"> <div> <span class="validation-icon">${icon}</span> <strong>${result.file.name}</strong> <small>(${(result.file.size / 1024 / 1024).toFixed(2)} MB)</small> ${result.errors.length > 0 ? `<div style="color: #dc3545; font-size: 12px;">${result.errors.join(', ')}</div>` : ''} </div> <button type="button" onclick="removeValidationResult(${index})" style="background: #dc3545; color: white; border: none; border-radius: 4px; padding: 4px 8px;">Remove</button> </div> `; }).join(''); validationContainer.innerHTML = results.length > 0 ? `<h4>File Validation Results (${validFiles.length}/${results.length} valid):</h4>${html}` : ''; // Update submit button state submitBtn.disabled = validFiles.length === 0; submitBtn.textContent = validFiles.length > 0 ? `Submit with ${validFiles.length} file(s)` : 'Submit (no files)'; } function removeValidationResult(index) { validationResults.splice(index, 1); displayValidationResults(validationResults); updateSelectedFiles(); } function updateSelectedFiles() { selectedFiles = validationResults .filter(result => result.valid) .map(result => result.file); } // Event handlers fileInput.addEventListener('change', (e) => { const results = validateFiles(e.target.files); displayValidationResults(results); updateSelectedFiles(); }); uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.style.borderColor = '#007bff'; }); uploadZone.addEventListener('dragleave', (e) => { e.preventDefault(); uploadZone.style.borderColor = '#ccc'; }); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.style.borderColor = '#ccc'; const results = validateFiles(e.dataTransfer.files); displayValidationResults(results); updateSelectedFiles(); }); form.addEventListener('submit', async (e) => { e.preventDefault(); const title = document.getElementById('title').value; const content = document.getElementById('content').value; if (!title || !content) { status.innerHTML = '<div style="color: red;">❌ Please fill in all required fields</div>'; return; } submitBtn.disabled = true; status.innerHTML = '<div style="color: blue;">⏳ Uploading files and submitting...</div>'; try { const attachments = selectedFiles.map(file => ({ filename: file.name, mimeType: file.type, size: file.size, data: file })); const result = await sdk.submit({ type: 'feedback', title: title, content: content, attachments: attachments, author: { email: 'user@example.com' // Replace with actual user email }, metadata: { file_count: attachments.length, total_size: attachments.reduce((sum, att) => sum + att.size, 0), validation_passed: true, upload_method: 'drag_drop_validation' } }); status.innerHTML = ` <div style="color: green;"> ✅ Successfully submitted with ${attachments.length} file(s)!<br> <small>Submission ID: ${result.data.id}</small> </div> `; // Reset form form.reset(); selectedFiles = []; validationResults = []; displayValidationResults([]); } catch (error) { let errorMessage = '❌ Upload failed: ' + error.message; if (error.message.includes('file size')) { errorMessage = '❌ One or more files are too large'; } else if (error.message.includes('file type')) { errorMessage = '❌ Unsupported file type detected'; } else if (error.message.includes('virus')) { errorMessage = '❌ File failed security scan'; } status.innerHTML = `<div style="color: red;">${errorMessage}</div>`; } finally { submitBtn.disabled = selectedFiles.length === 0; } }); // Make function globally accessible window.removeValidationResult = removeValidationResult; </script> </body> </html> ``` ## Batch File Processing Example ```html <!DOCTYPE html> <html> <head> <title>Batch File Processing</title> <style> .batch-container { max-width: 800px; margin: 50px auto; padding: 20px; } .file-queue { border: 1px solid #ddd; border-radius: 4px; padding: 15px; margin: 15px 0; } .queue-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; margin: 5px 0; border: 1px solid #eee; border-radius: 4px; } .status-pending { background: #fff3cd; } .status-uploading { background: #cff4fc; } .status-completed { background: #d1e7dd; } .status-failed { background: #f8d7da; } .progress-bar { width: 100px; height: 8px; background: #eee; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: #007bff; transition: width 0.3s; } </style> </head> <body> <div class="batch-container"> <h2>Batch File Upload Processing</h2> <div> <input type="file" id="file-input" multiple accept="image/*,video/*,.pdf,.txt"> <button onclick="addToQueue()" id="add-btn">Add to Queue</button> <button onclick="processQueue()" id="process-btn" disabled>Process Queue</button> <button onclick="clearQueue()" id="clear-btn">Clear Queue</button> </div> <div class="file-queue" id="file-queue"> <h3>Upload Queue (0 files)</h3> <div id="queue-items"></div> </div> <div id="batch-status"></div> </div> <script src="https://unpkg.com/collectlie/dist/index.umd.js"></script> <script> const collectlie = new Collectlie.Collectlie({ projectId: 'your_project_id', apiKey: 'proj_your_api_key' }); let uploadQueue = []; let isProcessing = false; function addToQueue() { const fileInput = document.getElementById('file-input'); const files = Array.from(fileInput.files); if (files.length === 0) { alert('Please select files to add to queue'); return; } // Add files to queue with metadata const newItems = files.map(file => ({ id: Date.now() + Math.random(), file: file, status: 'pending', progress: 0, error: null, submissionId: null })); uploadQueue.push(...newItems); fileInput.value = ''; // Clear input updateQueueDisplay(); } function updateQueueDisplay() { const queueContainer = document.getElementById('queue-items'); const queueTitle = document.querySelector('.file-queue h3'); queueTitle.textContent = `Upload Queue (${uploadQueue.length} files)`; if (uploadQueue.length === 0) { queueContainer.innerHTML = '<p style="color: #666; text-align: center;">No files in queue</p>'; document.getElementById('process-btn').disabled = true; return; } const html = uploadQueue.map(item => ` <div class="queue-item status-${item.status}" data-id="${item.id}"> <div> <strong>${item.file.name}</strong> <small>(${(item.file.size / 1024 / 1024).toFixed(2)} MB)</small> ${item.error ? `<div style="color: red; font-size: 12px;">${item.error}</div>` : ''} ${item.submissionId ? `<div style="color: green; font-size: 12px;">ID: ${item.submissionId}</div>` : ''} </div> <div style="display: flex; align-items: center; gap: 10px;"> <div class="progress-bar"> <div class="progress-fill" style="width: ${item.progress}%"></div> </div> <span>${getStatusText(item.status)} ${item.progress}%</span> <button onclick="removeFromQueue('${item.id}')" ${item.status === 'uploading' ? 'disabled' : ''}>Remove</button> </div> </div> `).join(''); queueContainer.innerHTML = html; document.getElementById('process-btn').disabled = isProcessing || uploadQueue.length === 0; } function getStatusText(status) { switch (status) { case 'pending': return '⏸️'; case 'uploading': return '⏳'; case 'completed': return '✅'; case 'failed': return '❌'; default: return '?'; } } function removeFromQueue(itemId) { uploadQueue = uploadQueue.filter(item => item.id !== itemId); updateQueueDisplay(); } async function processQueue() { if (isProcessing) return; isProcessing = true; document.getElementById('process-btn').disabled = true; const batchStatus = document.getElementById('batch-status'); batchStatus.innerHTML = '<h3>Processing batch upload...</h3>';