# 🎨 **CAPITOLO 8 - FRONTEND E UX** *Interfaccia Utente, JavaScript e User Experience* **Versione:** 1.0 **Data:** 17 Luglio 2025 **Ambiente:** Laravel 11 + Bootstrap 5 + Alpine.js + Vite --- ## πŸ“‹ **INDICE CAPITOLO** 1. [**Architettura Frontend**](#1-architettura-frontend) 2. [**Stack Tecnologico**](#2-stack-tecnologico) 3. [**Componenti UI**](#3-componenti-ui) 4. [**JavaScript e InterattivitΓ **](#4-javascript-e-interattivitΓ ) 5. [**Responsive Design**](#5-responsive-design) 6. [**Performance e Ottimizzazione**](#6-performance-e-ottimizzazione) 7. [**Gestione Stati**](#7-gestione-stati) 8. [**Validazione Frontend**](#8-validazione-frontend) 9. [**Esempi Pratici**](#9-esempi-pratici) 10. [**Best Practices**](#10-best-practices) --- ## 1. **ARCHITETTURA FRONTEND** ### 1.1 **Struttura Generale** ``` resources/ β”œβ”€β”€ js/ β”‚ β”œβ”€β”€ app.js # Entry point principale β”‚ β”œβ”€β”€ bootstrap.js # Configurazione librerie β”‚ β”œβ”€β”€ components/ # Componenti riutilizzabili β”‚ β”‚ β”œβ”€β”€ forms/ # Componenti form β”‚ β”‚ β”œβ”€β”€ tables/ # Componenti tabelle β”‚ β”‚ β”œβ”€β”€ modals/ # Componenti modal β”‚ β”‚ └── charts/ # Componenti grafici β”‚ β”œβ”€β”€ modules/ # Moduli specifici β”‚ β”‚ β”œβ”€β”€ auth/ # Autenticazione β”‚ β”‚ β”œβ”€β”€ dashboard/ # Dashboard β”‚ β”‚ β”œβ”€β”€ condominiums/ # Gestione condomini β”‚ β”‚ └── accounting/ # ContabilitΓ  β”‚ └── utils/ # Utility e helper β”œβ”€β”€ css/ β”‚ β”œβ”€β”€ app.css # Stili principali β”‚ β”œβ”€β”€ components/ # Stili componenti β”‚ └── themes/ # Temi personalizzati └── views/ β”œβ”€β”€ layouts/ # Layout principali β”œβ”€β”€ components/ # Blade components └── pages/ # Pagine specifiche ``` ### 1.2 **Principi Architetturali** #### **ModularitΓ ** ```javascript // Esempio struttura modulare const NetGescon = { modules: { auth: AuthModule, dashboard: DashboardModule, condominiums: CondominiumsModule, accounting: AccountingModule }, init() { // Inizializzazione globale this.initializeModules(); this.setupEventListeners(); this.configureAjax(); }, initializeModules() { Object.keys(this.modules).forEach(key => { if (this.modules[key].init) { this.modules[key].init(); } }); } }; ``` #### **Component-Based** ```javascript // Componente base riutilizzabile class BaseComponent { constructor(selector, options = {}) { this.element = document.querySelector(selector); this.options = { ...this.defaults, ...options }; this.init(); } init() { this.bindEvents(); this.render(); } bindEvents() { // Override in sottoclassi } render() { // Override in sottoclassi } destroy() { // Cleanup this.element.removeEventListener(); } } ``` --- ## 2. **STACK TECNOLOGICO** ### 2.1 **Frontend Stack** #### **Framework CSS** ```json // package.json { "devDependencies": { "bootstrap": "^5.3.0", "sass": "^1.77.8", "@fortawesome/fontawesome-free": "^6.5.0" } } ``` #### **JavaScript Libraries** ```json { "dependencies": { "alpinejs": "^3.13.0", "axios": "^1.6.0", "chart.js": "^4.4.0", "datatables.net": "^1.13.0", "sweetalert2": "^11.10.0" } } ``` ### 2.2 **Build Tools (Vite)** #### **Configurazione Vite** ```javascript // vite.config.js import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel({ input: [ 'resources/css/app.css', 'resources/js/app.js', ], refresh: true, }), ], resolve: { alias: { '@': '/resources/js', '~': '/resources/css', }, }, build: { rollupOptions: { output: { manualChunks: { vendor: ['bootstrap', 'axios', 'alpinejs'], charts: ['chart.js'], tables: ['datatables.net'], }, }, }, }, }); ``` ### 2.3 **Asset Management** #### **CSS Structure** ```scss // resources/css/app.scss @import 'bootstrap'; @import '@fortawesome/fontawesome-free/css/all.css'; // Custom variables @import 'variables'; // Core styles @import 'components/base'; @import 'components/forms'; @import 'components/tables'; @import 'components/modals'; @import 'components/dashboard'; // Theme styles @import 'themes/default'; @import 'themes/dark'; ``` --- ## 3. **COMPONENTI UI** ### 3.1 **Componenti Base** #### **Button Component** ```html @php $classes = [ 'btn', 'btn-' . ($variant ?? 'primary'), $size ? 'btn-' . $size : '', $disabled ? 'disabled' : '', $loading ? 'btn-loading' : '', $attributes->get('class') ]; @endphp ``` #### **Form Input Component** ```html
@if($label) @endif @error($name)
{{ $message }}
@enderror @if($help)
{{ $help }}
@endif
``` ### 3.2 **Componenti Avanzati** #### **DataTable Component** ```html
@foreach($columns as $column) @endforeach @if($actions) @endif {{ $slot }}
{{ $column['title'] }}Azioni
``` #### **Modal Component** ```html ``` --- ## 4. **JAVASCRIPT E INTERATTIVITΓ€** ### 4.1 **Gestione AJAX** #### **Configurazione Axios** ```javascript // resources/js/bootstrap.js import axios from 'axios'; // Configurazione globale window.axios = axios; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // CSRF Token let token = document.head.querySelector('meta[name="csrf-token"]'); if (token) { window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; } // Interceptors axios.interceptors.request.use(request => { // Mostra loading showLoading(); return request; }); axios.interceptors.response.use( response => { hideLoading(); return response; }, error => { hideLoading(); handleError(error); return Promise.reject(error); } ); ``` #### **Form Handler** ```javascript // resources/js/components/forms/FormHandler.js class FormHandler { constructor(formSelector, options = {}) { this.form = document.querySelector(formSelector); this.options = { showToast: true, resetOnSuccess: true, ...options }; this.init(); } init() { this.form.addEventListener('submit', this.handleSubmit.bind(this)); } async handleSubmit(e) { e.preventDefault(); const formData = new FormData(this.form); const url = this.form.action; const method = this.form.method; try { const response = await axios({ method, url, data: formData, headers: { 'Content-Type': 'multipart/form-data' } }); this.handleSuccess(response.data); } catch (error) { this.handleError(error); } } handleSuccess(data) { if (this.options.showToast) { Toast.success(data.message || 'Operazione completata'); } if (this.options.resetOnSuccess) { this.form.reset(); } if (this.options.onSuccess) { this.options.onSuccess(data); } } handleError(error) { if (error.response?.status === 422) { this.showValidationErrors(error.response.data.errors); } else { Toast.error(error.response?.data?.message || 'Errore durante l\'operazione'); } } showValidationErrors(errors) { Object.keys(errors).forEach(field => { const input = this.form.querySelector(`[name="${field}"]`); if (input) { input.classList.add('is-invalid'); let feedback = input.parentNode.querySelector('.invalid-feedback'); if (!feedback) { feedback = document.createElement('div'); feedback.className = 'invalid-feedback'; input.parentNode.appendChild(feedback); } feedback.textContent = errors[field][0]; } }); } } ``` ### 4.2 **Alpine.js Integration** #### **Dashboard Component** ```javascript // resources/js/components/dashboard/DashboardStats.js document.addEventListener('alpine:init', () => { Alpine.data('dashboardStats', () => ({ stats: {}, loading: true, async init() { await this.loadStats(); this.setupRealTimeUpdates(); }, async loadStats() { this.loading = true; try { const response = await axios.get('/api/dashboard/stats'); this.stats = response.data; } catch (error) { console.error('Errore caricamento statistiche:', error); } finally { this.loading = false; } }, setupRealTimeUpdates() { // Aggiorna ogni 30 secondi setInterval(() => { this.loadStats(); }, 30000); }, formatCurrency(amount) { return new Intl.NumberFormat('it-IT', { style: 'currency', currency: 'EUR' }).format(amount); } })); }); ``` #### **Template Usage** ```html
Condomini Attivi

