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
Markdown
# 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>';