📋 Commit iniziale con: - ✅ Documentazione unificata in docs/ - ✅ Codice Laravel in netgescon-laravel/ - ✅ Script automazione in scripts/ - ✅ Configurazione sync rsync - ✅ Struttura organizzata e pulita 🔄 Versione: 2025.07.19-1644 🎯 Sistema pronto per Git distribuito
43 KiB
43 KiB
🎨 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
- Architettura Frontend
- Stack Tecnologico
- Componenti UI
- JavaScript e Interattività
- Responsive Design
- Performance e Ottimizzazione
- Gestione Stati
- Validazione Frontend
- Esempi Pratici
- 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à
// 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
// 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
// package.json
{
"devDependencies": {
"bootstrap": "^5.3.0",
"sass": "^1.77.8",
"@fortawesome/fontawesome-free": "^6.5.0"
}
}
JavaScript Libraries
{
"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
// 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
// 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
<!-- resources/views/components/button.blade.php -->
@php
$classes = [
'btn',
'btn-' . ($variant ?? 'primary'),
$size ? 'btn-' . $size : '',
$disabled ? 'disabled' : '',
$loading ? 'btn-loading' : '',
$attributes->get('class')
];
@endphp
<button
{{ $attributes->merge(['class' => implode(' ', array_filter($classes))]) }}
@if($loading) disabled @endif
>
@if($loading)
<span class="spinner-border spinner-border-sm me-2"></span>
@endif
@if($icon)
<i class="fas fa-{{ $icon }} me-2"></i>
@endif
{{ $slot }}
</button>
Form Input Component
<!-- resources/views/components/form/input.blade.php -->
<div class="mb-3">
@if($label)
<label for="{{ $id ?? $name }}" class="form-label">
{{ $label }}
@if($required)
<span class="text-danger">*</span>
@endif
</label>
@endif
<input
type="{{ $type ?? 'text' }}"
name="{{ $name }}"
id="{{ $id ?? $name }}"
class="form-control @error($name) is-invalid @enderror"
value="{{ old($name, $value ?? '') }}"
@if($required) required @endif
{{ $attributes }}
>
@error($name)
<div class="invalid-feedback">
{{ $message }}
</div>
@enderror
@if($help)
<div class="form-text">{{ $help }}</div>
@endif
</div>
3.2 Componenti Avanzati
DataTable Component
<!-- resources/views/components/datatable.blade.php -->
<div class="table-responsive">
<table class="table table-striped table-hover" id="{{ $id }}">
<thead class="table-dark">
<tr>
@foreach($columns as $column)
<th>{{ $column['title'] }}</th>
@endforeach
@if($actions)
<th width="150">Azioni</th>
@endif
</tr>
</thead>
<tbody>
{{ $slot }}
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#{{ $id }}').DataTable({
processing: true,
serverSide: true,
ajax: '{{ $ajax }}',
columns: @json($columns),
language: {
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/it-IT.json'
},
responsive: true,
dom: 'Bfrtip',
buttons: ['copy', 'excel', 'pdf', 'print']
});
});
</script>
Modal Component
<!-- resources/views/components/modal.blade.php -->
<div class="modal fade" id="{{ $id }}" tabindex="-1">
<div class="modal-dialog {{ $size ? 'modal-' . $size : '' }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $title }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{{ $slot }}
</div>
@if($footer)
<div class="modal-footer">
{{ $footer }}
</div>
@endif
</div>
</div>
</div>
4. JAVASCRIPT E INTERATTIVITÀ
4.1 Gestione AJAX
Configurazione Axios
// 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
// 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
// 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
<!-- resources/views/dashboard.blade.php -->
<div x-data="dashboardStats">
<div class="row" x-show="!loading">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">Condomini Attivi</h5>
<h2 x-text="stats.active_condominiums"></h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">Fatturato Mensile</h5>
<h2 x-text="formatCurrency(stats.monthly_revenue)"></h2>
</div>
</div>
</div>
</div>
<div x-show="loading" class="text-center p-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
</div>
</div>
5. RESPONSIVE DESIGN
5.1 Breakpoints Strategy
Custom Breakpoints
// 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
<!-- resources/views/components/responsive-table.blade.php -->
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
@foreach($columns as $column)
<th class="{{ $column['responsive'] ?? '' }}">
{{ $column['title'] }}
</th>
@endforeach
</tr>
</thead>
<tbody>
{{ $slot }}
</tbody>
</table>
</div>
<style>
@media (max-width: 768px) {
.d-none-mobile {
display: none !important;
}
.table-responsive {
font-size: 0.875rem;
}
.table td {
padding: 0.5rem;
}
}
</style>
Mobile Navigation
<!-- resources/views/layouts/partials/mobile-nav.blade.php -->
<div class="mobile-nav d-lg-none">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ route('dashboard') }}">
NetGescon
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mobileNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mobileNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ route('dashboard') }}">
<i class="fas fa-home"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('condominiums.index') }}">
<i class="fas fa-building"></i> Condomini
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('accounting.index') }}">
<i class="fas fa-euro-sign"></i> Contabilità
</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
6. PERFORMANCE E OTTIMIZZAZIONE
6.1 Lazy Loading
Image Lazy Loading
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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 =>
`<div class="invalid-feedback d-block">${error}</div>`
).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
<!-- resources/views/condominiums/create.blade.php -->
<form id="condominiumForm" action="{{ route('condominiums.store') }}" method="POST"
x-data="condominiumForm" @submit.prevent="submitForm">
@csrf
<div class="row">
<div class="col-md-6">
<x-form.input
name="name"
label="Nome Condominio"
data-validate="required|minLength:3"
required
/>
</div>
<div class="col-md-6">
<x-form.input
name="fiscal_code"
label="Codice Fiscale"
data-validate="required|fiscal_code"
required
/>
</div>
</div>
<div class="row">
<div class="col-md-8">
<x-form.input
name="address"
label="Indirizzo"
data-validate="required|minLength:5"
required
/>
</div>
<div class="col-md-4">
<x-form.input
name="postal_code"
label="CAP"
data-validate="required|numeric|minLength:5|maxLength:5"
required
/>
</div>
</div>
<div class="row">
<div class="col-md-6">
<select name="city" class="form-select" data-validate="required" required>
<option value="">Seleziona città</option>
<option value="milano">Milano</option>
<option value="roma">Roma</option>
<option value="napoli">Napoli</option>
</select>
</div>
<div class="col-md-6">
<x-form.input
name="phone"
label="Telefono"
data-validate="phone"
type="tel"
/>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2" @click="resetForm">
Annulla
</button>
<x-button type="submit" :loading="submitting">
Salva Condominio
</x-button>
</div>
</form>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('condominiumForm', () => ({
submitting: false,
async submitForm() {
this.submitting = true;
try {
const formData = new FormData(this.$el);
const response = await axios.post(this.$el.action, formData);
Toast.success('Condominio salvato con successo');
window.location.href = '/condominiums';
} catch (error) {
if (error.response.status === 422) {
this.showValidationErrors(error.response.data.errors);
} else {
Toast.error('Errore durante il salvataggio');
}
} finally {
this.submitting = false;
}
},
resetForm() {
this.$el.reset();
this.$el.querySelectorAll('.is-invalid').forEach(el => {
el.classList.remove('is-invalid');
});
},
showValidationErrors(errors) {
Object.keys(errors).forEach(field => {
const input = this.$el.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];
}
});
}
}));
});
// Inizializza validazione live
document.addEventListener('DOMContentLoaded', () => {
new LiveValidator(document.getElementById('condominiumForm'));
});
</script>
9.2 Dashboard Charts
Chart Component
<!-- resources/views/dashboard/charts.blade.php -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Fatturato Mensile</h5>
</div>
<div class="card-body">
<canvas id="revenueChart" width="400" height="200"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Distribuzione Condomini</h5>
</div>
<div class="card-body">
<canvas id="condominiumsChart" width="400" height="200"></canvas>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
// Carica dati charts
const response = await axios.get('/api/dashboard/charts');
const data = response.data;
// Revenue Chart
const revenueCtx = document.getElementById('revenueChart').getContext('2d');
new Chart(revenueCtx, {
type: 'line',
data: {
labels: data.revenue.labels,
datasets: [{
label: 'Fatturato €',
data: data.revenue.values,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Fatturato Mensile'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return '€' + value.toLocaleString();
}
}
}
}
}
});
// Condominiums Chart
const condominiumsCtx = document.getElementById('condominiumsChart').getContext('2d');
new Chart(condominiumsCtx, {
type: 'doughnut',
data: {
labels: data.condominiums.labels,
datasets: [{
data: data.condominiums.values,
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
});
</script>
10. BEST PRACTICES
10.1 Code Organization
Module Pattern
// 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
// 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
// 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! 🎨