Fatturato Mensile

Caricamento...
``` --- ## 5. **RESPONSIVE DESIGN** ### 5.1 **Breakpoints Strategy** #### **Custom Breakpoints** ```scss // resources/css/variables.scss $grid-breakpoints: ( xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px, xxl: 1400px ); // Mixins personalizzati @mixin mobile-only { @media (max-width: 767px) { @content; } } @mixin tablet-only { @media (min-width: 768px) and (max-width: 991px) { @content; } } @mixin desktop-only { @media (min-width: 992px) { @content; } } ``` ### 5.2 **Responsive Components** #### **Responsive Table** ```html
@foreach($columns as $column) @endforeach {{ $slot }}
{{ $column['title'] }}
``` #### **Mobile Navigation** ```html
``` --- ## 6. **PERFORMANCE E OTTIMIZZAZIONE** ### 6.1 **Lazy Loading** #### **Image Lazy Loading** ```javascript // resources/js/utils/LazyLoader.js class LazyLoader { constructor() { this.images = document.querySelectorAll('img[data-src]'); this.imageObserver = new IntersectionObserver(this.handleIntersection.bind(this)); this.init(); } init() { this.images.forEach(img => { this.imageObserver.observe(img); }); } handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); this.imageObserver.unobserve(img); } }); } } // Inizializzazione document.addEventListener('DOMContentLoaded', () => { new LazyLoader(); }); ``` #### **Component Lazy Loading** ```javascript // resources/js/utils/ComponentLoader.js class ComponentLoader { static async loadComponent(componentName) { const module = await import(`../components/${componentName}.js`); return module.default; } static async loadOnDemand(selector, componentName) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { const Component = await this.loadComponent(componentName); elements.forEach(el => { new Component(el); }); } } } ``` ### 6.2 **Bundle Optimization** #### **Code Splitting** ```javascript // resources/js/app.js import './bootstrap'; // Core components caricati immediatamente import './components/base/Navigation'; import './components/base/Toast'; // Lazy loading per componenti specifici document.addEventListener('DOMContentLoaded', async () => { // Carica componenti solo se necessari if (document.querySelector('[data-component="chart"]')) { const { default: ChartComponent } = await import('./components/charts/ChartComponent'); new ChartComponent(); } if (document.querySelector('[data-component="datatable"]')) { const { default: DataTableComponent } = await import('./components/tables/DataTableComponent'); new DataTableComponent(); } }); ``` ### 6.3 **Caching Strategy** #### **Service Worker** ```javascript // public/sw.js const CACHE_NAME = 'netgescon-v1'; const urlsToCache = [ '/', '/css/app.css', '/js/app.js', '/images/logo.png' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) ); }); self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { if (response) { return response; } return fetch(event.request); }) ); }); ``` --- ## 7. **GESTIONE STATI** ### 7.1 **State Management** #### **Simple State Manager** ```javascript // resources/js/utils/StateManager.js class StateManager { constructor() { this.state = {}; this.subscribers = {}; } setState(key, value) { const oldValue = this.state[key]; this.state[key] = value; if (this.subscribers[key]) { this.subscribers[key].forEach(callback => { callback(value, oldValue); }); } } getState(key) { return this.state[key]; } subscribe(key, callback) { if (!this.subscribers[key]) { this.subscribers[key] = []; } this.subscribers[key].push(callback); } unsubscribe(key, callback) { if (this.subscribers[key]) { this.subscribers[key] = this.subscribers[key].filter(cb => cb !== callback); } } } // Istanza globale window.StateManager = new StateManager(); ``` #### **Usage Example** ```javascript // Componente che usa lo state class UserProfile { constructor() { this.init(); } init() { // Sottoscrizione ai cambiamenti StateManager.subscribe('user', this.updateProfile.bind(this)); // Caricamento dati utente this.loadUserData(); } async loadUserData() { try { const response = await axios.get('/api/user'); StateManager.setState('user', response.data); } catch (error) { console.error('Errore caricamento utente:', error); } } updateProfile(userData) { document.querySelector('#user-name').textContent = userData.name; document.querySelector('#user-email').textContent = userData.email; } } ``` ### 7.2 **Form State** #### **Form State Manager** ```javascript // resources/js/components/forms/FormState.js class FormState { constructor(formElement) { this.form = formElement; this.initialData = this.getFormData(); this.currentData = { ...this.initialData }; this.isDirty = false; this.init(); } init() { this.form.addEventListener('input', this.handleInput.bind(this)); this.form.addEventListener('change', this.handleChange.bind(this)); // Avviso prima di lasciare la pagina se ci sono modifiche window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this)); } getFormData() { const formData = new FormData(this.form); const data = {}; for (let [key, value] of formData.entries()) { data[key] = value; } return data; } handleInput(e) { this.currentData[e.target.name] = e.target.value; this.checkDirty(); } handleChange(e) { this.currentData[e.target.name] = e.target.value; this.checkDirty(); } checkDirty() { this.isDirty = JSON.stringify(this.currentData) !== JSON.stringify(this.initialData); this.updateUI(); } updateUI() { const saveButton = this.form.querySelector('[type="submit"]'); if (saveButton) { saveButton.disabled = !this.isDirty; } } handleBeforeUnload(e) { if (this.isDirty) { e.preventDefault(); e.returnValue = ''; } } reset() { this.currentData = { ...this.initialData }; this.isDirty = false; this.updateUI(); } } ``` --- ## 8. **VALIDAZIONE FRONTEND** ### 8.1 **Validation Rules** #### **Validator Class** ```javascript // resources/js/utils/Validator.js class Validator { constructor() { this.rules = { required: (value) => value !== null && value !== undefined && value !== '', email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), minLength: (value, min) => value.length >= min, maxLength: (value, max) => value.length <= max, numeric: (value) => /^\d+$/.test(value), alpha: (value) => /^[a-zA-Z]+$/.test(value), alphanumeric: (value) => /^[a-zA-Z0-9]+$/.test(value), phone: (value) => /^[+]?[\d\s\-\(\)]{8,}$/.test(value), fiscal_code: (value) => /^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$/i.test(value), vat_number: (value) => /^[0-9]{11}$/.test(value) }; this.messages = { required: 'Questo campo Γ¨ obbligatorio', email: 'Inserire un indirizzo email valido', minLength: 'Minimo {min} caratteri', maxLength: 'Massimo {max} caratteri', numeric: 'Inserire solo numeri', alpha: 'Inserire solo lettere', alphanumeric: 'Inserire solo lettere e numeri', phone: 'Inserire un numero di telefono valido', fiscal_code: 'Inserire un codice fiscale valido', vat_number: 'Inserire una partita IVA valida' }; } validate(value, rules) { const errors = []; rules.forEach(rule => { const [ruleName, ...params] = rule.split(':'); const ruleFunction = this.rules[ruleName]; if (ruleFunction && !ruleFunction(value, ...params)) { let message = this.messages[ruleName]; // Sostituisci parametri nel messaggio params.forEach((param, index) => { message = message.replace(`{${Object.keys(this.rules)[index]}}`, param); }); errors.push(message); } }); return errors; } validateForm(formElement) { const errors = {}; const inputs = formElement.querySelectorAll('[data-validate]'); inputs.forEach(input => { const rules = input.dataset.validate.split('|'); const fieldErrors = this.validate(input.value, rules); if (fieldErrors.length > 0) { errors[input.name] = fieldErrors; } }); return errors; } } ``` ### 8.2 **Real-time Validation** #### **Live Validator** ```javascript // resources/js/components/forms/LiveValidator.js class LiveValidator { constructor(formElement) { this.form = formElement; this.validator = new Validator(); this.debounceTime = 300; this.init(); } init() { const inputs = this.form.querySelectorAll('[data-validate]'); inputs.forEach(input => { input.addEventListener('input', this.debounce( this.validateField.bind(this, input), this.debounceTime )); input.addEventListener('blur', this.validateField.bind(this, input)); }); this.form.addEventListener('submit', this.validateForm.bind(this)); } validateField(input) { const rules = input.dataset.validate.split('|'); const errors = this.validator.validate(input.value, rules); this.showFieldErrors(input, errors); } validateForm(e) { const errors = this.validator.validateForm(this.form); if (Object.keys(errors).length > 0) { e.preventDefault(); this.showFormErrors(errors); } } showFieldErrors(input, errors) { const errorContainer = input.parentNode.querySelector('.validation-errors'); if (errors.length > 0) { input.classList.add('is-invalid'); if (errorContainer) { errorContainer.innerHTML = errors.map(error => `
${error}
` ).join(''); } } else { input.classList.remove('is-invalid'); input.classList.add('is-valid'); if (errorContainer) { errorContainer.innerHTML = ''; } } } showFormErrors(errors) { Object.keys(errors).forEach(fieldName => { const input = this.form.querySelector(`[name="${fieldName}"]`); if (input) { this.showFieldErrors(input, errors[fieldName]); } }); } debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } } ``` --- ## 9. **ESEMPI PRATICI** ### 9.1 **Condominium Form** #### **Complete Form Implementation** ```html
@csrf
Salva Condominio
``` ### 9.2 **Dashboard Charts** #### **Chart Component** ```html
Fatturato Mensile
Distribuzione Condomini
``` --- ## 10. **BEST PRACTICES** ### 10.1 **Code Organization** #### **Module Pattern** ```javascript // resources/js/modules/condominium/CondominiumModule.js const CondominiumModule = (() => { // Private variables let initialized = false; let currentCondominium = null; // Private methods const loadCondominium = async (id) => { try { const response = await axios.get(`/api/condominiums/${id}`); currentCondominium = response.data; return currentCondominium; } catch (error) { console.error('Error loading condominium:', error); throw error; } }; const updateUI = (condominium) => { document.querySelector('#condominium-name').textContent = condominium.name; document.querySelector('#condominium-address').textContent = condominium.address; }; // Public API return { init() { if (initialized) return; this.bindEvents(); initialized = true; }, bindEvents() { document.addEventListener('click', (e) => { if (e.target.matches('[data-action="load-condominium"]')) { const id = e.target.dataset.condominiumId; this.loadAndDisplay(id); } }); }, async loadAndDisplay(id) { try { const condominium = await loadCondominium(id); updateUI(condominium); } catch (error) { Toast.error('Errore caricamento condominio'); } }, getCurrentCondominium() { return currentCondominium; } }; })(); ``` ### 10.2 **Error Handling** #### **Global Error Handler** ```javascript // resources/js/utils/ErrorHandler.js class ErrorHandler { static handle(error, context = '') { console.error(`[${context}] Error:`, error); if (error.response) { // Server response error this.handleServerError(error.response); } else if (error.request) { // Network error this.handleNetworkError(); } else { // Generic error this.handleGenericError(error.message); } } static handleServerError(response) { switch (response.status) { case 401: Toast.error('Sessione scaduta. Effettuare nuovamente il login.'); setTimeout(() => { window.location.href = '/login'; }, 2000); break; case 403: Toast.error('Non hai i permessi per eseguire questa operazione.'); break; case 404: Toast.error('Risorsa non trovata.'); break; case 422: // Validation errors - handled by form components break; case 500: Toast.error('Errore interno del server. Riprova piΓΉ tardi.'); break; default: Toast.error(response.data.message || 'Errore sconosciuto'); } } static handleNetworkError() { Toast.error('Errore di connessione. Controlla la tua connessione internet.'); } static handleGenericError(message) { Toast.error(message || 'Si Γ¨ verificato un errore imprevisto.'); } } // Setup global error handling window.addEventListener('error', (e) => { ErrorHandler.handle(e.error, 'Global'); }); window.addEventListener('unhandledrejection', (e) => { ErrorHandler.handle(e.reason, 'Promise'); }); ``` ### 10.3 **Performance Tips** #### **Optimization Checklist** ```javascript // resources/js/utils/Performance.js class PerformanceOptimizer { static init() { this.setupImageOptimization(); this.setupScrollOptimization(); this.setupResizeOptimization(); this.measurePerformance(); } static setupImageOptimization() { // Lazy loading images const images = document.querySelectorAll('img[data-src]'); const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.remove('lazy'); imageObserver.unobserve(img); } }); }); images.forEach(img => imageObserver.observe(img)); } static setupScrollOptimization() { let ticking = false; const handleScroll = () => { if (!ticking) { requestAnimationFrame(() => { // Scroll handling logic ticking = false; }); ticking = true; } }; window.addEventListener('scroll', handleScroll, { passive: true }); } static setupResizeOptimization() { let resizeTimeout; const handleResize = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { // Resize handling logic this.recalculateLayout(); }, 250); }; window.addEventListener('resize', handleResize); } static measurePerformance() { // Performance monitoring new PerformanceObserver((list) => { list.getEntries().forEach(entry => { if (entry.entryType === 'navigation') { console.log('Page load time:', entry.loadEventEnd - entry.loadEventStart); } }); }).observe({ entryTypes: ['navigation'] }); } static recalculateLayout() { // Layout recalculation logic const tables = document.querySelectorAll('.table-responsive'); tables.forEach(table => { // Recalculate table dimensions }); } } // Initialize performance optimization document.addEventListener('DOMContentLoaded', () => { PerformanceOptimizer.init(); }); ``` --- ## 🎯 **RIEPILOGO CAPITOLO 8** ### **βœ… Completato** - **Architettura Frontend**: Struttura modulare, componenti base - **Stack Tecnologico**: Bootstrap 5, Alpine.js, Vite, Axios - **Componenti UI**: Button, Form, DataTable, Modal components - **JavaScript**: AJAX, Alpine.js, Event handling - **Responsive Design**: Breakpoints, mobile navigation - **Performance**: Lazy loading, code splitting, caching - **State Management**: Semplice gestore stato - **Validazione**: Real-time validation, regole custom - **Esempi Pratici**: Form completo, dashboard charts - **Best Practices**: Pattern modulari, error handling, ottimizzazione ### **🎯 Focus Principali** - Interfaccia moderna e responsive - Componenti riutilizzabili - Performance ottimizzata - Validazione robusta - Gestione errori completa ### **πŸ”§ Pronto per Implementazione** - Tutti i componenti base pronti - Esempi pratici testabili - Best practices definite - Struttura scalabile e mantenibile **Capitolo 8 completato con successo! 🎨**