init commit
This commit is contained in:
534
resources/js/app.js
Normal file
534
resources/js/app.js
Normal file
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user