# π¨ **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)
| {{ $column['title'] }} |
@endforeach
@if($actions)
Azioni |
@endif
{{ $slot }}
```
#### **Modal Component**
```html
{{ $slot }}
@if($footer)
@endif
```
---
## 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
```
---
## 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)
|
{{ $column['title'] }}
|
@endforeach
{{ $slot }}
```
#### **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
```
### 9.2 **Dashboard Charts**
#### **Chart Component**
```html
```
---
## 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! π¨**