535 lines
16 KiB
JavaScript
535 lines
16 KiB
JavaScript
/**
|
|
* Main JavaScript Application
|
|
* Professional Resume Builder - Laravel Application
|
|
*
|
|
* @author David Valera Melendez <david@valera-melendez.de>
|
|
* @created 2025-08-08
|
|
* @location Made in Germany 🇩🇪
|
|
*/
|
|
|
|
// Import Bootstrap JavaScript
|
|
import 'bootstrap';
|
|
|
|
// Import Axios for HTTP requests
|
|
import axios from 'axios';
|
|
|
|
// Configure Axios defaults
|
|
window.axios = axios;
|
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|
|
|
// CSRF Token setup
|
|
const token = document.head.querySelector('meta[name="csrf-token"]');
|
|
if (token) {
|
|
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
|
|
} else {
|
|
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
|
|
}
|
|
|
|
/**
|
|
* Application Class - Main Application Logic
|
|
*/
|
|
class ResumeBuilderApp {
|
|
constructor() {
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize the application
|
|
*/
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.setupFormValidation();
|
|
this.setupTooltips();
|
|
this.setupAnimations();
|
|
this.setupAccessibility();
|
|
this.setupProgressBars();
|
|
// Professional Resume Builder initialized - Made in Germany 🇩🇪
|
|
}
|
|
|
|
/**
|
|
* Setup global event listeners
|
|
*/
|
|
setupEventListeners() {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
this.handlePageLoad();
|
|
});
|
|
|
|
// Handle form submissions with loading states
|
|
document.addEventListener('submit', (e) => {
|
|
if (e.target.tagName === 'FORM') {
|
|
this.handleFormSubmit(e.target);
|
|
}
|
|
});
|
|
|
|
// Handle dynamic content loading
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target.matches('[data-action]')) {
|
|
this.handleDynamicAction(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle page load events
|
|
*/
|
|
handlePageLoad() {
|
|
// Animate elements on page load
|
|
this.animateOnLoad();
|
|
|
|
// Focus management
|
|
this.setupFocusManagement();
|
|
|
|
// Auto-dismiss alerts
|
|
this.setupAlertDismissal();
|
|
}
|
|
|
|
/**
|
|
* Setup form validation
|
|
*/
|
|
setupFormValidation() {
|
|
const forms = document.querySelectorAll('form[data-validate]');
|
|
|
|
forms.forEach(form => {
|
|
const inputs = form.querySelectorAll('input, select, textarea');
|
|
|
|
inputs.forEach(input => {
|
|
// Real-time validation
|
|
input.addEventListener('input', () => {
|
|
this.validateField(input);
|
|
});
|
|
|
|
input.addEventListener('blur', () => {
|
|
this.validateField(input);
|
|
});
|
|
});
|
|
|
|
// Form submission validation
|
|
form.addEventListener('submit', (e) => {
|
|
if (!this.validateForm(form)) {
|
|
e.preventDefault();
|
|
this.showValidationErrors(form);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validate individual field
|
|
*/
|
|
validateField(field) {
|
|
const isValid = field.checkValidity();
|
|
const hasValue = field.value.trim().length > 0;
|
|
|
|
field.classList.remove('is-valid', 'is-invalid');
|
|
|
|
if (hasValue) {
|
|
field.classList.add(isValid ? 'is-valid' : 'is-invalid');
|
|
}
|
|
|
|
// Custom validation rules
|
|
if (field.type === 'password' && field.name === 'password') {
|
|
this.validatePassword(field);
|
|
}
|
|
|
|
if (field.type === 'password' && field.name === 'password_confirmation') {
|
|
this.validatePasswordConfirmation(field);
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/**
|
|
* Validate password strength
|
|
*/
|
|
validatePassword(passwordField) {
|
|
const password = passwordField.value;
|
|
const minLength = 8;
|
|
const hasUpperCase = /[A-Z]/.test(password);
|
|
const hasLowerCase = /[a-z]/.test(password);
|
|
const hasNumbers = /\d/.test(password);
|
|
const hasSpecialChar = /[!@#$%^&*]/.test(password);
|
|
|
|
const isStrong = password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers;
|
|
|
|
passwordField.classList.remove('is-valid', 'is-invalid');
|
|
if (password.length > 0) {
|
|
passwordField.classList.add(isStrong ? 'is-valid' : 'is-invalid');
|
|
}
|
|
|
|
return isStrong;
|
|
}
|
|
|
|
/**
|
|
* Validate password confirmation
|
|
*/
|
|
validatePasswordConfirmation(confirmField) {
|
|
const password = document.querySelector('input[name="password"]')?.value || '';
|
|
const confirmation = confirmField.value;
|
|
|
|
const matches = password === confirmation && confirmation.length > 0;
|
|
|
|
confirmField.classList.remove('is-valid', 'is-invalid');
|
|
if (confirmation.length > 0) {
|
|
confirmField.classList.add(matches ? 'is-valid' : 'is-invalid');
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Validate entire form
|
|
*/
|
|
validateForm(form) {
|
|
const inputs = form.querySelectorAll('input[required], select[required], textarea[required]');
|
|
let isValid = true;
|
|
|
|
inputs.forEach(input => {
|
|
if (!this.validateField(input)) {
|
|
isValid = false;
|
|
}
|
|
});
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/**
|
|
* Show validation errors
|
|
*/
|
|
showValidationErrors(form) {
|
|
const firstInvalidField = form.querySelector('.is-invalid');
|
|
if (firstInvalidField) {
|
|
firstInvalidField.focus();
|
|
firstInvalidField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle form submission with loading states
|
|
*/
|
|
handleFormSubmit(form) {
|
|
const submitButton = form.querySelector('button[type="submit"]');
|
|
if (submitButton) {
|
|
const originalContent = submitButton.innerHTML;
|
|
const loadingText = submitButton.dataset.loading || 'Processing...';
|
|
|
|
submitButton.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${loadingText}`;
|
|
submitButton.disabled = true;
|
|
|
|
// Re-enable if form doesn't redirect (error case)
|
|
setTimeout(() => {
|
|
if (document.contains(submitButton)) {
|
|
submitButton.innerHTML = originalContent;
|
|
submitButton.disabled = false;
|
|
}
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup tooltips
|
|
*/
|
|
setupTooltips() {
|
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup animations
|
|
*/
|
|
setupAnimations() {
|
|
// Intersection Observer for scroll animations
|
|
if ('IntersectionObserver' in window) {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('animate-fade-in-up');
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-animate]').forEach(el => {
|
|
observer.observe(el);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animate elements on page load
|
|
*/
|
|
animateOnLoad() {
|
|
// Stagger animation for cards
|
|
const cards = document.querySelectorAll('.card');
|
|
cards.forEach((card, index) => {
|
|
setTimeout(() => {
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'translateY(20px)';
|
|
card.style.transition = 'all 0.5s ease';
|
|
|
|
setTimeout(() => {
|
|
card.style.opacity = '1';
|
|
card.style.transform = 'translateY(0)';
|
|
}, 100 * index);
|
|
}, 0);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup progress bars with animation
|
|
*/
|
|
setupProgressBars() {
|
|
const progressBars = document.querySelectorAll('.progress-bar');
|
|
|
|
const animateProgress = (entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const progressBar = entry.target;
|
|
const targetWidth = progressBar.style.width;
|
|
|
|
progressBar.style.width = '0%';
|
|
|
|
setTimeout(() => {
|
|
progressBar.style.transition = 'width 1s ease-in-out';
|
|
progressBar.style.width = targetWidth;
|
|
}, 200);
|
|
|
|
observer.unobserve(progressBar);
|
|
}
|
|
});
|
|
};
|
|
|
|
if ('IntersectionObserver' in window) {
|
|
const observer = new IntersectionObserver(animateProgress);
|
|
progressBars.forEach(bar => observer.observe(bar));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup accessibility features
|
|
*/
|
|
setupAccessibility() {
|
|
// Keyboard navigation for custom elements
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
if (e.target.matches('[role="button"]:not(button):not(input)')) {
|
|
e.preventDefault();
|
|
e.target.click();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Focus management for modals
|
|
document.addEventListener('shown.bs.modal', (e) => {
|
|
const modal = e.target;
|
|
const focusableElement = modal.querySelector('input, button, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
if (focusableElement) {
|
|
focusableElement.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup focus management
|
|
*/
|
|
setupFocusManagement() {
|
|
// Auto-focus first input in forms
|
|
const firstInput = document.querySelector('input:not([type="hidden"]):not([readonly])');
|
|
if (firstInput && !firstInput.value) {
|
|
setTimeout(() => firstInput.focus(), 100);
|
|
}
|
|
|
|
// Skip links for accessibility
|
|
const skipLinks = document.querySelectorAll('.skip-link');
|
|
skipLinks.forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const target = document.querySelector(link.getAttribute('href'));
|
|
if (target) {
|
|
target.focus();
|
|
target.scrollIntoView();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup alert auto-dismissal
|
|
*/
|
|
setupAlertDismissal() {
|
|
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)');
|
|
|
|
alerts.forEach(alert => {
|
|
// Auto-dismiss success alerts
|
|
if (alert.classList.contains('alert-success')) {
|
|
setTimeout(() => {
|
|
this.dismissAlert(alert);
|
|
}, 5000);
|
|
}
|
|
|
|
// Auto-dismiss info alerts
|
|
if (alert.classList.contains('alert-info')) {
|
|
setTimeout(() => {
|
|
this.dismissAlert(alert);
|
|
}, 8000);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dismiss alert with animation
|
|
*/
|
|
dismissAlert(alert) {
|
|
alert.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
|
alert.style.opacity = '0';
|
|
alert.style.transform = 'translateY(-20px)';
|
|
|
|
setTimeout(() => {
|
|
if (alert.parentNode) {
|
|
alert.parentNode.removeChild(alert);
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
/**
|
|
* Handle dynamic actions
|
|
*/
|
|
handleDynamicAction(e) {
|
|
e.preventDefault();
|
|
const action = e.target.dataset.action;
|
|
|
|
switch (action) {
|
|
case 'confirm-delete':
|
|
this.handleConfirmDelete(e.target);
|
|
break;
|
|
case 'copy-link':
|
|
this.handleCopyLink(e.target);
|
|
break;
|
|
case 'toggle-visibility':
|
|
this.handleToggleVisibility(e.target);
|
|
break;
|
|
default:
|
|
console.warn('Unknown action:', action);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle confirm delete action
|
|
*/
|
|
handleConfirmDelete(element) {
|
|
const message = element.dataset.message || 'Are you sure you want to delete this item?';
|
|
const form = element.closest('form') || document.querySelector(element.dataset.form);
|
|
|
|
if (confirm(message) && form) {
|
|
form.submit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle copy link action
|
|
*/
|
|
async handleCopyLink(element) {
|
|
const url = element.dataset.url || window.location.href;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(url);
|
|
this.showToast('Link copied to clipboard!', 'success');
|
|
} catch (err) {
|
|
console.error('Failed to copy link:', err);
|
|
this.showToast('Failed to copy link', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle toggle visibility action
|
|
*/
|
|
handleToggleVisibility(element) {
|
|
const target = document.querySelector(element.dataset.target);
|
|
const type = element.type;
|
|
|
|
if (target && type === 'password') {
|
|
const isPassword = target.type === 'password';
|
|
target.type = isPassword ? 'text' : 'password';
|
|
|
|
const icon = element.querySelector('i');
|
|
if (icon) {
|
|
icon.classList.toggle('bi-eye');
|
|
icon.classList.toggle('bi-eye-slash');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show toast notification
|
|
*/
|
|
showToast(message, type = 'info') {
|
|
const toastContainer = this.getOrCreateToastContainer();
|
|
const toast = this.createToast(message, type);
|
|
|
|
toastContainer.appendChild(toast);
|
|
|
|
const bsToast = new bootstrap.Toast(toast);
|
|
bsToast.show();
|
|
|
|
toast.addEventListener('hidden.bs.toast', () => {
|
|
toast.remove();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get or create toast container
|
|
*/
|
|
getOrCreateToastContainer() {
|
|
let container = document.querySelector('.toast-container');
|
|
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
container.style.zIndex = '1055';
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Create toast element
|
|
*/
|
|
createToast(message, type) {
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast';
|
|
toast.setAttribute('role', 'alert');
|
|
toast.setAttribute('aria-live', 'assertive');
|
|
toast.setAttribute('aria-atomic', 'true');
|
|
|
|
const iconMap = {
|
|
success: 'bi-check-circle-fill text-success',
|
|
error: 'bi-exclamation-circle-fill text-danger',
|
|
warning: 'bi-exclamation-triangle-fill text-warning',
|
|
info: 'bi-info-circle-fill text-info'
|
|
};
|
|
|
|
const icon = iconMap[type] || iconMap.info;
|
|
|
|
toast.innerHTML = `
|
|
<div class="toast-header">
|
|
<i class="bi ${icon} me-2"></i>
|
|
<strong class="me-auto">Resume Builder</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body">
|
|
${message}
|
|
</div>
|
|
`;
|
|
|
|
return toast;
|
|
}
|
|
}
|
|
|
|
// Initialize the application
|
|
const app = new ResumeBuilderApp();
|
|
|
|
// Export for global access
|
|
window.ResumeBuilderApp = app;
|