Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ server/public
vite.config.ts.*
*.tar.gz
.env
.dev.vars
*.pem
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Questo progetto è sotto licenza MIT. Consulta il file `LICENSE` per maggiori in

## 📞 Contatti

PrimeBuild PC - [primebuild.official@gmail.com](mailto:primebuild.official@gmail.com)
PrimeBuild PC - [preventivi@primebuild.website](mailto:preventivi@primebuild.website)

Project Link: [https://github.com/PrimeBuild-pc/webapp](https://github.com/PrimeBuild-pc/webapp)

Expand Down
2 changes: 1 addition & 1 deletion client/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- [Chi siamo](https://primebuild.website/chi-siamo/)
- Sito ufficiale: https://primebuild.website/
- Sede: Montegrotto Terme, 35036 (PD), Italia
- Email: primebuild.official@gmail.com
- Email: preventivi@primebuild.website
- Instagram: https://www.instagram.com/prime_build_/
- GitHub: https://github.com/PrimeBuild-pc
- Discord: https://discord.gg/ERUwSxE79q
Expand Down
15 changes: 10 additions & 5 deletions client/src/components/ContactSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ const ContactSection = () => {
trackFormSubmit('contact', true);
setFormData(initialState);
resetTurnstile();
toast({ title: "Messaggio inviato", description: "Ti contatteremo al piu presto." });
toast({
title: "Richiesta ricevuta",
description: json.confirmationSent === false
? "La conferma automatica non è partita: non inviare di nuovo, abbiamo comunque ricevuto il messaggio."
: "Controlla la tua email per il prossimo passo e il questionario preventivo.",
});
} catch (err) {
console.error('Network/contact error', err);
trackFormSubmit('contact', false);
Expand All @@ -157,8 +162,8 @@ const ContactSection = () => {

// Obfuscate email to prevent spam harvesters
const getEmail = () => {
const user = "primebuild.official";
const domain = "gmail.com";
const user = "preventivi";
const domain = "primebuild.website";
return `${user}@${domain}`;
};

Expand Down Expand Up @@ -198,7 +203,7 @@ const ContactSection = () => {
<SectionHeader
title="Parliamo del"
highlight="tuo prossimo PC"
subtitle="Raccontaci budget, giochi, software e obiettivi. Ti rispondiamo con una proposta chiara e personalizzata."
subtitle="Inviaci un primo contatto. Riceverai via email il questionario per qualificare la richiesta e scegliere il livello di preventivo."
alignment="left"
/>

Expand Down Expand Up @@ -289,7 +294,7 @@ const ContactSection = () => {
disabled={isSubmitting}
className="button-primary disabled:cursor-not-allowed disabled:opacity-50"
>
{isSubmitting ? "Invio in corso..." : "Invia Messaggio"}
{isSubmitting ? "Invio in corso..." : "Invia richiesta"}
</button>
<p className="text-xs text-neutral-400">
Inviando questo modulo, dichiari di aver letto la nostra{" "}
Expand Down
2 changes: 1 addition & 1 deletion client/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const buildSchema = (location: string) => {
url: `${SITE_URL}/`,
logo: { "@type": "ImageObject", url: assetUrl("/logo.png"), width: 302, height: 302 },
image: assetUrl("/og-image.png"),
email: "primebuild.official@gmail.com",
email: "preventivi@primebuild.website",
description: "Assemblaggio PC gaming su misura, assistenza tecnica e ottimizzazione a Montegrotto Terme, Padova e in tutta Italia.",
address: {
"@type": "PostalAddress",
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const About = () => {
<h2 className="text-3xl font-bold">Identità e contatti ufficiali</h2>
<ul className="mt-5 space-y-3 text-neutral-300">
<li>Sede operativa: Montegrotto Terme, 35036 (PD), Italia</li>
<li>Email: <a className="text-primary underline" href="mailto:primebuild.official@gmail.com">primebuild.official@gmail.com</a></li>
<li>Email: <a className="text-primary underline" href="mailto:preventivi@primebuild.website">preventivi@primebuild.website</a></li>
<li><a className="text-primary underline" href="https://www.instagram.com/prime_build_/" target="_blank" rel="noopener noreferrer">Instagram</a></li>
<li><a className="text-primary underline" href="https://github.com/PrimeBuild-pc" target="_blank" rel="noopener noreferrer">GitHub</a></li>
<li><a className="text-primary underline" href="https://discord.gg/ERUwSxE79q" target="_blank" rel="noopener noreferrer">Community Discord</a></li>
Expand Down
21 changes: 12 additions & 9 deletions client/src/pages/Privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Privacy = () => {
<div className="prose prose-invert prose-neutral max-w-none space-y-8">
<section>
<p className="text-neutral-300 text-sm">
Ultimo aggiornamento: 7 Dicembre 2025
Ultimo aggiornamento: 24 luglio 2026
</p>
</section>

Expand All @@ -39,7 +39,7 @@ const Privacy = () => {
Il titolare del trattamento dei dati personali e Prime Build, con sede a Padova, Italia.
</p>
<p className="text-neutral-300">
Email di contatto: <a href="mailto:primebuild.official@gmail.com" className="text-primary hover:underline">primebuild.official@gmail.com</a>
Email di contatto: <a href="mailto:preventivi@primebuild.website" className="text-primary hover:underline">preventivi@primebuild.website</a>
</p>
</section>

Expand All @@ -53,6 +53,7 @@ const Privacy = () => {
<li>Nome</li>
<li>Indirizzo email</li>
<li>Oggetto e contenuto del messaggio</li>
<li>Informazioni tecniche e di budget fornite nel questionario preventivo</li>
</ul>

<h3 className="text-xl font-medium text-white mb-2 mt-6">2.2 Dati raccolti automaticamente (con consenso)</h3>
Expand All @@ -75,7 +76,8 @@ const Privacy = () => {
<h2 className="text-2xl font-semibold text-white mb-4">3. Finalita del Trattamento</h2>
<p className="text-neutral-300 mb-4">I tuoi dati vengono utilizzati per:</p>
<ul className="list-disc list-inside text-neutral-300 space-y-1 ml-4">
<li><strong>Rispondere alle richieste</strong> inviate tramite il modulo di contatto</li>
<li><strong>Rispondere e inviare conferme operative</strong> per le richieste trasmesse tramite il modulo</li>
<li><strong>Qualificare le richieste di preventivo</strong> e preparare la stima scelta dall'utente</li>
<li><strong>Migliorare il sito web</strong> analizzando statistiche anonime di utilizzo</li>
<li><strong>Garantire la sicurezza</strong> del sito prevenendo abusi e spam</li>
</ul>
Expand Down Expand Up @@ -112,15 +114,16 @@ const Privacy = () => {
I tuoi dati non vengono venduti a terzi. Potrebbero essere condivisi solo con:
</p>
<ul className="list-disc list-inside text-neutral-300 space-y-1 ml-4 mt-4">
<li><strong>Google LLC</strong> - Per l'elaborazione delle statistiche Analytics (soggetto alle loro policy privacy)</li>
<li><strong>Cloudflare Inc.</strong> - Per l'hosting e la protezione del sito</li>
<li><strong>Google LLC</strong> - Per Google Analytics e, se compilato, per il questionario Google Forms</li>
<li><strong>Cloudflare Inc.</strong> - Per hosting, protezione anti-spam e gestione tecnica delle richieste</li>
<li><strong>Resend</strong> - Per l'invio delle email operative e delle conferme automatiche</li>
</ul>
</section>

<section>
<h2 className="text-2xl font-semibold text-white mb-4">6. Conservazione dei Dati</h2>
<ul className="list-disc list-inside text-neutral-300 space-y-1 ml-4">
<li>I messaggi inviati tramite il form di contatto vengono conservati per il tempo necessario a evadere la richiesta</li>
<li>I messaggi e le risposte al questionario vengono conservati per il tempo necessario a valutare ed evadere la richiesta</li>
<li>I dati di Google Analytics vengono conservati per 14 mesi, poi automaticamente eliminati</li>
</ul>
</section>
Expand All @@ -138,8 +141,8 @@ const Privacy = () => {
</ul>
<p className="text-neutral-300 mt-4">
Per esercitare questi diritti, contattaci a{" "}
<a href="mailto:primebuild.official@gmail.com" className="text-primary hover:underline">
primebuild.official@gmail.com
<a href="mailto:preventivi@primebuild.website" className="text-primary hover:underline">
preventivi@primebuild.website
</a>
</p>
</section>
Expand Down Expand Up @@ -169,7 +172,7 @@ const Privacy = () => {
puoi contattarci a:
</p>
<ul className="list-none text-neutral-300 space-y-1 mt-4">
<li>Email: <a href="mailto:primebuild.official@gmail.com" className="text-primary hover:underline">primebuild.official@gmail.com</a></li>
<li>Email: <a href="mailto:preventivi@primebuild.website" className="text-primary hover:underline">preventivi@primebuild.website</a></li>
<li>Instagram: <a href="https://www.instagram.com/prime_build_/" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">@prime_build_</a></li>
<li>Discord: <a href="https://discord.gg/ERUwSxE79q" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Prime Build Community</a></li>
</ul>
Expand Down
171 changes: 171 additions & 0 deletions docs/email-flow-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Flusso email professionale Prime Build

## Architettura scelta

```text
Modulo sito + Turnstile
|
v
Cloudflare Pages Function /contact
|-- Resend --> [PRIMO CONTATTO] alla casella interna
`-- Resend --> conferma al cliente + link Google Form
|
v
Google Form di qualificazione
|
v
notifica/risposta qualificata
```

- **Indirizzo pubblico e Reply-To:** `preventivi@primebuild.website`.
- **Destinazione reale:** la casella Gmail esistente, nascosta dietro Cloudflare Email Routing.
- **Mittente automatico:** `Prime Build Preventivi <preventivi@primebuild.website>`.
- Non usare `no-reply@`: impedisce una risposta naturale e comunica distanza proprio quando il cliente deve fidarsi.
- `info@` è generico e riceve più spam; `supporto@` va riservato in futuro ai clienti con un servizio già attivo.

Il primo messaggio resta disponibile, ma arriva con oggetto `[PRIMO CONTATTO]`: in Gmail può essere archiviato automaticamente sotto l'etichetta `Da qualificare`. La posta prioritaria sarà quindi la notifica di una risposta completa al Google Form.

## Perché il Google Form va nella prima risposta automatica

Il link inviato per email aggiunge un piccolo impegno e verifica che l'indirizzo sia raggiungibile. Chi compila il secondo passaggio ha già letto differenze e prezzo dei due preventivi. Non mostrare subito il link sul sito: annullerebbe questo filtro.

Il form dovrebbe richiedere almeno:

1. email usata nel primo contatto;
2. servizio scelto: Base gratuito oppure Completo €25;
3. budget massimo e indicazione se include monitor/periferiche;
4. giochi, software e utilizzo principale;
5. risoluzione e refresh rate desiderati;
6. componenti già posseduti;
7. preferenze o vincoli (dimensioni, rumore, Wi-Fi, estetica);
8. tempistiche;
9. acquisto/assemblaggio autonomo oppure servizio Prime Build;
10. conferma di aver compreso cosa include il livello scelto.

Evita upload di file e domande non necessarie. Non richiedere l'accesso Google: l'apertura del link dalla casella email è già un filtro sufficiente e il login farebbe perdere lead validi.

## Setup manuale Cloudflare Email Routing

Email Routing risulta già attivo sul dominio: sono presenti gli MX Cloudflare e l'SPF `include:_spf.mx.cloudflare.net`. Non eliminarli.

1. Apri **Cloudflare → Email → Email Routing → Destination addresses**.
2. Verifica la casella Gmail di destinazione, se non è già verificata.
3. In **Routing rules → Custom addresses**, crea:
- `preventivi@primebuild.website` → Gmail esistente;
- facoltativo: `dmarc@primebuild.website` → Gmail esistente, per ricevere i report DMARC.
4. Invia un messaggio esterno a `preventivi@primebuild.website` e verifica ricezione e risposta.
5. Non attivare un catch-all: aumenta lo spam senza migliorare il servizio.

### Regola legacy `no-reply@`

`no-reply@primebuild.website` (con trattino) non compare nel codice né negli altri repository Prime Build. La versione precedente del sito usava soltanto `noreply@primebuild.website` (senza trattino) come mittente in uscita, e questa PR la sostituisce con `preventivi@primebuild.website`. Una regola Email Routing non è necessaria per spedire: serve solo a ricevere risposte. Si può quindi disattivare la regola `no-reply@`, attendere 7–14 giorni e poi eliminarla se non emergono messaggi inattesi.

Non eliminare invece i record DNS MX/TXT di `noreply.primebuild.website`: sono il Return-Path tecnico verificato di Resend e non rappresentano una casella o una regola di inoltro.

## Setup manuale Resend e DNS

Sono già presenti il DKIM `resend._domainkey.primebuild.website` e il Return-Path tecnico `noreply.primebuild.website` con SPF/MX Amazon SES. Devono restare invariati e il dominio deve risultare **Verified** nella dashboard Resend. La creazione dell'indirizzo inoltrato `dmarc@primebuild.website` non crea il record DNS DMARC: sono due configurazioni distinte.

1. Apri **Resend → Domains** e aggiungi o seleziona `primebuild.website`.
2. Copia in **Cloudflare → DNS** esattamente i record DKIM, SPF e MX mostrati da Resend.
3. Non sostituire gli MX Cloudflare all'apice e non creare un secondo SPF all'apice. In questa configurazione Resend usa `noreply.primebuild.website` come Return-Path, separato dall'indirizzo `no-reply@primebuild.website` e dall'SPF di Email Routing.
4. Lascia i record email in modalità **DNS only** quando applicabile.
5. Premi **Verify DNS Records** in Resend e attendi lo stato `Verified`.
6. Aggiungi il record iniziale:

```text
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=none; rua=mailto:dmarc@primebuild.website; adkim=r; aspf=r; pct=100
```

Dopo almeno 2–4 settimane di report senza mittenti legittimi non allineati, valuta `p=quarantine`. Non partire direttamente con `reject`.
7. In **Resend → API Keys**, crea una chiave con sola autorizzazione di invio. Copiala una volta e non inserirla nel repository.
8. Lascia disattivato il tracking di aperture/click salvo reale necessità: riduce trattamento dati e complessità privacy.

## Variabili Cloudflare Pages

Apri **Cloudflare → Workers & Pages → website → Settings → Variables and Secrets** e configura Production (e Preview solo per i test):

| Nome | Tipo | Valore |
|---|---|---|
| `RESEND_API_KEY` | Secret | `re_...` |
| `QUALIFICATION_FORM_URL` | Variable | URL HTTPS completo del Google Form |
| `TURNSTILE_SECRET_KEY` | Secret | chiave Turnstile già in uso |
| `MAIL_TO` | Variable/Secret | casella Gmail interna |
| `MAIL_FROM` | Variable | `preventivi@primebuild.website` |
| `MAIL_REPLY_TO` | Variable | `preventivi@primebuild.website` |
| `EMAIL_STRICT` | Variable | `1` |
| `ALLOWED_ORIGINS` | Variable | `https://primebuild.website` |

La chiave Resend deve esistere soltanto lato Cloudflare. Non usare mai un nome `VITE_RESEND_API_KEY`: le variabili `VITE_*` finiscono nel JavaScript pubblico.

Alternativa CLI per i secret:

```bash
npx wrangler pages secret put RESEND_API_KEY --project-name website
npx wrangler pages secret put TURNSTILE_SECRET_KEY --project-name website
```

Per sviluppo locale crea `.dev.vars` (ignorato da Git):

```dotenv
RESEND_API_KEY=re_xxx
TURNSTILE_SECRET_KEY=xxx
QUALIFICATION_FORM_URL=https://docs.google.com/forms/d/e/xxx/viewform
MAIL_TO=casella-interna@example.com
MAIL_FROM=preventivi@primebuild.website
MAIL_REPLY_TO=preventivi@primebuild.website
```

Dopo ogni variazione alle impostazioni, avvia un nuovo deploy Cloudflare.

## Configurazione Google Form e Gmail

1. Collega il Google Form a un Google Sheet.
2. Attiva **Ricevi notifiche email per le nuove risposte** dal menu delle risposte/Sheet.
3. Nel testo finale del Form usa:

> Grazie, la richiesta è completa. Il team Prime Build potrà ora valutarla. L'invio del questionario non comporta acquisti automatici; eventuali passaggi successivi saranno comunicati via email.

4. In Gmail crea un filtro per gli oggetti che contengono `[PRIMO CONTATTO]`: applica l'etichetta `Da qualificare` e, se desiderato, ignora la Posta in arrivo.
5. Lascia in Posta in arrivo le notifiche del Google Form: saranno i lead che hanno completato il percorso.

## Copy della conferma automatica

**Oggetto:** `Abbiamo ricevuto la tua richiesta | Prime Build`

> Ciao {{nome}},
>
> abbiamo ricevuto il tuo primo contatto.
>
> Per preparare una proposta adatta alle tue esigenze, completa il questionario: ci aiuterà a valutare budget, utilizzo, giochi o software, risoluzione, componenti già disponibili, preferenze e tempistiche.
>
> **Preventivo Base — gratuito**
> Include l'analisi delle tue esigenze, una stima del costo complessivo e l'indicazione della fascia di configurazione più adatta. È sufficiente per capire che tipo di PC e quale livello di prestazioni potresti ricevere affidando a Prime Build la realizzazione e l'assemblaggio.
>
> **Preventivo Completo — €25**
> Include la stessa analisi, con in più il modello esatto di ogni componente, i link di acquisto, i prezzi consigliati rilevati al momento della ricerca e una checklist completa di compatibilità. È pensato per chi vuole poter acquistare e assemblare la build in autonomia.
>
> Se dopo aver acquistato il Preventivo Completo decidi di affidare comunque a Prime Build l'assemblaggio, i €25 già pagati verranno sottratti dal costo della manodopera.
>
> I preventivi riflettono prezzi e disponibilità presenti nel momento in cui vengono preparati. Il mercato dei componenti può cambiare rapidamente e Prime Build non vende direttamente i singoli componenti. Se rileviamo un prezzo particolarmente alto, te lo comunichiamo con trasparenza e, quando ci sono indicazioni concrete di un possibile calo a breve, possiamo consigliarti di attendere.
>
> Se ci hai contattato per assistenza relativa a un servizio già in corso, puoi rispondere direttamente a questa email senza compilare il questionario.
>
> Team Prime Build

Il codice in `functions/contact.js` genera sia HTML sia testo semplice e inserisce il pulsante verso `QUALIFICATION_FORM_URL`.

## Test prima del merge

1. Configura routing, DNS, API key e URL del Form.
2. Invia il modulo con una casella diversa da quella interna.
3. Verifica:
- Turnstile blocca invii privi di token;
- arriva la notifica interna con Reply-To del cliente;
- il cliente riceve HTML e testo semplice;
- il pulsante apre il Form corretto;
- rispondendo alla conferma si raggiunge `preventivi@primebuild.website`;
- SPF, DKIM e DMARC risultano `PASS` nelle intestazioni del messaggio.
4. In Cloudflare crea facoltativamente una regola di rate limiting su `POST /contact` (per esempio 5 richieste ogni 10 minuti per IP), senza sostituire Turnstile.
Loading
Loading