Logo
DuoSync

DuoSync

20 novembre 2025 → 3 dicembre 2025
Next.js TypeScript PostgreSQL Drizzle ORM PWA Push Notifications React Context Tailwind CSS Radix UI Web Push API

Il Problema

Tutto è nato dalla difficoltà di trovare un orario per andare in palestra con un mio amico. La situazione era particolare: non era semplice comprendere in maniera chiara quando fossimo liberi, e soprattutto non avevamo quasi mai orari definiti in anticipo. Tra impegni di lavoro, appuntamenti e routine quotidiane che cambiano di giorno in giorno, coordinare gli orari richiedeva continui messaggi che potevano richiedere molti messaggi e molto tempo solo per capire quando entrambi eravamo liberi.

DuoSync nasce da questa esigenza concreta ed è stato progettato per funzionare in situazioni dove gli orari non sono fissi e la comunicazione diretta non è sempre la soluzione più pratica. Supporta fino a 10 utenti, permettendo di visualizzare la disponibilità condivisa e ricevere notifiche quando qualcuno aggiorna i propri impegni.

La Soluzione

DuoSync è un’applicazione web progressiva costruita con Next.js 16 che permette a gruppi fino a 10 utenti di gestire i propri appuntamenti e visualizzare automaticamente i momenti di disponibilità condivisa. L’applicazione offre una timeline visiva che mostra la disponibilità personale e quella condivisa, gestione appuntamenti one-time e ricorrenti, notifiche push per sincronizzazione automatica, supporto multi-lingua (italiano/inglese), tema chiaro/scuro, e architettura feature-based per manutenibilità e scalabilità.

DuoSync - Dashboard principale

Come Funziona

Il cuore dell’applicazione è la timeline di disponibilità, che mostra due visualizzazioni: la timeline personale mostra tutti gli appuntamenti dell’utente attivo con colori distinti per categoria (sonno, altri impegni, disponibile), mentre la timeline condivisa evidenzia in verde i momenti in cui tutti gli utenti sono liberi contemporaneamente, facilitando la pianificazione di attività comuni.

Gli appuntamenti possono essere creati in due modalità: one-time per impegni specifici di una data, o ricorrenti con giorni della settimana selezionabili. Il sistema valida automaticamente che non ci siano sovrapposizioni e suggerisce il prossimo slot disponibile quando necessario.

Non è necessario inserire appuntamenti tutti i giorni: l’applicazione è pensata per essere utilizzata quando serve, non come un calendario da aggiornare quotidianamente. Quando un utente si ricorda di aggiornare i propri impegni e conferma tramite il pulsante “Conferma”, viene inviata automaticamente una notifica push agli altri utenti (se hanno abilitato le notifiche), avvisandoli che l’utente specifico ha confermato i propri impegni. Questo sistema evita che qualcuno si dimentichi di aggiornare: la notifica serve da promemoria e permette agli altri di vedere immediatamente quando la disponibilità è stata aggiornata.

DuoSync - Timeline di disponibilità

Le Sfide Tecniche

La gestione degli appuntamenti ricorrenti ha richiesto un’architettura a due livelli: i template ricorrenti sono salvati nella tabella recurring_appointments con i giorni della settimana, mentre quando viene richiesta la lista per una data specifica, il sistema applica i template ricorrenti attivi per quel giorno e li combina con gli appuntamenti one-time. Questo approccio permette di modificare o eliminare i template senza dover aggiornare migliaia di record.

L’implementazione delle notifiche push ha richiesto l’integrazione con Web Push API, gestione delle VAPID keys, registrazione delle subscription nel database, e un service worker per gestire l’arrivo delle notifiche. Ho implementato un sistema che invia notifiche solo quando un utente conferma esplicitamente i propri impegni, evitando spam e mantenendo l’utente informato solo quando necessario.

DuoSync - Gestione appuntamenti

Architettura e Design Patterns

L’applicazione segue un’architettura feature-based dove ogni dominio (appointments, users, availability) ha la propria cartella con servizi di business logic puri e context per la gestione dello stato. I componenti sono presentazionali e ricevono dati tramite hook custom, mantenendo la separazione tra logica e presentazione.

Il sistema di state management utilizza React Context con provider isolati per feature. Ogni feature espone un context provider e un hook custom (es. useAppointments, useUsers) che i componenti utilizzano per accedere allo stato. Questo pattern evita prop drilling e mantiene lo stato locale alla feature, migliorando le performance con re-render mirati.

La persistenza dei dati è gestita tramite Drizzle ORM con PostgreSQL. Ho implementato un sistema di migrazioni schema-first dove lo schema è definito in TypeScript e sincronizzato con il database tramite Drizzle Kit. Le query sono ottimizzate con indici su userId e date per performance efficienti anche con molti appuntamenti.

