feat(legal): pagine Privacy e Termini di servizio + link da home, auth, cookie banner

Aggiunge /legal/privacy e /legal/terms come pagine pubbliche SSG, conformi
GDPR art. 13 (dati art. 9 salute, destinatari Stripe/Google/SendGrid/R2,
diritti, reclamo al Garante) e D.Lgs. 206/2005 (oggetto, trial 30gg + Pro,
recesso 14gg, foro consumatore). Link da: footer home, cookie banner,
welcome (disclaimer), sign-in (legalNote), sign-up (disclaimer rich).

Titolare: Carlo Santoleri (persona fisica, beta). Ultimo aggiornamento: 2026-05-20.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-05-20 12:27:39 +02:00
1 parent 3f1317b2da
commit ddc266536a
8 files changed
+645 -8

No files matched your search

+200 -3
View File
@@ -62,7 +62,9 @@
"ctaSectionTitle": "Pronti a partire?",
"ctaSectionBody": "Sette passaggi, dieci minuti, e il tuo primo piano è pronto. Senza carta di credito.",
"principlesEyebrow": "Tre principi non negoziabili",
"footerCopyright": "KetoPath — anno I, Italia."
"footerCopyright": "KetoPath — anno I, Italia.",
"footerLinkPrivacy": "Privacy",
"footerLinkTerms": "Termini"
},
"Welcome": {
"title": "Prima di iniziare",
@@ -81,6 +83,7 @@
"CookieBanner": {
"aria": "Informativa cookie",
"message": "Usiamo solo cookie essenziali per autenticazione e sessione. Niente analytics o tracker di terze parti.",
"learnMore": "Leggi l'Informativa Privacy",
"dismiss": "Ho capito"
},
"Metadata": {
@@ -98,7 +101,8 @@
"google": "Continua con Google",
"or": "oppure",
"noAccount": "Non hai ancora un account? <link>Iscriviti</link>",
"error": "Credenziali non valide. Riprova."
"error": "Credenziali non valide. Riprova.",
"legalNote": "Accedendo accetti i <terms>Termini di servizio</terms> e la nostra <privacy>Informativa Privacy</privacy>."
},
"SignUp": {
"title": "Iscriviti",
@@ -113,7 +117,7 @@
"or": "oppure",
"haveAccount": "Hai già un account? <link>Accedi</link>",
"error": "Non è stato possibile completare la registrazione. Riprova.",
"disclaimer": "Procedendo accetti che i tuoi dati siano trattati come descritto nell'informativa sulla privacy. KetoPath non sostituisce il parere medico."
"disclaimer": "Procedendo accetti i <terms>Termini di servizio</terms> e l'<privacy>Informativa Privacy</privacy>. KetoPath è uno strumento di stile di vita, non sostituisce il parere medico."
}
},
"Fasting": {
@@ -769,5 +773,198 @@
"api_error_401": "Devi accedere per gestire l'abbonamento.",
"api_error_500": "Errore del server. Riprova fra poco."
}
},
"Legal": {
"Shared": {
"lastUpdated": "Ultimo aggiornamento — {date}",
"toc": "Indice",
"related": "Pagine collegate",
"linkPrivacy": "Informativa Privacy",
"linkTerms": "Termini di servizio",
"linkHome": "Torna alla home"
},
"Privacy": {
"metaTitle": "Informativa Privacy — KetoPath",
"metaDescription": "Informativa sul trattamento dei dati personali ai sensi degli artt. 13 e 14 del Regolamento (UE) 2016/679 (GDPR).",
"eyebrow": "Capitolo X — Informativa Privacy",
"title": "Come trattiamo i tuoi dati",
"subtitle": "Questa informativa spiega quali dati personali raccogliamo quando usi KetoPath, perché lo facciamo, per quanto tempo li conserviamo e quali diritti puoi esercitare in qualsiasi momento. È redatta ai sensi degli artt. 13 e 14 del Regolamento (UE) 2016/679 (GDPR) e del D.Lgs. 196/2003 come modificato dal D.Lgs. 101/2018.",
"sections": {
"controller": {
"heading": "Titolare del trattamento",
"p1": "Il titolare del trattamento dei dati personali raccolti tramite il sito lamiadieta.luzaonline.net e l'applicazione KetoPath è Carlo Santoleri, in qualità di persona fisica. Per qualunque richiesta relativa alla presente informativa o all'esercizio dei diritti di cui agli artt. 15-22 GDPR puoi scrivere all'indirizzo email carlo.santoleri@gmail.com.",
"p2": "Non è stato nominato un Responsabile della Protezione dei Dati (DPO) in quanto non ricorrono i presupposti obbligatori di cui all'art. 37 GDPR. Il titolare resta comunque l'unico referente per ogni richiesta e si impegna a rispondere entro 30 giorni dalla ricezione, salvo proroghe motivate ai sensi dell'art. 12 par. 3 GDPR."
},
"dataCollected": {
"heading": "Categorie di dati raccolti",
"p1": "Dati di account e autenticazione: indirizzo email, nome visualizzato, password (memorizzata esclusivamente come hash scrypt, mai in chiaro), identificativo OAuth se accedi tramite Google, data di creazione dell'account, data dell'ultimo accesso, indirizzo IP di sessione e identificativo del dispositivo.",
"p2": "Dati di profilo nutrizionale: data di nascita, sesso biologico (necessario al calcolo del metabolismo basale secondo la formula Mifflin-St Jeor), altezza, peso iniziale, peso obiettivo, livello di attività fisica, protocollo di digiuno intermittente preferito, esclusioni alimentari (allergie, intolleranze, scelte etiche).",
"p3": "Dati di tracking (categoria particolare ai sensi dell'art. 9 GDPR — vedi sezione successiva): pesate ricorrenti, misure corporee, eventuali fotografie di progresso caricate volontariamente, registrazioni di check-in soggettivi quotidiani (energia, sonno, fame, umore), sintomi associati ai digiuni, sessioni di digiuno avviate e completate.",
"p4": "Dati di pagamento e fatturazione: identificativo cliente Stripe, ultime quattro cifre della carta utilizzata, stato dell'abbonamento, periodi di rinnovo e fatture. La carta di pagamento non viene mai trasmessa ai nostri server: è gestita interamente dalla piattaforma Stripe Inc. e dalla sua controllata europea (vedi sezione \"Destinatari\").",
"p5": "Dati tecnici di funzionamento: log di errore lato server (senza riferimenti diretti ai dati di salute), preferenze di sessione (lingua, locale, consenso cookie), tipo di browser e sistema operativo per finalità di compatibilità e sicurezza."
},
"healthData": {
"heading": "Dati relativi alla salute",
"p1": "I dati di tracking (peso, misure, foto, sintomi, check-in soggettivi) rientrano nella nozione di \"dati relativi alla salute\" di cui all'art. 4 n. 15 e art. 9 GDPR. Il loro trattamento richiede una base giuridica rafforzata, individuata nel consenso esplicito da te prestato in sede di onboarding (art. 9 par. 2 lett. a GDPR), che puoi revocare in qualsiasi momento dalla pagina Profilo > Dati senza che ciò pregiudichi la liceità del trattamento effettuato prima della revoca.",
"p2": "Questi dati sono cifrati at-rest tramite algoritmo AES-GCM 256 a livello applicativo prima di essere persistiti nel database, in aggiunta alla cifratura del filesystem del server. Non vengono utilizzati per profilazione, scoring sanitario o decisioni automatizzate ai sensi dell'art. 22 GDPR, né condivisi con compagnie assicurative, datori di lavoro, autorità sanitarie o terzi a fini commerciali. I log applicativi sono configurati per non includere alcun campo classificato come dato di salute."
},
"purposes": {
"heading": "Finalità del trattamento",
"p1": "Erogare il servizio: creare il tuo account, calcolare fabbisogno calorico e macros, generare il piano alimentare settimanale e la lista della spesa, mostrarti grafici di andamento, gestire le sessioni di digiuno, inviarti i promemoria che hai esplicitamente scelto di ricevere.",
"p2": "Gestire l'abbonamento: fatturare il periodo di prova (gratuito 30 giorni) e gli eventuali rinnovi del piano Pro, conservare le fatture per gli obblighi fiscali, comunicarti scadenze e cambi di stato dell'abbonamento.",
"p3": "Garantire la sicurezza: rilevare e prevenire accessi abusivi, frodi sui pagamenti, abusi dell'API, attacchi automatizzati. Per questa finalità conserviamo per un periodo limitato gli indirizzi IP di sessione e i tentativi di accesso falliti.",
"p4": "Adempiere a obblighi di legge: conservare la documentazione fiscale e contabile, rispondere a richieste motivate dell'autorità giudiziaria, dell'autorità garante per la protezione dei dati personali o di altre autorità competenti."
},
"legalBasis": {
"heading": "Base giuridica",
"p1": "Per i dati di account e per l'esecuzione del piano alimentare la base giuridica è l'esecuzione del contratto di cui sei parte ai sensi dell'art. 6 par. 1 lett. b GDPR.",
"p2": "Per i dati relativi alla salute (peso, misure, foto, sintomi, check-in) la base giuridica è il tuo consenso esplicito ai sensi dell'art. 9 par. 2 lett. a GDPR, prestato durante l'onboarding insieme all'accettazione del disclaimer medico.",
"p3": "Per la fatturazione, la sicurezza, la prevenzione frodi e la conservazione dei log la base giuridica è il legittimo interesse del titolare e l'adempimento di obblighi legali ai sensi dell'art. 6 par. 1 lett. c ed f GDPR."
},
"recipients": {
"heading": "Destinatari e responsabili del trattamento",
"p1": "I tuoi dati sono accessibili al solo titolare e ai fornitori di servizi tecnici (\"responsabili del trattamento\" ai sensi dell'art. 28 GDPR) strettamente necessari all'erogazione del servizio. Tutti i responsabili sono vincolati da un accordo (Data Processing Agreement) che ne disciplina obblighi e limiti.",
"p2": "Hosting e database: il server applicativo, il database PostgreSQL e Redis sono ospitati su infrastruttura Virtualmin/VPS gestita all'interno dell'Unione Europea. I backup giornalieri sono cifrati e conservati sullo stesso provider.",
"p3": "Autenticazione Google (opzionale): se scegli di accedere con Google, Google Ireland Limited tratta il tuo indirizzo email e il tuo nome al solo scopo di autenticarti. KetoPath riceve esclusivamente le informazioni minime di profilo necessarie a creare la sessione.",
"p4": "Pagamenti: Stripe Payments Europe Limited (sede in Irlanda) elabora le transazioni economiche e gestisce direttamente i dati della carta. KetoPath riceve da Stripe soltanto i metadati strettamente necessari alla fatturazione e all'identificazione del cliente.",
"p5": "Email transazionali: SendGrid (Twilio Inc.) viene utilizzato per l'invio di email di sistema (conferma account, reset password, promemoria di rinnovo). A SendGrid trasmettiamo unicamente il tuo indirizzo email e il contenuto della comunicazione.",
"p6": "Storage delle foto progresso: le immagini che decidi di caricare sono archiviate su Cloudflare R2 in region europea, accessibili tramite URL firmati a scadenza breve. Nessun altro soggetto può accedere alle tue foto."
},
"transfers": {
"heading": "Trasferimenti extra-UE",
"p1": "Alcuni responsabili (Stripe, SendGrid, Cloudflare) appartengono a gruppi societari con sede negli Stati Uniti. I trasferimenti di dati al di fuori dello Spazio Economico Europeo sono effettuati sulla base delle Clausole Contrattuali Standard approvate dalla Commissione Europea (decisione 2021/914) e, ove applicabile, sulla base del Data Privacy Framework UE-USA.",
"p2": "Su richiesta puoi ottenere copia delle garanzie contrattuali in essere scrivendo all'indirizzo email del titolare."
},
"retention": {
"heading": "Tempi di conservazione",
"p1": "Dati di account: per tutta la durata del tuo utilizzo del servizio e fino a 12 mesi dall'ultimo accesso, dopodiché l'account è soggetto a procedura di chiusura automatica con preavviso. Su tua richiesta possiamo procedere alla cancellazione immediata.",
"p2": "Dati relativi alla salute: per la durata dell'account, salvo revoca del consenso. Alla cancellazione dell'account vengono rimossi entro 30 giorni, esclusi i backup cifrati che decadono al massimo entro 90 giorni dalla cancellazione.",
"p3": "Dati fiscali e di fatturazione: 10 anni dalla data di emissione del documento, in adempimento agli obblighi del Codice Civile e della normativa fiscale italiana."
},
"security": {
"heading": "Misure di sicurezza",
"p1": "Adottiamo misure tecniche e organizzative adeguate al rischio (art. 32 GDPR): connessione HTTPS con certificato Let's Encrypt, cifratura del database a livello di disco e a livello di campo per i dati sensibili, hashing scrypt delle password, rate limiting sulle API, accesso al server limitato a chiavi SSH dedicate, backup giornalieri cifrati, monitoraggio degli accessi anomali e degli errori applicativi.",
"p2": "Nonostante l'impegno costante, nessun sistema può garantire la sicurezza assoluta. Ti chiediamo di scegliere una password robusta, di non condividerla e di segnalarci immediatamente qualunque attività sospetta sul tuo account."
},
"rights": {
"heading": "I tuoi diritti",
"p1": "Hai il diritto di accedere ai tuoi dati (art. 15), di chiederne la rettifica (art. 16), la cancellazione (art. 17 — diritto all'oblio), la limitazione del trattamento (art. 18), di ricevere i dati in formato strutturato e leggibile da dispositivo automatico per trasmetterli ad altro titolare (art. 20 — portabilità), di opporti al trattamento (art. 21) e di revocare in qualsiasi momento il consenso prestato senza pregiudizio per la liceità del trattamento precedente.",
"p2": "Puoi esercitare questi diritti direttamente dalla pagina Profilo > Dati (esportazione e cancellazione) oppure scrivendo a carlo.santoleri@gmail.com. La risposta è gratuita e ti perverrà entro 30 giorni, salvo proroga motivata.",
"p3": "Se ritieni che il trattamento dei tuoi dati violi il GDPR puoi proporre reclamo al Garante per la Protezione dei Dati Personali (Piazza Venezia 11, 00187 Roma — www.garanteprivacy.it) o all'autorità di controllo dello Stato membro UE in cui risiedi abitualmente."
},
"minors": {
"heading": "Minori",
"p1": "Il servizio è destinato esclusivamente a persone maggiorenni. Non raccogliamo intenzionalmente dati di minori di 18 anni. Se vieni a conoscenza di un account riconducibile a un minore, ti invitiamo a segnalarcelo: provvederemo alla rimozione immediata."
},
"cookies": {
"heading": "Cookie e tecnologie simili",
"p1": "KetoPath utilizza esclusivamente cookie tecnici di sessione e cookie di funzionalità (preferenze di lingua, conferma dell'avviso cookie). Non utilizziamo cookie di profilazione né tracker di terze parti: nessun Google Analytics, nessun pixel pubblicitario.",
"p2": "L'unica eccezione riguarda Stripe, che durante le pagine di checkout può impostare cookie di sicurezza necessari a prevenire frodi sui pagamenti: si tratta di cookie tecnici esenti dal consenso ai sensi del provvedimento del Garante del 10 giugno 2021."
},
"changes": {
"heading": "Modifiche all'informativa",
"p1": "Possiamo aggiornare questa informativa quando cambiamo le finalità, le tecnologie o i fornitori che trattano i tuoi dati. In caso di modifiche sostanziali ti avviseremo via email o tramite un avviso visibile in app almeno 15 giorni prima dell'entrata in vigore. La data dell'ultimo aggiornamento è indicata in cima alla pagina."
},
"contact": {
"heading": "Contatti",
"p1": "Per ogni richiesta legata a questa informativa o all'esercizio dei tuoi diritti puoi scrivere a carlo.santoleri@gmail.com. Cerchiamo di rispondere il prima possibile e comunque entro i termini di legge."
}
}
},
"Terms": {
"metaTitle": "Termini di servizio — KetoPath",
"metaDescription": "Termini e condizioni d'uso dell'applicazione KetoPath: oggetto del servizio, account, abbonamento, recesso e responsabilità.",
"eyebrow": "Capitolo XI — Termini di servizio",
"title": "I termini, in chiaro",
"subtitle": "Queste sono le condizioni che regolano l'uso di KetoPath. Sono scritte in italiano semplice perché tu possa leggerle davvero. Iscrivendoti accetti questi termini insieme all'Informativa Privacy collegata.",
"sections": {
"object": {
"heading": "Oggetto del servizio",
"p1": "KetoPath è uno strumento di stile di vita pensato per accompagnarti in un percorso di alimentazione chetogenica e digiuno intermittente: piani alimentari personalizzati, lista della spesa automatica, tracking del peso e delle misure, timer per i protocolli di digiuno, suggerimenti progressivi sulle tre fasi (intensiva, transizione, mantenimento).",
"p2": "Il servizio è fornito da Carlo Santoleri (\"il titolare\") attraverso il sito lamiadieta.luzaonline.net. Iscrivendoti dichiari di aver letto e compreso questi termini e di accettarli integralmente."
},
"noMedicalAdvice": {
"heading": "Non sostituisce il medico",
"p1": "KetoPath non è un dispositivo medico, non emette diagnosi e non prescrive trattamenti. Le indicazioni nutrizionali generate dall'app sono basate sui parametri che inserisci e su formule biometriche di letteratura: hanno valore informativo e di stile di vita, non valore sanitario.",
"p2": "Prima di iniziare un percorso chetogenico o di digiuno intermittente ti raccomandiamo di confrontarti con il tuo medico, in particolare se sei in gravidanza o allattamento, se hai diabete (di tipo 1 o di tipo 2 in terapia), patologie cardiache, renali o epatiche, disturbi del comportamento alimentare, oppure se assumi farmaci con dosaggio sensibile (es. ipoglicemizzanti, anticoagulanti, antipertensivi).",
"p3": "L'utente è l'unico responsabile dell'uso che fa delle informazioni fornite dall'app e delle decisioni alimentari conseguenti. In caso di sintomi inattesi sospendi l'uso del servizio e consulta un professionista."
},
"account": {
"heading": "Account utente",
"p1": "Per usare il servizio devi avere almeno 18 anni e creare un account fornendo un indirizzo email valido oppure accedendo con Google. Sei responsabile della riservatezza delle credenziali e di ogni attività svolta tramite il tuo account.",
"p2": "Devi inserire dati veritieri: età, sesso, peso, altezza e livello di attività sono i parametri che alimentano i calcoli energetici dell'app. Dati errati produrranno piani non aderenti alle tue reali esigenze.",
"p3": "Puoi chiudere l'account in qualsiasi momento dalla pagina Profilo > Dati. La chiusura comporta la cessazione del servizio e l'avvio della procedura di cancellazione dei dati nei tempi descritti nell'Informativa Privacy."
},
"trialAndSubscription": {
"heading": "Periodo di prova e abbonamento",
"p1": "KetoPath è disponibile gratuitamente per 30 giorni a partire dalla creazione dell'account. Durante questo periodo hai accesso a tutte le funzionalità della versione Pro senza alcun addebito e senza richiesta di carta di pagamento.",
"p2": "Al termine del periodo di prova, per continuare ad accedere alle funzionalità riservate (generazione e adattamento del piano settimanale, salvataggio illimitato dei tracking, esportazione PDF, lista della spesa avanzata), occorre sottoscrivere l'abbonamento Pro. Le funzionalità di base — visualizzazione del piano già generato, lettura dei tracking esistenti, contenuti informativi — restano disponibili anche senza sottoscrizione.",
"p3": "L'abbonamento Pro è offerto con cadenza mensile o annuale. Il prezzo vigente è indicato nella pagina di checkout al momento della sottoscrizione e può variare nel tempo per i nuovi sottoscrittori: per chi è già abbonato eventuali aumenti saranno comunicati con almeno 30 giorni di preavviso e potrai recedere prima della loro entrata in vigore.",
"p4": "L'abbonamento si rinnova automaticamente alla scadenza del periodo (mensile o annuale) salvo disdetta da effettuarsi entro la fine del periodo in corso direttamente dalla pagina Abbonamento."
},
"paymentAndBilling": {
"heading": "Pagamento e fatturazione",
"p1": "I pagamenti sono gestiti da Stripe Payments Europe Limited. KetoPath non memorizza i dati della tua carta: vediamo soltanto l'identificativo cliente Stripe e gli ultimi quattro numeri della carta. Stripe accetta le principali carte di credito e debito.",
"p2": "Riceverai conferma del pagamento via email e, se richiedi fatturazione intestata, potrai inserire i tuoi dati fiscali direttamente nel portale clienti Stripe accessibile dalla pagina Abbonamento.",
"p3": "In caso di pagamento non riuscito (carta scaduta, fondi insufficienti, blocchi dell'emittente) Stripe ritenterà l'addebito per alcuni giorni. Trascorso questo periodo l'abbonamento entrerà in stato \"sospeso\" e l'accesso alle funzionalità Pro sarà temporaneamente disattivato finché il pagamento non sarà regolarizzato."
},
"withdrawal": {
"heading": "Diritto di recesso",
"p1": "Ai sensi degli artt. 52 e seguenti del Codice del Consumo (D.Lgs. 206/2005) hai diritto di recedere senza fornire motivazioni entro 14 giorni dalla sottoscrizione dell'abbonamento Pro a pagamento.",
"p2": "Per esercitare il recesso è sufficiente comunicarcelo via email a carlo.santoleri@gmail.com indicando l'indirizzo email associato all'account. Provvederemo al rimborso del prezzo pagato entro 14 giorni dalla ricezione, utilizzando lo stesso strumento di pagamento.",
"p3": "Il periodo di prova gratuito di 30 giorni non comporta alcun addebito: per interromperlo è sufficiente non sottoscrivere l'abbonamento o chiudere l'account in qualunque momento, senza necessità di esercitare il recesso."
},
"userObligations": {
"heading": "Obblighi e divieti",
"p1": "Devi usare il servizio in conformità alla legge e in buona fede. È vietato accedere all'account altrui, decompilare o reverse-engineering del software, aggirare i limiti tecnici, sovraccaricare i sistemi con automazioni non autorizzate.",
"p2": "È vietato caricare contenuti illeciti, lesivi della dignità altrui, pedopornografici, contrari all'ordine pubblico o protetti da diritti d'autore di terzi. Le foto progresso sono accessibili solo a te: non utilizzare il servizio come canale di pubblicazione.",
"p3": "È vietato utilizzare il servizio per fornire consulenze nutrizionali o sanitarie a terzi: KetoPath è uno strumento personale, non una piattaforma per professionisti.",
"p4": "Eventuali violazioni possono comportare la sospensione o la chiusura immediata dell'account, fermo restando il diritto al risarcimento dei danni e alla segnalazione alle autorità competenti."
},
"ip": {
"heading": "Proprietà intellettuale",
"p1": "Il software KetoPath, il design dell'interfaccia, i testi editoriali, le ricette curate, gli algoritmi di calcolo e tutti i contenuti del servizio sono di proprietà del titolare o di chi gli ha concesso licenza, e sono protetti dalle leggi italiane ed europee sul diritto d'autore.",
"p2": "Con la sottoscrizione ti viene concessa una licenza personale, non esclusiva, non trasferibile e revocabile a usare il servizio per finalità non commerciali. Nessun contenuto può essere riprodotto, distribuito o utilizzato per addestrare modelli di intelligenza artificiale senza preventiva autorizzazione scritta."
},
"userContent": {
"heading": "Contenuti generati dall'utente",
"p1": "I dati che inserisci nell'app (peso, misure, foto, sintomi, note) restano di tua proprietà. Concedi al titolare la licenza minima necessaria a erogarti il servizio: archiviarli, mostrarteli, generare statistiche personali, esportarli quando li richiedi.",
"p2": "Non utilizziamo i tuoi contenuti per scopi diversi dall'erogazione del servizio: nessuna pubblicità, nessuna cessione a terzi, nessun addestramento di modelli di intelligenza artificiale generativa."
},
"serviceChanges": {
"heading": "Evoluzione del servizio",
"p1": "Il servizio è in continuo sviluppo: alcune funzionalità possono essere modificate, sostituite o rimosse. In caso di modifiche sostanziali che incidano sulle funzionalità incluse nell'abbonamento attivo ti avviseremo con almeno 30 giorni di preavviso.",
"p2": "Eventuali nuove funzionalità potranno essere offerte come parte dell'abbonamento esistente o come moduli aggiuntivi separati. Non sei mai obbligato ad accettarle."
},
"termination": {
"heading": "Sospensione e cessazione",
"p1": "Il titolare può sospendere o chiudere l'account in caso di violazione di questi termini, di uso fraudolento del servizio o di mancato pagamento prolungato. Nei casi non urgenti riceverai un preavviso ragionevole con possibilità di rimediare.",
"p2": "Tu puoi chiudere l'account in qualsiasi momento dalla pagina Profilo > Dati. La chiusura non dà diritto al rimborso del periodo già pagato, salvo il caso del recesso entro 14 giorni descritto sopra."
},
"liability": {
"heading": "Limitazione di responsabilità",
"p1": "Nei limiti consentiti dalla legge, il titolare non risponde dei danni indiretti, di lucro cessante, di perdita di dati o di occasione, derivanti dall'uso o dall'impossibilità di uso del servizio.",
"p2": "In ogni caso la responsabilità complessiva del titolare per danni diretti non potrà superare l'importo effettivamente pagato dall'utente nei 12 mesi precedenti l'evento dannoso, salvi i casi di dolo o colpa grave.",
"p3": "Nessuna clausola di questi termini esclude o limita la responsabilità per danni alla persona derivanti dalla violazione di diritti inderogabili previsti dalla legge italiana, in particolare in materia di consumatori (D.Lgs. 206/2005)."
},
"forceMajeure": {
"heading": "Forza maggiore",
"p1": "Non rispondiamo dei mancati adempimenti dovuti a cause di forza maggiore o caso fortuito, ivi compresi guasti dei provider di hosting, interruzioni di rete, attacchi informatici massivi, provvedimenti delle autorità, calamità naturali, scioperi generali."
},
"governingLaw": {
"heading": "Legge applicabile e foro",
"p1": "Questi termini sono regolati dalla legge italiana. Per ogni controversia relativa all'interpretazione e all'esecuzione del contratto è competente in via esclusiva il foro del consumatore individuato dal D.Lgs. 206/2005, ovvero il foro del luogo di residenza o domicilio dell'utente, se ubicato in Italia.",
"p2": "Prima di adire l'autorità giudiziaria ti incoraggiamo a contattarci via email per cercare una soluzione bonaria. È inoltre disponibile la piattaforma europea ODR (Online Dispute Resolution) all'indirizzo ec.europa.eu/consumers/odr."
},
"changes": {
"heading": "Modifiche ai termini",
"p1": "Possiamo aggiornare questi termini per ragioni operative, normative o di sicurezza. In caso di modifiche sostanziali ti avviseremo via email o con un avviso visibile in app almeno 15 giorni prima dell'entrata in vigore. La prosecuzione dell'uso del servizio dopo l'entrata in vigore vale come accettazione; in caso contrario puoi recedere senza costi."
},
"contact": {
"heading": "Contatti",
"p1": "Per qualsiasi domanda su questi termini o per esercitare i tuoi diritti puoi scrivere a carlo.santoleri@gmail.com."
}
}
}
}
}
@@ -53,6 +53,26 @@ export default function SignInPage({ params: { locale } }: { params: { locale: s
),
})}
</p>
<p className="border-ink/15 text-ink-dim mt-10 border-t pt-5 text-xs leading-relaxed">
{t.rich('legalNote', {
terms: (chunks) => (
<Link
href="/legal/terms"
className="text-pomodoro hover:decoration-ink underline decoration-[1.5px] underline-offset-[4px]"
>
{chunks}
</Link>
),
privacy: (chunks) => (
<Link
href="/legal/privacy"
className="text-pomodoro hover:decoration-ink underline decoration-[1.5px] underline-offset-[4px]"
>
{chunks}
</Link>
),
})}
</p>
</section>
</main>
</div>
@@ -41,7 +41,24 @@ export default function SignUpPage({ params: { locale } }: { params: { locale: s
<p className="editorial-eyebrow mb-8">{t('title')}</p>
<SignUpForm googleEnabled={enabledSocialProviders.includes('google')} />
<p className="border-ink/15 text-ink-dim mt-10 border-t pt-5 text-xs leading-relaxed">
{t('disclaimer')}
{t.rich('disclaimer', {
terms: (chunks) => (
<Link
href="/legal/terms"
className="text-pomodoro hover:decoration-ink underline decoration-[1.5px] underline-offset-[4px]"
>
{chunks}
</Link>
),
privacy: (chunks) => (
<Link
href="/legal/privacy"
className="text-pomodoro hover:decoration-ink underline decoration-[1.5px] underline-offset-[4px]"
>
{chunks}
</Link>
),
})}
</p>
<p className="text-ink-dim mt-6 text-sm leading-relaxed md:hidden">
{t.rich('haveAccount', {
@@ -0,0 +1,181 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Masthead } from '@/components/masthead';
const LAST_UPDATED_ISO = '2026-05-20';
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string };
}): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Legal.Privacy' });
return {
title: t('metaTitle'),
description: t('metaDescription'),
robots: { index: true, follow: true },
};
}
export default function PrivacyPage({ params: { locale } }: { params: { locale: string } }) {
setRequestLocale(locale);
return <PrivacyContent />;
}
function PrivacyContent() {
const t = useTranslations('Legal.Privacy');
const tShared = useTranslations('Legal.Shared');
const lastUpdated = new Intl.DateTimeFormat('it-IT', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(`${LAST_UPDATED_ISO}T00:00:00`));
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="Capitolo X — Privacy" />
<main className="relative pb-24 pt-10 sm:pt-12">
<span
aria-hidden
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
>
X
</span>
<p className="editorial-eyebrow animate-fade-up">{t('eyebrow')}</p>
<h1 className="font-display text-mega bleed-left animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
<span className="text-ink block">
{t('title')}
<span className="text-pomodoro">.</span>
</span>
</h1>
<p className="font-display text-ink-soft animate-fade-up mt-8 max-w-3xl text-2xl italic leading-snug [animation-delay:240ms]">
{t('subtitle')}
</p>
<p className="text-ink-dim animate-fade-up mt-4 font-mono text-[11px] uppercase tracking-widest [animation-delay:300ms]">
{tShared('lastUpdated', { date: lastUpdated })}
</p>
<div className="rule animate-rule-in my-12 [animation-delay:360ms]" />
{/* TOC + corpo */}
<div className="grid gap-12 md:grid-cols-12">
<aside className="animate-fade-up [animation-delay:420ms] md:col-span-3">
<p className="editorial-eyebrow mb-4">{tShared('toc')}</p>
<ol className="space-y-2 font-mono text-[11px] uppercase tracking-widest">
{SECTIONS.map((s, i) => (
<li key={s.id}>
<Link
href={`#${s.id}`}
className="text-ink-soft hover:text-pomodoro flex gap-3 transition-colors"
>
<span className="text-ink-dim w-6 shrink-0">
{String(i + 1).padStart(2, '0')}
</span>
<span>{t(`sections.${s.id}.heading` as never)}</span>
</Link>
</li>
))}
</ol>
</aside>
<article className="animate-fade-up md:border-ink/15 [animation-delay:480ms] md:col-span-9 md:border-l md:pl-10">
{SECTIONS.map((s, i) => (
<Section key={s.id} id={s.id} index={i + 1} body={s.body} />
))}
</article>
</div>
<div className="rule animate-rule-in my-16" />
{/* Quick links bottom */}
<nav aria-label="Pagine collegate" className="grid grid-cols-12 gap-6 pt-2 text-sm">
<p className="text-ink-soft col-span-12 font-mono text-[11px] uppercase tracking-widest md:col-span-3">
{tShared('related')}
</p>
<ul className="col-span-12 flex flex-wrap gap-x-8 gap-y-3 font-mono text-[11px] uppercase tracking-widest md:col-span-9">
<li>
<Link
href="/legal/terms"
className="text-ink hover:text-pomodoro transition-colors"
>
→ {tShared('linkTerms')}
</Link>
</li>
<li>
<Link href="/" className="text-ink hover:text-pomodoro transition-colors">
← {tShared('linkHome')}
</Link>
</li>
</ul>
</nav>
</main>
<div className="rule-thick" />
</div>
</div>
);
}
interface SectionDef {
id:
| 'controller'
| 'dataCollected'
| 'healthData'
| 'purposes'
| 'legalBasis'
| 'recipients'
| 'transfers'
| 'retention'
| 'security'
| 'rights'
| 'minors'
| 'cookies'
| 'changes'
| 'contact';
/** Numero di paragrafi tradotti come `sections.<id>.p1`, `p2`, ... */
body: number;
}
const SECTIONS: ReadonlyArray<SectionDef> = [
{ id: 'controller', body: 2 },
{ id: 'dataCollected', body: 5 },
{ id: 'healthData', body: 2 },
{ id: 'purposes', body: 4 },
{ id: 'legalBasis', body: 3 },
{ id: 'recipients', body: 6 },
{ id: 'transfers', body: 2 },
{ id: 'retention', body: 3 },
{ id: 'security', body: 2 },
{ id: 'rights', body: 3 },
{ id: 'minors', body: 1 },
{ id: 'cookies', body: 2 },
{ id: 'changes', body: 1 },
{ id: 'contact', body: 1 },
];
function Section({ id, index, body }: { id: SectionDef['id']; index: number; body: number }) {
const t = useTranslations('Legal.Privacy');
return (
<section id={id} className="scroll-mt-24 pb-12 last:pb-0">
<div className="grid grid-cols-[3rem_1fr] items-baseline gap-4">
<span className="text-ink-dim font-mono text-xs uppercase tracking-widest">
{String(index).padStart(2, '0')}
</span>
<h2 className="font-display text-ink text-3xl font-medium leading-tight tracking-tight md:text-4xl">
{t(`sections.${id}.heading` as never)}
</h2>
</div>
<div className="mt-5 space-y-4 md:ml-[3rem]">
{Array.from({ length: body }).map((_, i) => (
<p key={i} className="font-display text-ink text-base leading-relaxed md:text-lg">
{t(`sections.${id}.p${i + 1}` as never)}
</p>
))}
</div>
</section>
);
}
@@ -0,0 +1,182 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Masthead } from '@/components/masthead';
const LAST_UPDATED_ISO = '2026-05-20';
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string };
}): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'Legal.Terms' });
return {
title: t('metaTitle'),
description: t('metaDescription'),
robots: { index: true, follow: true },
};
}
export default function TermsPage({ params: { locale } }: { params: { locale: string } }) {
setRequestLocale(locale);
return <TermsContent />;
}
function TermsContent() {
const t = useTranslations('Legal.Terms');
const tShared = useTranslations('Legal.Shared');
const lastUpdated = new Intl.DateTimeFormat('it-IT', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(new Date(`${LAST_UPDATED_ISO}T00:00:00`));
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="Capitolo XI — Termini di servizio" />
<main className="relative pb-24 pt-10 sm:pt-12">
<span
aria-hidden
className="font-display text-stroke-thin text-chapter pointer-events-none absolute -right-4 -top-12 select-none italic"
>
XI
</span>
<p className="editorial-eyebrow animate-fade-up">{t('eyebrow')}</p>
<h1 className="font-display text-mega bleed-left animate-fade-up mt-3 font-medium leading-[0.95] tracking-tight [animation-delay:120ms]">
<span className="text-ink block">
{t('title')}
<span className="text-pomodoro">.</span>
</span>
</h1>
<p className="font-display text-ink-soft animate-fade-up mt-8 max-w-3xl text-2xl italic leading-snug [animation-delay:240ms]">
{t('subtitle')}
</p>
<p className="text-ink-dim animate-fade-up mt-4 font-mono text-[11px] uppercase tracking-widest [animation-delay:300ms]">
{tShared('lastUpdated', { date: lastUpdated })}
</p>
<div className="rule animate-rule-in my-12 [animation-delay:360ms]" />
<div className="grid gap-12 md:grid-cols-12">
<aside className="animate-fade-up [animation-delay:420ms] md:col-span-3">
<p className="editorial-eyebrow mb-4">{tShared('toc')}</p>
<ol className="space-y-2 font-mono text-[11px] uppercase tracking-widest">
{SECTIONS.map((s, i) => (
<li key={s.id}>
<Link
href={`#${s.id}`}
className="text-ink-soft hover:text-pomodoro flex gap-3 transition-colors"
>
<span className="text-ink-dim w-6 shrink-0">
{String(i + 1).padStart(2, '0')}
</span>
<span>{t(`sections.${s.id}.heading` as never)}</span>
</Link>
</li>
))}
</ol>
</aside>
<article className="animate-fade-up md:border-ink/15 [animation-delay:480ms] md:col-span-9 md:border-l md:pl-10">
{SECTIONS.map((s, i) => (
<Section key={s.id} id={s.id} index={i + 1} body={s.body} />
))}
</article>
</div>
<div className="rule animate-rule-in my-16" />
<nav aria-label="Pagine collegate" className="grid grid-cols-12 gap-6 pt-2 text-sm">
<p className="text-ink-soft col-span-12 font-mono text-[11px] uppercase tracking-widest md:col-span-3">
{tShared('related')}
</p>
<ul className="col-span-12 flex flex-wrap gap-x-8 gap-y-3 font-mono text-[11px] uppercase tracking-widest md:col-span-9">
<li>
<Link
href="/legal/privacy"
className="text-ink hover:text-pomodoro transition-colors"
>
→ {tShared('linkPrivacy')}
</Link>
</li>
<li>
<Link href="/" className="text-ink hover:text-pomodoro transition-colors">
← {tShared('linkHome')}
</Link>
</li>
</ul>
</nav>
</main>
<div className="rule-thick" />
</div>
</div>
);
}
interface SectionDef {
id:
| 'object'
| 'noMedicalAdvice'
| 'account'
| 'trialAndSubscription'
| 'paymentAndBilling'
| 'withdrawal'
| 'userObligations'
| 'ip'
| 'userContent'
| 'serviceChanges'
| 'termination'
| 'liability'
| 'forceMajeure'
| 'governingLaw'
| 'changes'
| 'contact';
body: number;
}
const SECTIONS: ReadonlyArray<SectionDef> = [
{ id: 'object', body: 2 },
{ id: 'noMedicalAdvice', body: 3 },
{ id: 'account', body: 3 },
{ id: 'trialAndSubscription', body: 4 },
{ id: 'paymentAndBilling', body: 3 },
{ id: 'withdrawal', body: 3 },
{ id: 'userObligations', body: 4 },
{ id: 'ip', body: 2 },
{ id: 'userContent', body: 2 },
{ id: 'serviceChanges', body: 2 },
{ id: 'termination', body: 2 },
{ id: 'liability', body: 3 },
{ id: 'forceMajeure', body: 1 },
{ id: 'governingLaw', body: 2 },
{ id: 'changes', body: 1 },
{ id: 'contact', body: 1 },
];
function Section({ id, index, body }: { id: SectionDef['id']; index: number; body: number }) {
const t = useTranslations('Legal.Terms');
return (
<section id={id} className="scroll-mt-24 pb-12 last:pb-0">
<div className="grid grid-cols-[3rem_1fr] items-baseline gap-4">
<span className="text-ink-dim font-mono text-xs uppercase tracking-widest">
{String(index).padStart(2, '0')}
</span>
<h2 className="font-display text-ink text-3xl font-medium leading-tight tracking-tight md:text-4xl">
{t(`sections.${id}.heading` as never)}
</h2>
</div>
<div className="mt-5 space-y-4 md:ml-[3rem]">
{Array.from({ length: body }).map((_, i) => (
<p key={i} className="font-display text-ink text-base leading-relaxed md:text-lg">
{t(`sections.${id}.p${i + 1}` as never)}
</p>
))}
</div>
</section>
);
}
+17 -3
View File
@@ -806,9 +806,23 @@ function Disclaimer() {
<p className="font-display text-ink-soft col-span-12 text-base italic leading-relaxed md:col-span-7 md:col-start-2">
{t('disclaimer')}
</p>
<p className="text-ink-dim col-span-12 font-mono text-[10px] uppercase tracking-widest md:col-span-3 md:col-start-10 md:text-right">
{t('footerCopyright')}
</p>
<div className="col-span-12 flex flex-col gap-2 md:col-span-3 md:col-start-10 md:items-end">
<ul className="flex gap-5 font-mono text-[10px] uppercase tracking-widest">
<li>
<Link href="/legal/privacy" className="text-ink hover:text-pomodoro transition-colors">
{t('footerLinkPrivacy')}
</Link>
</li>
<li>
<Link href="/legal/terms" className="text-ink hover:text-pomodoro transition-colors">
{t('footerLinkTerms')}
</Link>
</li>
</ul>
<p className="text-ink-dim font-mono text-[10px] uppercase tracking-widest">
{t('footerCopyright')}
</p>
</div>
</footer>
);
}
@@ -1,4 +1,5 @@
import { prisma } from '@ketopath/db';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server';
@@ -64,6 +65,24 @@ function WelcomePageContent() {
<p className="font-display text-ink-dim mt-5 text-sm italic leading-relaxed">
{t('legalNote')}
</p>
<ul className="mt-4 flex flex-wrap gap-x-6 gap-y-2 font-mono text-[11px] uppercase tracking-widest">
<li>
<Link
href="/legal/privacy"
className="text-ink-soft hover:text-pomodoro transition-colors"
>
→ Informativa Privacy
</Link>
</li>
<li>
<Link
href="/legal/terms"
className="text-ink-soft hover:text-pomodoro transition-colors"
>
→ Termini di servizio
</Link>
</li>
</ul>
</main>
</div>
);
+8 -1
View File
@@ -1,5 +1,6 @@
'use client';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
@@ -34,7 +35,13 @@ export function CookieBanner() {
<div className="mx-auto flex max-w-5xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="font-display text-ink-soft text-sm italic leading-snug">
<span className="editorial-eyebrow mr-2 not-italic">Privacy</span>
{t('message')}
{t('message')}{' '}
<Link
href="/legal/privacy"
className="text-pomodoro hover:decoration-ink underline decoration-[1.5px] underline-offset-[4px]"
>
{t('learnMore')}
</Link>
</p>
<Button type="button" size="sm" variant="outline" onClick={dismiss}>
{t('dismiss')}