init commit

This commit is contained in:
David Melendez
2026-01-14 22:38:44 +01:00
parent 4e0c415f0b
commit e25d53d054
124 changed files with 21653 additions and 1 deletions

534
resources/js/app.js Normal file
View 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;