L’architettura API segue il pattern Server/Client separation di Next.js: le route API (/app/api/*) gestiscono la validazione e chiamano i servizi di database, mentre i servizi client-side (features/*/services/*.service.ts) utilizzano fetch() per chiamare le API routes. Questo mantiene una chiara separazione tra browser e server, con validazione sia lato client (per UX) che lato server (per sicurezza).


Showcase video


Dettagli Tecnici

Stack Tecnologico
CategoriaTecnologiaVersioneScopo
FrameworkNext.js16.0.3React framework con App Router
React19.2.0UI library
LinguaggioTypeScript5Type safety
DatabasePostgreSQL16Database relazionale
Drizzle ORM0.44.7ORM type-safe
StylingTailwind CSS4Utility-first CSS
UI ComponentsRadix UILatestAccessible components
lucide-react0.554.0Icon library
Utilitiesdayjs1.11.19Date/time manipulation
date-fns4.1.0Date utilities
sonner2.0.7Toast notifications
PWAnext-themes0.4.6Theme management
web-push3.6.7Push notifications
Funzionalità Principali
FunzionalitàDescrizione
Gestione AppuntamentiCreazione, modifica, eliminazione appuntamenti one-time e ricorrenti
Timeline VisivaVisualizzazione disponibilità personale e condivisa con colori distinti
Validazione SlotControllo automatico sovrapposizioni e suggerimento prossimo slot disponibile
Appuntamenti RicorrentiTemplate ricorrenti con giorni della settimana selezionabili
Notifiche PushNotifiche quando un altro utente conferma i propri impegni
Multi-utenteSupporto fino a 10 utenti con switch rapido tra profili
InternazionalizzazioneSupporto italiano e inglese con context i18n
Tema Chiaro/ScuroSupporto per preferenze utente e tema di sistema
PWAInstallabile come app nativa con service worker
Admin PanelArea amministrativa protetta da PIN per gestione utenti
Architettura

Struttura Progetto

duosync/
├── app/ # Next.js App Router
│ ├── api/ # API routes (server-side)
│ │ ├── appointments/ # Endpoints per appuntamenti
│ │ ├── notifications/ # Endpoints per push notifications
│ │ └── users/ # Endpoints per utenti
│ ├── admin/ # Area amministrativa
│ ├── onboarding/ # Setup iniziale
│ └── page.tsx # Dashboard principale
├── components/ # Componenti UI riutilizzabili
│ ├── dashboard/ # Componenti specifici dashboard
│ ├── header/ # Header con user switcher
│ ├── layout/ # Layout components
│ └── ui/ # Radix UI components
├── features/ # Logica di business isolata
│ ├── appointments/ # Gestione appuntamenti
│ │ ├── services/ # Business logic pura
│ │ └── appointments-context.tsx
│ ├── availability/ # Calcolo disponibilità
│ │ └── services/
│ └── users/ # Gestione utenti
│ ├── services/
│ └── users-context.tsx
├── hooks/ # Custom hooks riutilizzabili
│ ├── use-other-user-appointments.ts
│ ├── use-time-input-validation.ts
│ └── useNotifications.ts
├── lib/ # Utilities e servizi
│ ├── db/ # Database schema e connection
│ ├── time/ # Utility per gestione tempo
│ └── notifications/ # Servizio push notifications
├── types/ # TypeScript types globali
│ └── index.tsx
└── i18n/ # Traduzioni e context i18n
├── en.json
└── it.json

State Management

  • Feature Contexts: Ogni feature ha il proprio context provider isolato
  • Custom Hooks: API pubblica esposta tramite hook (es. useAppointments())
  • State Isolation: Stato isolato per feature, evitando re-render globali
  • Optimistic Updates: Aggiornamenti ottimistici con rollback su errore

Timeline Algorithm

  • Time Points Collection: Raccoglie tutti i punti temporali dagli appuntamenti di tutti gli utenti
  • Segment Creation: Crea segmenti tra punti temporali consecutivi
  • Category Calculation: Calcola categoria basata su disponibilità di ciascun utente
  • Priority Logic: sleep > other > match > available
Database Schema

Tabelle Principali

TabellaChiaveRelazione
app_settingsid (PK)Singleton table
usersid (PK, serial)-
appointmentsid (PK, text)→ users.id
recurring_appointmentsid (PK, text)→ users.id
push_subscriptionsid (PK, serial)→ users.id

Campi Principali

app_settings:

  • adminPin (text) - PIN amministratore
  • isInitialized (boolean) - Stato inizializzazione

users:

  • id (serial, PK)
  • name (text) - Nome utente

appointments:

  • id (text, PK) - ID univoco appuntamento
  • userId (integer, FK → users.id)
  • date (date) - Data appuntamento
  • startTime (text) - Formato HH:mm
  • endTime (text) - Formato HH:mm
  • category (text) - “sleep” | “other”
  • description (text, nullable)

recurring_appointments:

  • id (text, PK)
  • userId (integer, FK → users.id)
  • startTime, endTime (text) - Formato HH:mm
  • category (text) - “sleep” | “other”
  • repeatDays (text[]) - Array di day IDs (1-7)

push_subscriptions:

  • id (serial, PK)
  • userId (integer, FK → users.id)
  • endpoint (text, unique) - Push subscription endpoint
  • p256dh, auth (text) - Chiavi crittografiche

Indici

  • appointments_userId_date_idx - Query efficienti per userId + date
  • recurring_appointments_userId_idx - Query template ricorrenti
  • push_subscriptions_userId_idx - Query subscription per utente
  • push_subscriptions_endpoint_idx - Lookup rapido per endpoint
API Endpoints
MetodoEndpointDescrizioneAccesso
GET/api/appointments?userId=1&date=2025-01-20Lista appuntamenti per utente e dataPublic
GET/api/appointments/batch?userIds=1,2&date=2025-01-20Lista appuntamenti per multipli utentiPublic
GET/api/appointments/recurring?userId=1Lista template ricorrentiPublic
POST/api/appointments/addCrea nuovo appuntamentoPublic
PUT/api/appointments/updateAggiorna appuntamento esistentePublic
POST/api/appointments/removeElimina appuntamentoPublic
GET/api/usersLista utentiPublic
POST/api/usersCrea nuovo utentePublic
PUT/api/usersAggiorna utentePublic
DELETE/api/usersElimina utentePublic
GET/api/notifications/vapid-public-keyOttiene chiave pubblica VAPIDPublic
POST/api/notifications/subscribeRegistra subscription pushPublic
DELETE/api/notifications/subscribeRimuove subscription pushPublic
POST/api/notifications/confirmInvia notifiche agli altri utentiPublic
POST/api/onboardingSetup iniziale appPublic
POST/api/admin/loginLogin area amministrativaPublic

Response Structure:

// Success
{ appointments: Appointment[] }
{ appointmentsByUser: Record<number, Appointment[]> }
{ users: UserProfile[] }
// Error
{ error: string }
Validazioni & Regole
CampoRequisitiDipendenze
OrarioFormato HH:mm (00:00-23:59)-
Fine giornata00:00 convertito in 24:00 se startTime >= 12:00-
SovrapposizioniNessuna sovrapposizione con appuntamenti esistentiValidazione lato client e server
Appuntamenti ricorrentiAlmeno un giorno della settimana selezionato-
PIN Admin6 cifre numericheSolo onboarding
Nome utenteNon vuoto, max 255 caratteri-
Limite utentiMassimo 10 utentiValidazione lato server

Validazione Slot:

  • Controllo formato orario (HH:mm)
  • Verifica che endTime > startTime
  • Controllo sovrapposizioni con appuntamenti esistenti
  • Suggerimento prossimo slot disponibile se invalido
Push Notifications

Configurazione

VAPID Keys:

  • Generazione tramite npx web-push generate-vapid-keys
  • Chiave pubblica esposta via /api/notifications/vapid-public-key
  • Chiave privata utilizzata server-side per invio notifiche

Service Worker:

  • Registrato in /public/sw.js
  • Gestisce l’arrivo delle notifiche push
  • Mostra notifiche con titolo e body personalizzati

Flusso Notifiche

  1. Registrazione: Utente abilita notifiche → browser richiede permesso → subscription registrata su server
  2. Invio: Utente clicca “Conferma” → chiamata /api/notifications/confirm → notifiche inviate a tutti gli altri utenti
  3. Ricezione: Service worker riceve notifica → mostra notifica browser → utente può aprire app

Sicurezza

  • Subscription endpoint univoco per utente
  • Validazione subscription prima dell’invio
  • Gestione errori con retry automatico
  • Notifiche inviate solo su azione esplicita utente
PWA Features

Manifest

  • Name: DuoSync
  • Short Name: DuoSync
  • Icons: 16x16 fino a 512x512 (PNG)
  • Theme Color: Dinamico basato su tema
  • Display: standalone
  • Start URL: /

Service Worker

  • Caching Strategy: Network-first con fallback cache
  • Offline Support: App funzionante offline per visualizzazione dati cached
  • Push Notifications: Gestione notifiche push tramite service worker

Install Prompt

  • Prompt automatico dopo onboarding
  • Hook usePWAInstall per gestione installazione
  • Supporto per installazione su desktop e mobile
Internazionalizzazione

Lingue Supportate

  • Italiano (it): Lingua principale
  • Inglese (en): Traduzione completa

Implementazione

  • Context i18n: I18nProvider con hook useI18n()
  • Traduzioni: File JSON in /i18n/ (en.json, it.json)
  • Template Interpolation: Supporto per valori dinamici {{name}}
  • Locale Persistence: Preferenza salvata in localStorage

Utilizzo

const { t, locale, setLocale } = useI18n()
const greeting = t('dashboard.greeting', { name: 'Mario' })
Documentazione

Tutta la documentazione è disponibile nel repository GitHub:

  • api-docs/duosync-api.postman_collection.json - Postman collection API
  • api-docs/duosync-api.postman_environment.json - Environment variables
  • Schema database definito in lib/db/schema.ts
  • Types globali in types/index.tsx