Files
unitforge/frontend/static/js/main.js
William Valentin 860f60591c Fix contrast issues with text-muted and bg-dark classes
- Fixed Bootstrap bg-dark class to use better contrasting color
- Added comprehensive text-muted contrast fixes for various contexts
- Improved dark theme colors for better accessibility
- Fixed CSS inheritance issues for code elements in dark contexts
- All color choices meet WCAG AA contrast requirements
2025-09-14 14:58:35 -07:00

383 lines
12 KiB
JavaScript

// UnitForge Main JavaScript
// Handles general functionality across the application
class UnitForge {
constructor() {
this.baseUrl = window.location.origin;
this.apiUrl = `${this.baseUrl}/api`;
this.init();
}
init() {
this.setupEventListeners();
this.initializeTooltips();
}
setupEventListeners() {
// Upload modal functionality
const fileInput = document.getElementById('fileInput');
if (fileInput) {
fileInput.addEventListener('change', this.handleFileSelect.bind(this));
}
// Copy to clipboard functionality
document.addEventListener('click', (e) => {
if (e.target.matches('[data-copy]') || e.target.closest('[data-copy]')) {
const target = e.target.matches('[data-copy]') ? e.target : e.target.closest('[data-copy]');
this.copyToClipboard(target.dataset.copy);
}
});
}
initializeTooltips() {
// Initialize Bootstrap tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// File upload handling
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
this.displayFileContent(e.target.result, file.name);
};
reader.readAsText(file);
}
displayFileContent(content, filename) {
// This will be overridden in specific pages
console.log('File loaded:', filename, content);
}
// API calls
async apiCall(endpoint, options = {}) {
const url = `${this.apiUrl}${endpoint}`;
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
};
const finalOptions = { ...defaultOptions, ...options };
try {
const response = await fetch(url, finalOptions);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
async validateUnitFile(content, filename = null) {
return await this.apiCall('/validate', {
method: 'POST',
body: JSON.stringify({
content: content,
filename: filename
})
});
}
async uploadUnitFile(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${this.apiUrl}/upload`, {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Upload failed: ${response.statusText}`);
}
return await response.json();
}
async downloadUnitFile(content, filename) {
return await this.apiCall('/download', {
method: 'POST',
body: JSON.stringify({
content: content,
filename: filename
})
});
}
async getTemplates() {
return await this.apiCall('/templates');
}
async getTemplate(name) {
return await this.apiCall(`/templates/${name}`);
}
async generateFromTemplate(templateName, parameters, filename = null) {
return await this.apiCall('/generate', {
method: 'POST',
body: JSON.stringify({
template_name: templateName,
parameters: parameters,
filename: filename
})
});
}
// Utility functions
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copied to clipboard!', 'success');
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
this.showToast('Copied to clipboard!', 'success');
} catch (err) {
this.showToast('Failed to copy to clipboard', 'error');
}
document.body.removeChild(textArea);
}
}
showToast(message, type = 'info', duration = 3000) {
// Create toast container if it doesn't exist
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
// Create toast element
const toastId = `toast-${Date.now()}`;
const toastHtml = `
<div id="${toastId}" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<i class="fas fa-${this.getToastIcon(type)} text-${this.getToastColor(type)} me-2"></i>
<strong class="me-auto">UnitForge</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
`;
toastContainer.insertAdjacentHTML('beforeend', toastHtml);
// Initialize and show toast
const toastElement = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: duration
});
toast.show();
// Remove toast element after it's hidden
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
getToastIcon(type) {
const icons = {
success: 'check-circle',
error: 'exclamation-circle',
warning: 'exclamation-triangle',
info: 'info-circle'
};
return icons[type] || 'info-circle';
}
getToastColor(type) {
const colors = {
success: 'success',
error: 'danger',
warning: 'warning',
info: 'info'
};
return colors[type] || 'info';
}
formatValidationResults(validation) {
if (!validation) return '';
let html = '';
if (validation.valid) {
html += `
<div class="validation-success">
<i class="fas fa-check-circle me-2"></i>
Unit file is valid!
</div>
`;
}
if (validation.errors && validation.errors.length > 0) {
html += '<div class="mb-3">';
html += `<h6 class="text-danger"><i class="fas fa-exclamation-circle me-2"></i>Errors (${validation.errors.length})</h6>`;
validation.errors.forEach(error => {
const location = error.section + (error.key ? `.${error.key}` : '');
html += `
<div class="validation-error">
<strong>[${location}]</strong> ${error.message}
</div>
`;
});
html += '</div>';
}
if (validation.warnings && validation.warnings.length > 0) {
html += '<div class="mb-3">';
html += `<h6 class="text-warning"><i class="fas fa-exclamation-triangle me-2"></i>Warnings (${validation.warnings.length})</h6>`;
validation.warnings.forEach(warning => {
const location = warning.section + (warning.key ? `.${warning.key}` : '');
html += `
<div class="validation-warning">
<strong>[${location}]</strong> ${warning.message}
</div>
`;
});
html += '</div>';
}
return html;
}
downloadTextFile(content, filename) {
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
showLoading(element, message = 'Loading...') {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (element) {
element.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">${message}</span>
</div>
<p class="mt-3 text-muted">${message}</p>
</div>
`;
}
}
hideLoading(element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
if (element) {
element.innerHTML = '';
}
}
}
// Global functions for HTML onclick handlers
function showUploadModal() {
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
modal.show();
}
function showCliModal() {
const modal = new bootstrap.Modal(document.getElementById('cliModal'));
modal.show();
}
async function validateFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
unitforge.showToast('Please select a file first', 'warning');
return;
}
try {
const result = await unitforge.uploadUnitFile(file);
// Display results
const resultsDiv = document.getElementById('uploadResults');
const outputDiv = document.getElementById('validationOutput');
if (resultsDiv && outputDiv) {
outputDiv.innerHTML = unitforge.formatValidationResults(result.validation);
resultsDiv.classList.remove('d-none');
}
if (result.validation.valid) {
unitforge.showToast('File validation completed successfully!', 'success');
} else {
unitforge.showToast(`Validation found ${result.validation.errors.length} error(s)`, 'warning');
}
} catch (error) {
unitforge.showToast(`Validation failed: ${error.message}`, 'error');
}
}
// Initialize the application
const unitforge = new UnitForge();
// Export for use in other modules
window.UnitForge = UnitForge;
window.unitforge = unitforge;