Compare commits

..

30 Commits

Author SHA1 Message Date
f71d1b327e Merge pull request 'fix(nav): Service Commande dans le menu mobile (6 pages)' (#21) from fix/mobile-nav-service-commande into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Failing after 2m2s
2026-06-22 10:48:15 +03:00
Serge RAKOTO HARRY-NAIVO
616f9d75a1 fix(nav): ajouter « Service Commande » au menu mobile (6 pages)
Le menu mobile (<nav class="mobile-nav">) omettait le lien « Service Commande »
sur about, application, cgv, contact, guide-envoi et tarifs — incoherent avec le
menu desktop et le footer (qui l'ont) et avec accueil (mobile complet). Insertion
du lien apres « Tarifs », identique aux autres entrees.

Constat issu de l'audit du site (revue qualite 2026-06-21).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:09:18 +02:00
499109f8a9 Merge pull request 'chore(site): cache-bust translations.js' (#20) from chore/cache-bust-translations into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-06-03 12:32:21 +03:00
Serge RAKOTO HARRY-NAIVO
43e576249c chore(site): cache-bust translations.js (?v=20260603)
translations.js a Cache-Control max-age=7j sans versioning -> les visiteurs
gardaient l'ancien menu en cache. Cache-buster pour rendre le renommage
Contact->Inscription visible immédiatement par tous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:32:13 +02:00
5399fc1a40 Merge pull request 'feat(site): Contact -> Inscription (menu + page)' (#19) from fix/contact-to-inscription into main
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-06-03 12:21:47 +03:00
Serge RAKOTO HARRY-NAIVO
61a2a67b00 feat(site): renomme "Contact" en "Inscription" (menu + page contact)
- Menu : "Contact" -> "Inscription" (FR) / "Sign Up" (EN) / "Fisoratana anarana" (MG)
- Page : titre "Contactez-Nous" -> "Inscrivez-vous"
- Sous-titre : retire "Inscrivez-vous et" -> "Commencez à envoyer vos colis dès aujourd'hui"
- i18n (translations.js, 3 langues) + fallback HTML en dur sur toutes les pages
- footer "Contact" (coordonnées) laissé inchangé

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:21:15 +02:00
2a2ea2f5e4 Merge pull request 'fix: relayer token_hash vers le deep link reset password' (#18) from fix/reset-password-token-hash into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-05-30 06:04:51 +03:00
Serge RAKOTO HARRY-NAIVO
df8bf01759 fix: relayer token_hash vers le deep link reset password
Le nouveau flux reset (Edge Function mva-password-reset, deploye 2026-05-30)
envoie un lien ?token_hash=HASH&type=recovery au lieu de ?token=. La page-relais
lit desormais token_hash + type et construit le deep link
mvaglobalfret://reset-password?token_hash=...&type=... que l'app consomme via
verifyOtp({ token_hash, type }). Si token_hash est absent, on affiche un message
d'erreur (lien invalide) au lieu de tenter le redirect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 04:49:51 +02:00
ee17b2b48c Merge pull request 'feat(guide): add armes � feu + substances ill�gales aux articles interdits' (#16) from feat/add-firearms-illegal-substances into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-05-22 02:02:45 +03:00
Serge RAKOTO HARRY-NAIVO
84b4b7753e feat(guide): add armes à feu in Explosifs + substances illégales in Substances toxiques
Demande Serge — clarifier les articles interdits sur mva-globalfret.com/guide-envoi.html.

Changes :
- guide-envoi.html : append 'armes à feu' à cat1Desc + 'substances illégales' à cat6Desc (visible immédiat sans JS i18n).
- js/translations.js : sync les 3 langues FR/EN/MG sur cat1Desc + cat6Desc.

FR : armes à feu / substances illégales
EN : firearms / illegal substances
MG : basy / zavatra tsy ara-dalàna

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 01:02:14 +02:00
b6b492f224 Merge pull request 'chore: remove decommissioned cloudflare-worker' (#15) from chore/remove-decommissioned-cloudflare-worker into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-05-16 17:41:54 +03:00
Serge RAKOTO HARRY-NAIVO
32c7d65698 chore: remove decommissioned cloudflare-worker
Le worker hubspot-proxy.js a ete decommissione le 2026-05-10 :
migration HubSpot CRM -> mva-api Fastify + Postgres + Resend (= mention
explicite dans js/form-handler.js).

Le dossier cloudflare-worker/ (DEPLOIEMENT.md + hubspot-proxy.js + wrangler.toml)
n'est plus utilise par le frontend mais traine dans le repo. Cleanup.

Refs:
- js/form-handler.js commentaire 'Migration 2026-05-10 : remplace l ancien
  Cloudflare Worker mva-hubspot-proxy.sergemind4s.workers.dev (= decommissionne)
  par les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts.'
- Audit hygiene M4S 2026-05-16
2026-05-16 16:39:22 +02:00
24104ac9f4 Merge pull request 'fix(site): setup-password token length 64 -> 96' (#14) from fix/setup-token-length-96 into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-05-11 00:14:37 +03:00
Serge RAKOTO HARRY-NAIVO
a6d219453c fix(site): setup-password.html validation token 64 -> 96 chars
Bug compagnon de api PR #57 : crypto.randomBytes(48).toString hex = 96
caracteres, pas 64. La validation JS cote site rejetait tous les vrais
tokens avec 'Lien invalide ou incomplet'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:14:30 +02:00
2878e8e01a Merge pull request 'feat(site): page setup-password.html (bridge lead -> user)' (#13) from feat/setup-password-page into main
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-10 23:31:46 +03:00
Serge RAKOTO HARRY-NAIVO
af58c04776 feat(site): page setup-password.html pour bridge lead -> user
Compagnon de api PR #55 (bridge lead -> user).

Flow:
- Lead clique email setup compte
- Arrive sur /setup-password.html?token=XXX
- JS POST /auth/lookup-setup-token pre-remplit email + firstname + ref
- Form 2 password avec validation (min 8 majuscule chiffre)
- Submit POST /auth/setup-from-lead -> user cree + JWT
- Affiche success + lien vers application.html pour download app

Design: card centree, MVA gold/navy, mobile responsive, vanilla JS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:31:36 +02:00
61397720e8 Merge pull request 'feat(api): migrate Worker mva-hubspot-proxy → mva-api /leads/* routes' (#12) from feat/migrate-worker-to-mva-api into main
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-10 14:02:18 +03:00
Serge RAKOTO HARRY-NAIVO
7217f12bd2 feat(api): migrate Worker mva-hubspot-proxy → mva-api /leads/* routes
Le Cloudflare Worker hubspot-proxy était une solution temporaire car
le panel admin AdminJS n'était pas encore disponible. Maintenant qu'
AdminJS marche, on rapatrie tout le flow inscription côté MVA backend.

## Changements

- `js/form-handler.js`
  - WORKER_PROXY_URL → API_BASE_URL = https://api.mva.mind4solutions.com
  - checkExistingContact : POST /leads/check-email
    (response shape : {exists, firstname, reference_client})
  - setupContactForm : POST /leads/request-verification
  - sendWelcomeBackEmail : POST /leads/welcome-back

- `js/confirmation.js`
  - WORKER_PROXY_URL → API_BASE_URL
  - POST /leads/verify-token (= au lieu de Worker action verifyToken)
  - Detection token expiré/invalide via code INVALID_OR_EXPIRED

## Aucun changement HTML

Les forms HTML, IDs des éléments, validation côté client, gestion
Turnstile sont tous inchangés. Seules les URLs API changent.

## Côté backend (PR #44 mva-prestige-v2)

Les routes mva-api /leads/* sont déployées séparément avec :
- Validation Zod + Turnstile + rate limit
- DB Postgres (table leads + leads_pending) remplace HubSpot Contacts + KV
- Resend pour les emails (= unification écosystème M4S)
- AdminJS Resource leads pour Mélissa CRUD

## Cutover

Cette PR doit être merged + déployée APRÈS la migration backend
(PR #44) + après le run du script migrate-hubspot-to-postgres.js
(= les 4 contacts HubSpot existants en DB).
2026-05-10 11:34:07 +02:00
bc919b07e0 Merge pull request 'fix(reset-password): redirect to mvaglobalfret:// custom scheme' (#11) from fix/reset-password-deep-link-mva into main
Some checks failed
Deploy site to GitHub Pages / deploy (push) Has been cancelled
2026-05-07 22:31:54 +03:00
Serge RAKOTO HARRY-NAIVO
605fa63f70 fix(reset-password): redirect to mvaglobalfret:// custom scheme for mva-api tokens
Le bridge HTML redirige maintenant vers le custom scheme natif
mvaglobalfret://reset-password?token=... au lieu de
https://auth.mind4solutions.com/reset-password.

Le flow Supabase (auth.m4s.com / Phase 2.1) ne sait pas valider les
tokens UUID custom émis par mva-api (Fastify). Sans ce fix, les emails
reset password Cluster A B2 atterriraient sur une page d'erreur
"Lien invalide".

UI fallback HTML conservée + bouton CTA stylé pour les cas où le
deep-link automatique ne déclenche pas l'app (browser desktop, app
non installée).

Fixes Cluster A B2 blocker (= session 2026-05-07 MVA app).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:27:59 +02:00
2774c25a61 chore: post-review cleanup (3 Important + dead code purge -116 lines) (#10)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 18:37:10 +03:00
e14b0ff01a fix(worker): restore original Ravis de vous revoir email template (#9)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 17:07:36 +03:00
5c34e59a8d feat(worker): re-enable welcome-back email via Worker + Resend (footer 2026) (#8)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 16:43:58 +03:00
db43583a62 fix(js): animate-on-scroll threshold 0.1 ? 0 (mobile cgv + politique invisible bug) (#7)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 16:37:39 +03:00
5b84e5697e chore(post-cutover): fix 3 polish bugs (welcome-back, mobile lang switcher, animate-on-scroll) (#6)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 16:36:28 +03:00
5f88891a83 fix(worker): replace Forms API submission with CRM API direct (#5)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 16:10:11 +03:00
10960e8ae1 feat(cloudflare): point HTML/JS to new Mind4Solutions Worker + Turnstile sitekey (#4)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 15:28:23 +03:00
483195711e refactor(worker): Brevo+EmailJS ? Resend + remove cron + fix github.io URLs (#3)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 15:27:41 +03:00
2d3526da06 feat(seo): sitemap + robots.txt + mobile reset-password bridge (#2)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 01:04:05 +03:00
c973b67ec9 feat(images): self-host hero photos from Unsplash (#1)
Some checks are pending
Deploy site to GitHub Pages / deploy (push) Waiting to run
2026-05-07 01:01:54 +03:00
22 changed files with 614 additions and 1200 deletions

View File

@ -27,7 +27,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -47,14 +47,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -213,7 +209,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -239,7 +235,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -31,7 +31,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -54,13 +54,8 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -235,7 +230,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
<li><a href="application.html" data-i18n="nav.app">Prochainement</a></li> <li><a href="application.html" data-i18n="nav.app">Prochainement</a></li>
</ul> </ul>
</div> </div>
@ -274,7 +269,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -25,7 +25,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Application</a> <a href="application.html" data-i18n="nav.app">Application</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -43,14 +43,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Application</a> <a href="application.html" data-i18n="nav.app">Application</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -283,7 +279,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -309,7 +305,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script> <script>
document.getElementById('notifyForm')?.addEventListener('submit', function(e) { document.getElementById('notifyForm')?.addEventListener('submit', function(e) {

View File

@ -65,7 +65,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -83,14 +83,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -324,7 +320,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
<li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li> <li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li>
</ul> </ul>
</div> </div>
@ -351,7 +347,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -1,185 +0,0 @@
# Déploiement Cloudflare Worker — Welcome cron MVA
Ce Worker fait deux choses :
1. **Proxy HubSpot** (déjà fonctionnel — vérification doublon + numéro de référence)
2. **Cron Welcome** (NOUVEAU — envoie l'email de bienvenue **après** que le client a cliqué sur "Confirmer" dans le mail HubSpot de double opt-in)
L'email de bienvenue contient le **numéro de référence client** ET (à ajouter dans le template EmailJS) **l'adresse du dépôt à Paris**. Ces infos ne sont **jamais** envoyées avant validation de l'email — protection contre les bots et les expéditions de cartons vides.
---
## Étapes de déploiement (10 min)
### 1. Mettre à jour le code du Worker
1. Aller sur https://dash.cloudflare.com/
2. Workers & Pages → cliquer sur **`mva-hubspot-proxy`**
3. Onglet **Modifier le code** (ou *Quick edit*)
4. Tout sélectionner et remplacer par le contenu de `cloudflare-worker/hubspot-proxy.js` (de ce repo)
5. Cliquer **Déployer / Save and deploy**
### 2. Variables d'environnement
Dans **Paramètres → Variables et secrets**, ajouter (si pas déjà là) :
| Nom | Valeur |
|-----|--------|
| `HUBSPOT_TOKEN` | `pat-eu1-...` (existant — vérifier que le scope est read+write contacts) |
| `EMAILJS_PUBLIC_KEY` | `8KUlaQ7BDVIbkZRyP` |
| `EMAILJS_SERVICE_ID` | `service_aeamo3x` |
| `EMAILJS_TEMPLATE_ID` | `template_s1kr2et` |
| `PARIS_DEPOT_ADDRESS` | **Ton adresse exacte à Paris** (rue, code postal, ville, étage, nom à indiquer sur le carton…) |
⚠️ **`PARIS_DEPOT_ADDRESS`** est l'info la plus sensible — c'est l'adresse que tu veux protéger. Elle ne quitte jamais Cloudflare/EmailJS et n'arrive au client que dans le mail de bienvenue, qui n'est envoyé qu'aux contacts qui ont confirmé leur email.
> Si une variable n'est pas définie, le Worker utilise le fallback hardcodé.
### 3. Stockage KV (idempotence — empêche de spammer le welcome)
Dans **Paramètres → Stockage et bases de données → Bindings KV** :
1. Cliquer **Ajouter un binding**
2. Variable name : **`WELCOME_KV`**
3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker`
4. Sauvegarder
### 4. Cron Trigger — toutes les 5 minutes
Dans **Paramètres → Déclencheurs (Triggers) → Cron Triggers** :
1. Cliquer **Add Cron Trigger**
2. Schedule : `*/5 * * * *`
3. Sauvegarder
### 5a. EmailJS — mettre à jour le template pour inclure l'adresse Paris
1. Aller sur https://dashboard.emailjs.com/admin/templates
2. Cliquer sur le template `template_s1kr2et`
3. Modifier le contenu pour inclure les variables suivantes :
- `{{firstname}}` — prénom du client
- `{{reference_client}}` — sa référence (ex : `MVA-001`)
- `{{paris_address}}` — l'adresse complète du dépôt à Paris
4. Exemple de corps d'email recommandé :
```
Bonjour {{firstname}},
Bienvenue chez MVA Global Fret ! Votre inscription est confirmée.
══════════════════════════════════════════════
VOTRE NUMÉRO DE RÉFÉRENCE CLIENT
{{reference_client}}
══════════════════════════════════════════════
Conservez précieusement ce numéro — il vous permet de suivre vos colis
et nous facilite la prise en charge.
📦 ADRESSE DU DÉPÔT À PARIS
{{paris_address}}
⚠️ Important : indiquez bien votre numéro de référence
({{reference_client}}) sur chaque colis que vous nous envoyez.
Cela nous permet de vous identifier rapidement.
Pour toute question, contactez-nous via Messenger ou par WhatsApp.
— L'équipe MVA Global Fret
+261 38 49 737 51
```
5. Sauvegarder le template
### 5b. EmailJS — autoriser les appels serveur (CRITIQUE)
Sans cette étape, le Worker ne pourra pas envoyer les emails (erreur 403).
1. Aller sur https://dashboard.emailjs.com/admin/account
2. Onglet **Security**
3. Décocher **"Allow EmailJS API for non-browser applications"**
- Le terme est trompeur : décocher autorise les appels serveur
- (Cocher = bloquer les appels serveur pour anti-spam)
4. Sauvegarder
### 6. Vérifier le scope HubSpot
Le token HubSpot doit avoir :
- ✅ `crm.objects.contacts.read`
- ✅ `crm.objects.contacts.write` (NOUVEAU — pour mettre à jour la propriété en cas de besoin)
- ✅ `crm.lists.read` (déjà bon)
Si tu obtiens des erreurs 403, regénère le token sur https://app-eu1.hubspot.com/private-apps/148163754/
---
## Test manuel
Une fois tout déployé, tu peux forcer une exécution du cron sans attendre 5 min :
```bash
curl -X POST https://mva-hubspot-proxy.mvaglobalfret.workers.dev \
-H "Content-Type: application/json" \
-d '{"action":"triggerWelcomeQueue"}'
```
Réponse attendue :
```json
{ "ok": true, "stats": { "scanned": 3, "sent": 1, "skipped": 2, "errors": 0 } }
```
- `scanned` : combien de contacts confirmés ont été inspectés
- `sent` : combien d'emails de bienvenue ont été envoyés cette fois-ci
- `skipped` : déjà envoyés précédemment (KV tracking)
- `errors` : envois qui ont échoué
---
## Logs en production
Cloudflare → ton Worker → **Logs (en temps réel)** : tu verras chaque exécution cron avec les stats.
---
## En cas de problème
| Symptôme | Cause probable | Fix |
|---|---|---|
| `EmailJS 403: API access disabled` | Étape 5 pas faite | Décocher "Allow API for non-browser" sur EmailJS |
| `HubSpot 403` | Token n'a pas le scope write | Regénérer token avec scope contacts.write |
| `HubSpot 401` | Token invalide / expiré | Regénérer un Private App token |
| Le cron ne tourne pas | Cron trigger pas activé | Vérifier *Triggers → Cron Triggers* |
| KV: `env.WELCOME_KV is undefined` | Binding pas créé | Vérifier *Storage & Databases → KV bindings* |
| Aucun email envoyé même après confirmation | Le filtre HubSpot ne matche pas | Inspecter dans Logs : voir si `searchConfirmedContacts` retourne des résultats |
---
## Architecture finale
```
┌──────────────────────────────────────────────────────────────────┐
│ 1. User remplit le formulaire sur contact.html │
│ ↓ │
│ 2. form-handler.js soumet à HubSpot (Forms API) │
│ ↓ │
│ 3. HubSpot crée le contact AVEC reference_client │
│ ↓ │
│ 4. HubSpot envoie l'email de double opt-in (sujet + bouton │
│ « Confirmer » seulement — PAS la référence ni l'adresse) │
│ ↓ │
│ ─────── User clique sur « Confirmer » ─────── │
│ ↓ │
│ 5. HubSpot met à jour hs_emailconfirmationstatus = CONFIRMED │
│ ↓ │
│ 6. Cron Cloudflare (toutes les 5 min) : │
│ - cherche les contacts CONFIRMED non encore welcomed │
│ - vérifie KV : welcomed:{email} │
│ - envoie email via EmailJS REST API │
│ (template contient : prénom, ref, adresse Paris) │
│ - écrit welcomed:{email} dans KV │
│ ↓ │
│ 7. Le client reçoit son welcome email avec sa référence ET │
│ l'adresse de dépôt à Paris. │
└──────────────────────────────────────────────────────────────────┘
```
**Conclusion** : il est désormais impossible pour un bot de récupérer ta référence client ou ton adresse Paris sans avoir au préalable un email valide ET cliqué sur le lien de confirmation. Anti-spam blindé. ✅

View File

@ -1,730 +0,0 @@
// ============================================================
// MVA Global Fret — Cloudflare Worker : Proxy HubSpot + Welcome cron
// ============================================================
// Ce Worker fait deux choses :
//
// 1) Proxy HubSpot (via fetch handler, appelé par le navigateur)
// - Vérification doublon par email
// - Génération du prochain numéro de référence séquentiel
//
// 2) Cron post-confirmation (via scheduled handler, appelé par
// Cloudflare toutes les 5 min) :
// - Cherche les contacts qui ont CONFIRMÉ leur double opt-in
// - Pour chacun, envoie l'email de bienvenue (avec sa référence)
// - Marque le contact dans Cloudflare KV pour ne pas re-envoyer
//
// L'email de bienvenue n'arrive donc QU'APRÈS que le client a cliqué
// sur "Confirmer" dans le mail HubSpot. La référence client + l'adresse
// du dépôt à Paris ne fuitent jamais avant validation.
//
// ============================================================
// DÉPLOIEMENT
// ============================================================
//
// Sur https://dash.cloudflare.com/ :
//
// 1. Workers & Pages → ton Worker mva-hubspot-proxy → Modifier le code
// → coller ce fichier → Déployer
//
// 2. Workers & Pages → ton Worker → Paramètres → Variables et secrets :
// • HUBSPOT_TOKEN = pat-eu1-... (déjà existant, lecture+écriture contacts)
// • EMAILJS_PUBLIC_KEY = 8KUlaQ7BDVIbkZRyP
// • EMAILJS_SERVICE_ID = service_aeamo3x
// • EMAILJS_TEMPLATE_ID = template_s1kr2et
//
// 3. Workers & Pages → ton Worker → Paramètres → Stockage et bases
// → Bindings KV → Ajouter :
// Variable name : WELCOME_KV
// Namespace : créer "mva-welcome-tracker"
//
// 4. Workers & Pages → ton Worker → Paramètres → Déclencheurs (Triggers)
// → Cron Triggers → Ajouter :
// */5 * * * * (toutes les 5 minutes)
//
// 5. ⚠️ EmailJS : sur https://dashboard.emailjs.com/admin/account →
// Security → décocher "Allow EmailJS API for non-browser applications"
// → la décocher (= autoriser les appels serveur). Sinon le Worker ne
// pourra pas envoyer les emails.
//
// 6. ⚠️ Le token HubSpot doit avoir le scope crm.objects.contacts.write
// en plus du read (pour mettre à jour les propriétés contact).
// → si erreur 403 sur la mise à jour KV/contact, regénérer le token
// avec ce scope sur https://app-eu1.hubspot.com/private-apps/...
//
// ============================================================
// Fallbacks pour les valeurs publiques d'EmailJS (déjà visibles dans le
// JavaScript du site). Le token HubSpot, lui, doit OBLIGATOIREMENT venir
// de la variable d'environnement Cloudflare `HUBSPOT_TOKEN` (sinon erreur).
const FALLBACK_EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
const FALLBACK_EMAILJS_SERVICE_ID = 'service_aeamo3x';
const FALLBACK_EMAILJS_TEMPLATE_ID= 'template_s1kr2et';
const HUBSPOT_API = 'https://api.hubapi.com';
const HUBSPOT_PORTAL_ID = '148163754';
const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a';
const EMAILJS_API = 'https://api.emailjs.com/api/v1.0/email/send';
const corsHeaders = {
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
export default {
// -----------------------------------------------------------
// 1) Handler navigateur (POST depuis le formulaire / page de confirmation)
// -----------------------------------------------------------
async fetch(request, env) {
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const token = env.HUBSPOT_TOKEN;
if (!token) {
return jsonResponse({ error: 'HUBSPOT_TOKEN env var not set' }, 500);
}
try {
const body = await request.json();
const { email, action } = body;
// ── action: nextRef ─────────────────────────────────────
if (action === 'nextRef') {
return jsonResponse({ nextRef: await getNextRef(token) });
}
// ── action: triggerWelcome (admin/debug, optionnel) ─────
if (action === 'triggerWelcomeQueue') {
const stats = await processWelcomeQueue(env);
return jsonResponse({ ok: true, stats });
}
// ── action: sendWelcomeNow ───────────────────────────────
// Envoi immédiat du welcome email via EmailJS (avec l'adresse
// Paris depuis env var). Appelé par form-handler.js après
// soumission du formulaire. L'adresse n'apparaît jamais dans
// le code JS public — elle vient des secrets Cloudflare.
// Anti-bot : on vérifie d'abord le token Cloudflare Turnstile.
if (action === 'sendWelcomeNow') {
if (!body.email) return jsonResponse({ error: 'email requis' }, 400);
// Validation Turnstile (anti-bot)
const turnstileOk = await verifyTurnstile(env, body.turnstile_token, request);
if (!turnstileOk) {
return jsonResponse({ ok: false, error: 'Turnstile validation failed' }, 403);
}
try {
await sendWelcomeEmail(env, {
firstname : body.firstname || '',
email : body.email,
reference_client : body.reference_client || '',
});
return jsonResponse({ ok: true });
} catch (err) {
return jsonResponse({ ok: false, error: err.message }, 500);
}
}
// ── action: requestVerification ──────────────────────────
// Génère un token unique, stocke TOUTES les données du formulaire en KV,
// et envoie un email de validation via Brevo. Le contact n'est créé
// dans HubSpot QU'APRÈS clic sur le lien de confirmation (anti-spam :
// les inscriptions non vérifiées ne polluent pas le CRM).
// Anti-bot : Turnstile vérifié d'abord.
if (action === 'requestVerification') {
if (!body.email) return jsonResponse({ error: 'email requis' }, 400);
const turnstileOk = await verifyTurnstile(env, body.turnstile_token, request);
if (!turnstileOk) {
return jsonResponse({ ok: false, error: 'Turnstile validation failed' }, 403);
}
try {
const verToken = crypto.randomUUID().replace(/-/g, '');
const tokenData = {
firstname : body.firstname || '',
lastname : body.lastname || '',
phone : body.phone || '',
email : body.email.toLowerCase().trim(),
address : body.address || '',
createdAt : new Date().toISOString(),
};
if (!env.WELCOME_KV) {
return jsonResponse({ ok: false, error: 'KV not bound' }, 500);
}
// Token valide 24h
await env.WELCOME_KV.put(`verify:${verToken}`, JSON.stringify(tokenData), {
expirationTtl: 60 * 60 * 24,
});
await sendVerificationEmail(env, tokenData, verToken);
return jsonResponse({ ok: true });
} catch (err) {
return jsonResponse({ ok: false, error: err.message }, 500);
}
}
// ── action: verifyToken ──────────────────────────────────
// Appelé par confirmation.html quand l'utilisateur clique sur
// le lien dans l'email de validation. C'est ICI que le contact
// est CRÉÉ dans HubSpot (avec une référence générée à la volée),
// puis le welcome email est envoyé (ref + adresse Paris).
// Idempotent : un 2ème clic ne re-crée pas de contact.
if (action === 'verifyToken') {
if (!body.token) return jsonResponse({ error: 'token requis' }, 400);
if (!env.WELCOME_KV) return jsonResponse({ ok: false, error: 'KV not bound' }, 500);
const key = `verify:${body.token}`;
const raw = await env.WELCOME_KV.get(key);
if (!raw) {
return jsonResponse({ ok: false, error: 'Token invalide ou expiré' }, 404);
}
const tokenData = JSON.parse(raw);
// Idempotence : si déjà consommé, retourne le résultat précédent
// sans recréer le contact ni renvoyer d'email.
if (tokenData.used) {
return jsonResponse({
ok: true,
firstname : tokenData.firstname,
reference_client : tokenData.reference_client || '',
});
}
try {
// 1) Récupère la ref existante si le contact est déjà dans HubSpot
// (réinscription après suppression d'un test, ou création via
// l'ancien flow Forms API). Sinon génère la prochaine ref.
let refNumber;
try {
const existing = await searchContactByEmail(token, tokenData.email);
const existingResult = (existing.results || [])[0];
const existingRef = existingResult?.properties?.reference_client;
refNumber = existingRef || await getNextRef(token);
} catch (_) {
// Si la search échoue (scope manquant, etc.), fallback : génère
// une nouvelle ref. Le Forms API gérera la dédup côté HubSpot.
refNumber = await getNextRef(token);
}
// 2) Soumet via Forms API HubSpot (sans auth, pas besoin de scope
// write). Forms API crée le contact OU met à jour s'il existe.
const formRes = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fields: [
{ name: 'firstname', value: tokenData.firstname || '' },
{ name: 'lastname', value: tokenData.lastname || '' },
{ name: 'phone', value: tokenData.phone || '' },
{ name: 'email', value: tokenData.email },
{ name: 'address', value: tokenData.address || '' },
{ name: 'reference_client', value: refNumber },
],
context: {
pageUri : 'https://mva-global-fret.github.io/site-mva-global-fret/contact.html',
pageName: 'Verified signup (MVA Global Fret)',
},
}),
}
);
if (!formRes.ok) {
const errTxt = await formRes.text();
throw new Error(`HubSpot Forms API failed ${formRes.status}: ${errTxt}`);
}
// 3) Envoie le welcome email avec ref + adresse Paris
const welcomeContact = { ...tokenData, reference_client: refNumber };
await sendWelcomeViaBrevo(env, welcomeContact);
// 4) Marque le token consommé (gardé 7j pour idempotence)
await env.WELCOME_KV.put(key, JSON.stringify({
...tokenData,
used : true,
usedAt : new Date().toISOString(),
reference_client : refNumber,
}), {
expirationTtl: 60 * 60 * 24 * 7,
});
return jsonResponse({
ok: true,
firstname : tokenData.firstname,
reference_client : refNumber,
});
} catch (err) {
return jsonResponse({ ok: false, error: err.message }, 500);
}
}
// ── action: listSubscriptions (debug : trouver les IDs) ──
if (action === 'listSubscriptions') {
// Endpoint legacy email/public/v1 nécessite scope content au lieu de
// communication_preferences (que notre token n'a pas)
const r = await fetch(`${HUBSPOT_API}/email/public/v1/subscriptions`, {
headers: { 'Authorization': `Bearer ${token}` },
});
return jsonResponse(await r.json());
}
// ── action: subscribe ────────────────────────────────────
// Inscrit un contact à un type d'abonnement marketing (déclenche
// l'envoi du mail de double opt-in si DOI activé au niveau compte).
if (action === 'subscribe') {
if (!email || typeof email !== 'string') {
return jsonResponse({ error: 'Email requis' }, 400);
}
const subId = body.subscriptionId;
if (!subId) return jsonResponse({ error: 'subscriptionId requis' }, 400);
const r = await fetch(`${HUBSPOT_API}/communication-preferences/v3/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
emailAddress: email.toLowerCase().trim(),
subscriptionId: subId,
legalBasis: 'LEGITIMATE_INTEREST_CLIENT',
legalBasisExplanation: 'Soumission du formulaire MVA Global Fret',
}),
});
const data = await r.text();
return jsonResponse({ status: r.status, body: data });
}
// ── action par défaut : vérification doublon par email ──
if (!email || typeof email !== 'string') {
return jsonResponse({ error: 'Email requis' }, 400);
}
const data = await searchContactByEmail(token, email);
return jsonResponse(data);
} catch (err) {
return jsonResponse({ error: err.message }, 500);
}
},
// -----------------------------------------------------------
// 2) Handler cron (Cloudflare scheduler, toutes les 5 min)
// -----------------------------------------------------------
async scheduled(event, env, ctx) {
ctx.waitUntil(processWelcomeQueue(env));
},
};
// =============================================================
// File d'attente : envoi du welcome aux contacts confirmés
// =============================================================
async function processWelcomeQueue(env) {
const token = env.HUBSPOT_TOKEN;
const stats = { scanned: 0, sent: 0, skipped: 0, errors: 0 };
// Liste des contacts qui ont CONFIRMÉ leur opt-in marketing
// Filtre HubSpot : hs_emailconfirmationstatus EQ "CONFIRMED"
const confirmed = await searchConfirmedContacts(token);
for (const contact of confirmed) {
stats.scanned++;
const props = contact.properties || {};
const email = (props.email || '').toLowerCase();
if (!email) { stats.skipped++; continue; }
// Idempotence : si on a déjà envoyé, on saute
const kvKey = `welcomed:${email}`;
const already = env.WELCOME_KV ? await env.WELCOME_KV.get(kvKey) : null;
if (already) { stats.skipped++; continue; }
try {
await sendWelcomeEmail(env, {
firstname : props.firstname || '',
email : email,
reference_client : props.reference_client || '',
});
// Marquer comme envoyé dans KV (TTL 1 an pour éviter de garder
// indéfiniment des entrées si quelqu'un se désabonne et se réabonne)
if (env.WELCOME_KV) {
await env.WELCOME_KV.put(kvKey, new Date().toISOString(), {
expirationTtl: 60 * 60 * 24 * 365,
});
}
stats.sent++;
} catch (err) {
stats.errors++;
console.warn('[welcome]', email, err.message);
}
}
return stats;
}
// =============================================================
// HubSpot : recherches & lectures
// =============================================================
// Date avant laquelle les contacts CONFIRMÉS ne déclenchent PAS de welcome.
// Évite de spammer les contacts déjà existants au moment du déploiement
// du nouveau cron. Tout contact créé APRÈS cette date (et qui confirme
// son email) recevra son email de bienvenue normalement.
const WELCOME_CUTOFF_ISO = '2026-05-05T00:00:00Z';
async function searchConfirmedContacts(token) {
// On limite à 100 contacts par cron run (largement suffisant pour 1 PME)
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
filterGroups: [{
filters: [
{ propertyName: 'hs_emailconfirmationstatus', operator: 'EQ', value: 'CONFIRMED' },
{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' },
{ propertyName: 'createdate', operator: 'GTE', value: WELCOME_CUTOFF_ISO },
],
}],
properties: ['firstname', 'lastname', 'email', 'reference_client', 'hs_emailconfirmationstatus'],
sorts: [{ propertyName: 'lastmodifieddate', direction: 'DESCENDING' }],
limit: 100,
}),
});
if (!res.ok) {
throw new Error(`HubSpot search failed: ${res.status}`);
}
const data = await res.json();
return data.results || [];
}
async function searchContactByEmail(token, email) {
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email.toLowerCase().trim() }],
}],
properties: ['firstname', 'lastname', 'email', 'reference_client'],
}),
});
if (!res.ok) {
throw new Error(`HubSpot lookup failed: ${res.status}`);
}
return res.json();
}
async function getNextRef(token) {
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
filterGroups: [{
filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' }],
}],
properties: ['reference_client'],
limit: 100,
}),
});
const data = await res.json();
let maxNum = 0;
(data.results || []).forEach(c => {
const m = (c.properties?.reference_client || '').match(/^MVA-(\d+)$/);
if (m) {
const n = parseInt(m[1], 10);
if (n > maxNum) maxNum = n;
}
});
return 'MVA-' + String(maxNum + 1).padStart(3, '0');
}
// =============================================================
// EmailJS : envoi serveur via REST API
// =============================================================
async function sendWelcomeEmail(env, params) {
const payload = {
service_id : env.EMAILJS_SERVICE_ID || FALLBACK_EMAILJS_SERVICE_ID,
template_id: env.EMAILJS_TEMPLATE_ID || FALLBACK_EMAILJS_TEMPLATE_ID,
user_id : env.EMAILJS_PUBLIC_KEY || FALLBACK_EMAILJS_PUBLIC_KEY,
template_params: {
firstname : params.firstname,
email : params.email,
reference_client : params.reference_client,
// Adresse du dépôt Paris — définie via l'env var PARIS_DEPOT_ADDRESS
// dans Cloudflare. Si non définie, on envoie un placeholder visible
// pour signaler à l'admin qu'il faut la configurer.
paris_address : env.PARIS_DEPOT_ADDRESS || '[À configurer dans Cloudflare]',
},
};
const res = await fetch(EMAILJS_API, {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`EmailJS ${res.status}: ${text}`);
}
}
// =============================================================
// Brevo (ex-Sendinblue) : envoi d'emails (verification + welcome)
// =============================================================
// Brevo est utilisé pour l'envoi car il accepte la "single-sender
// verification" : on valide juste une adresse email (mvaglobalfret@gmail.com)
// au lieu de devoir vérifier tout un domaine. Free tier : 300 emails/jour.
//
// Setup requis :
// - env.BREVO_API_KEY = clé API Brevo (xkeysib-...)
// - env.BREVO_SENDER_EMAIL = adresse expéditrice validée chez Brevo
// (ex: "mvaglobalfret@gmail.com")
// - env.BREVO_SENDER_NAME = nom affiché à l'expéditeur (ex: "MVA Global Fret")
// - env.SITE_URL = base URL du site (ex: "https://mva-global-fret.github.io/site-mva-global-fret")
//
// API doc : https://developers.brevo.com/reference/sendtransacemail
const BREVO_API = 'https://api.brevo.com/v3/smtp/email';
async function brevoSend(env, { to, subject, html }) {
if (!env.BREVO_API_KEY) {
throw new Error('BREVO_API_KEY env var not set');
}
const senderEmail = env.BREVO_SENDER_EMAIL || 'mvaglobalfret@gmail.com';
const senderName = env.BREVO_SENDER_NAME || 'MVA Global Fret';
const res = await fetch(BREVO_API, {
method: 'POST',
headers: {
'api-key' : env.BREVO_API_KEY,
'accept' : 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify({
sender : { name: senderName, email: senderEmail },
to : [{ email: to }],
subject : subject,
htmlContent: html,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Brevo ${res.status}: ${text}`);
}
return res.json();
}
async function sendVerificationEmail(env, contact, verToken) {
const siteUrl = env.SITE_URL || 'https://mva-global-fret.github.io/site-mva-global-fret';
const verifyUrl = `${siteUrl}/confirmation.html?token=${verToken}`;
const logoUrl = `${siteUrl}/PNG%20MVA%20GLOBAL%20FRET.png`;
const firstname = escapeHtml(contact.firstname || '');
const html = `<!DOCTYPE html>
<html lang="fr">
<body style="margin:0;padding:0;font-family:Arial,sans-serif;background:#f5f5f5;">
<div style="max-width:600px;margin:0 auto;background:#fff;">
<div style="background:#1a1a3e;padding:24px 30px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td style="width:80px;vertical-align:middle;">
<img src="${logoUrl}" alt="MVA Global Fret" style="display:block;width:70px;height:auto;border:0;">
</td>
<td style="vertical-align:middle;text-align:center;padding-right:80px;">
<div style="color:#c5a55a;font-size:24px;font-weight:700;letter-spacing:2px;">MVA GLOBAL FRET</div>
<div style="color:#fff;font-size:13px;margin-top:6px;">Fret Aérien Paris Antananarivo</div>
</td>
</tr>
</table>
</div>
<div style="padding:40px;">
<p style="font-size:18px;color:#1a1a3e;font-weight:bold;">Bonjour ${firstname},</p>
<p style="color:#333;line-height:1.6;">
Merci pour votre inscription chez <strong>MVA Global Fret</strong> !
</p>
<p style="color:#333;line-height:1.6;">
Pour finaliser votre inscription et recevoir votre <strong>numéro de référence client</strong>
ainsi que <strong>l'adresse de notre dépôt à Paris</strong>, cliquez sur le bouton ci-dessous :
</p>
<div style="text-align:center;margin:32px 0;">
<a href="${verifyUrl}" style="display:inline-block;background:#c5a55a;color:#1a1a3e;padding:16px 40px;border-radius:50px;text-decoration:none;font-weight:700;font-size:16px;letter-spacing:0.5px;">
Confirmer mon email
</a>
</div>
<p style="color:#666;font-size:13px;line-height:1.6;">
Ce lien est valable <strong>24 heures</strong>. Si vous n'êtes pas à l'origine de cette inscription, ignorez simplement cet email.
</p>
<p style="color:#666;font-size:12px;line-height:1.6;border-top:1px solid #eee;padding-top:18px;margin-top:30px;">
Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :<br>
<span style="color:#c5a55a;word-break:break-all;">${verifyUrl}</span>
</p>
</div>
<div style="background:#1a1a3e;padding:18px;text-align:center;color:#c5a55a;font-size:12px;">
© 2026 MVA Global Fret Tous droits réservés
</div>
</div>
</body>
</html>`;
return brevoSend(env, {
to: contact.email,
subject: 'Confirmez votre inscription chez MVA Global Fret',
html,
});
}
async function sendWelcomeViaBrevo(env, contact) {
const siteUrl = env.SITE_URL || 'https://mva-global-fret.github.io/site-mva-global-fret';
const logoUrl = `${siteUrl}/PNG%20MVA%20GLOBAL%20FRET.png`;
const firstname = escapeHtml(contact.firstname || '');
const ref = escapeHtml(contact.reference_client || '');
const refRaw = contact.reference_client || '';
// Format adresse Paris : la 1ère ligne (nom du destinataire) reçoit
// automatiquement la référence client entre parenthèses, comme ça
// le client a directement la bonne forme à recopier sur son colis.
// Support aussi un placeholder {{ref}} si présent dans l'env var.
let parisAddrRaw = env.PARIS_DEPOT_ADDRESS || '';
if (parisAddrRaw.includes('{{ref}}')) {
parisAddrRaw = parisAddrRaw.replace(/\{\{ref\}\}/g, refRaw);
} else if (refRaw && parisAddrRaw) {
const lines = parisAddrRaw.split('\n');
lines[0] = `${lines[0]} (${refRaw})`;
parisAddrRaw = lines.join('\n');
}
const parisAddr = escapeHtml(parisAddrRaw).replace(/\n/g, '<br>');
const html = `<!DOCTYPE html>
<html lang="fr">
<body style="margin:0;padding:0;font-family:Arial,sans-serif;background:#f5f5f5;">
<div style="max-width:600px;margin:0 auto;background:#fff;">
<div style="background:#1a1a3e;padding:24px 30px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td style="width:80px;vertical-align:middle;">
<img src="${logoUrl}" alt="MVA Global Fret" style="display:block;width:70px;height:auto;border:0;">
</td>
<td style="vertical-align:middle;text-align:center;padding-right:80px;">
<div style="color:#c5a55a;font-size:24px;font-weight:700;letter-spacing:2px;">MVA GLOBAL FRET</div>
<div style="color:#fff;font-size:13px;margin-top:6px;">Fret Aérien Paris Antananarivo</div>
</td>
</tr>
</table>
</div>
<div style="padding:40px;">
<p style="font-size:18px;color:#1a1a3e;font-weight:bold;">Bonjour ${firstname},</p>
<p style="color:#333;line-height:1.6;">
Bienvenu(e) chez <strong>MVA Global Fret</strong> ! Votre email est confirmé,
votre inscription est désormais active.
</p>
<div style="background:#f0ead8;border-left:4px solid #c5a55a;padding:16px 20px;margin:24px 0;border-radius:4px;">
<p style="margin:0;color:#1a1a3e;font-size:14px;">Votre numéro de référence client :</p>
<p style="margin:8px 0 0;color:#1a1a3e;font-size:22px;font-weight:bold;letter-spacing:2px;">${ref}</p>
<p style="margin:6px 0 0;color:#666;font-size:12px;">Conservez ce numéro précieusement.</p>
</div>
<p style="color:#333;margin-top:28px;"><strong>L'adresse à Paris pour l'envoi de vos colis :</strong></p>
<div style="background:#f9f9f9;border:1px solid #ddd;padding:20px 24px;border-radius:6px;margin:12px 0;font-family:monospace;font-size:15px;line-height:1.8;color:#1a1a3e;">
${parisAddr}
</div>
<div style="background:#fff3cd;border:1px solid #ffc107;padding:14px 18px;border-radius:6px;margin:16px 0;">
<p style="margin:0 0 8px;color:#856404;font-size:14px;font-weight:bold;">
Important : ne modifiez rien à ces informations.
</p>
<p style="margin:0;color:#856404;font-size:14px;line-height:1.5;">
Recopiez l'adresse <strong>exactement telle qu'elle est indiquée ci-dessus</strong>,
sans rien retirer ni ajouter. Votre numéro de référence <strong>${ref}</strong>
fait partie intégrante de l'adresse — c'est ce qui garantit que votre colis nous arrive bien.
</p>
</div>
<p style="color:#333;line-height:1.6;">
Pour toute question, contactez-nous :<br>
📧 mvaglobalfret@gmail.com<br>
📞 +33 7 80 97 08 25 (France) +261 38 49 737 51 (Madagascar)
</p>
</div>
<div style="background:#1a1a3e;padding:18px;text-align:center;color:#c5a55a;font-size:12px;">
© 2026 MVA Global Fret Tous droits réservés
</div>
</div>
</body>
</html>`;
return brevoSend(env, {
to: contact.email,
subject: `Bienvenue chez MVA Global Fret — Votre référence ${ref}`,
html,
});
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
// =============================================================
// Cloudflare Turnstile : validation anti-bot
// =============================================================
// Reçoit le token généré côté client (window.turnstileToken) et
// l'envoie à l'API Cloudflare avec le secret pour validation.
// Renvoie true uniquement si Cloudflare confirme que c'est un
// utilisateur humain.
async function verifyTurnstile(env, token, request) {
if (!token) return false;
if (!env.TURNSTILE_SECRET) {
// En dev / si pas configuré, on laisse passer (à durcir en prod)
console.warn('TURNSTILE_SECRET not set, skipping validation');
return true;
}
const ip = request.headers.get('CF-Connecting-IP') || '';
const formData = new FormData();
formData.append('secret', env.TURNSTILE_SECRET);
formData.append('response', token);
if (ip) formData.append('remoteip', ip);
try {
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: formData,
});
const data = await res.json();
return data.success === true;
} catch (err) {
console.warn('Turnstile verification error:', err);
return false;
}
}
// =============================================================
// Helpers
// =============================================================
function jsonResponse(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}

View File

@ -13,9 +13,6 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<!-- EmailJS -->
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
<style> <style>
.confirmation-shell { .confirmation-shell {
min-height: 100vh; min-height: 100vh;
@ -175,7 +172,7 @@
© 2026 MVA Global Fret. Tous droits réservés. © 2026 MVA Global Fret. Tous droits réservés.
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/confirmation.js"></script> <script src="js/confirmation.js"></script>
</body> </body>

View File

@ -25,7 +25,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -43,14 +43,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -59,8 +55,8 @@
<!-- HERO --> <!-- HERO -->
<section class="hero hero-sub" style="background-image: url('images/hero/contact-hero.jpg');"> <section class="hero hero-sub" style="background-image: url('images/hero/contact-hero.jpg');">
<div class="hero-content animate-on-scroll"> <div class="hero-content animate-on-scroll">
<h1 data-i18n="contact.heroTitle">Contactez-Nous</h1> <h1 data-i18n="contact.heroTitle">Inscrivez-vous</h1>
<p data-i18n="contact.heroSubtitle">Inscrivez-vous et commencez à envoyer vos colis dès aujourd'hui</p> <p data-i18n="contact.heroSubtitle">Commencez à envoyer vos colis dès aujourd'hui</p>
</div> </div>
</section> </section>
@ -142,7 +138,7 @@
</div> </div>
<!-- Cloudflare Turnstile (CAPTCHA invisible/léger pour bloquer les bots) --> <!-- Cloudflare Turnstile (CAPTCHA invisible/léger pour bloquer les bots) -->
<div class="cf-turnstile" data-sitekey="0x4AAAAAADKDuc7Rmlb1svIL" data-callback="onTurnstileSuccess" data-error-callback="onTurnstileError" style="margin-bottom: 16px;"></div> <div class="cf-turnstile" data-sitekey="0x4AAAAAADKk5L_1GV_IS_s9" data-callback="onTurnstileSuccess" data-error-callback="onTurnstileError" style="margin-bottom: 16px;"></div>
<div id="formErrorGlobal" style="display:none; color: var(--red); margin-bottom: 16px; font-size: 0.9rem;"></div> <div id="formErrorGlobal" style="display:none; color: var(--red); margin-bottom: 16px; font-size: 0.9rem;"></div>
@ -284,7 +280,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -310,8 +306,7 @@
</div> </div>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/translations.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/form-handler.js"></script> <script src="js/form-handler.js"></script>

View File

@ -25,7 +25,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -43,14 +43,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -77,7 +73,7 @@
<div class="prohibited-item"> <div class="prohibited-item">
<i class="fa-solid fa-bomb" aria-hidden="true"></i> <i class="fa-solid fa-bomb" aria-hidden="true"></i>
<h4 data-i18n="guide.cat1Title">Explosifs</h4> <h4 data-i18n="guide.cat1Title">Explosifs</h4>
<p data-i18n="guide.cat1Desc">Dynamite, munitions, feux d'artifice, pétards</p> <p data-i18n="guide.cat1Desc">Dynamite, munitions, feux d'artifice, pétards, armes à feu</p>
</div> </div>
<div class="prohibited-item"> <div class="prohibited-item">
<i class="fa-solid fa-fire-flame-curved" aria-hidden="true"></i> <i class="fa-solid fa-fire-flame-curved" aria-hidden="true"></i>
@ -102,7 +98,7 @@
<div class="prohibited-item"> <div class="prohibited-item">
<i class="fa-solid fa-skull-crossbones" aria-hidden="true"></i> <i class="fa-solid fa-skull-crossbones" aria-hidden="true"></i>
<h4 data-i18n="guide.cat6Title">Substances toxiques</h4> <h4 data-i18n="guide.cat6Title">Substances toxiques</h4>
<p data-i18n="guide.cat6Desc">Poisons, pesticides, substances infectieuses</p> <p data-i18n="guide.cat6Desc">Poisons, pesticides, substances infectieuses, substances illégales</p>
</div> </div>
<div class="prohibited-item"> <div class="prohibited-item">
<i class="fa-solid fa-flask" aria-hidden="true"></i> <i class="fa-solid fa-flask" aria-hidden="true"></i>
@ -265,7 +261,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -291,7 +287,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -62,7 +62,7 @@
</main> </main>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script> <script>
/* i18n minimal ------------------------------------------------------- */ /* i18n minimal ------------------------------------------------------- */
(function () { (function () {

View File

@ -1,23 +1,27 @@
// ============================================================ // ============================================================
// MVA Global Fret — Page de confirmation post-validation email // MVA Global Fret — Page de confirmation post-validation email
// ============================================================ // ============================================================
// Cette page est la cible du lien dans l'email de validation // Cette page est la cible du lien dans l'email de validation envoyé
// (envoyé par Brevo après soumission du formulaire). // par mva-api (= Resend) lors d'une inscription via contact.html.
// //
// URL : https://mva-global-fret.github.io/site-mva-global-fret/confirmation.html?token=XXX // URL : https://mva-globalfret.com/confirmation.html?token=XXX
// //
// Étapes : // Étapes :
// 1. Lire le token depuis l'URL // 1. Lire le token depuis l'URL
// 2. POST au Worker avec action 'verifyToken' // 2. POST mva-api /leads/verify-token avec { token }
// 3. Worker valide le token, envoie le welcome email (avec ref + // 3. mva-api INSERT le lead en DB, génère la ref MVA-NNN, envoie le
// adresse Paris) via Brevo, puis renvoie OK // welcome email (= ref + adresse Paris) via Resend, puis renvoie
// 4. Page affiche "Inscription confirmée !" // { ok: true, firstname, reference_client }
// 4. Page affiche "Inscription confirmée !" + la ref
// //
// Si le token est invalide / expiré : affichage d'un message d'erreur // Si le token est invalide / expiré / déjà consommé : affichage d'un
// avec invitation à contacter le support. // message d'erreur avec invitation à contacter le support.
//
// Migration 2026-05-10 : remplace l'ancien Cloudflare Worker
// `mva-hubspot-proxy.sergemind4s.workers.dev` (= décommissionné).
// ============================================================ // ============================================================
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.mvaglobalfret.workers.dev'; const API_BASE_URL = 'https://api.mva.mind4solutions.com';
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const token = new URLSearchParams(window.location.search).get('token'); const token = new URLSearchParams(window.location.search).get('token');
@ -28,18 +32,20 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/verify-token`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'verifyToken', token }), body: JSON.stringify({ token }),
}); });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
showSuccess(data.reference_client || null); showSuccess(data.reference_client || null);
} else { } else {
// Token expiré, déjà utilisé, ou inconnu // Token invalide, expiré, ou inconnu
showError(data.error === 'Token invalide ou expiré' const isInvalid = data.code === 'INVALID_OR_EXPIRED'
|| data.message === 'Token invalide ou expiré';
showError(isInvalid
? 'Ce lien de confirmation a expiré ou a déjà été utilisé.' ? 'Ce lien de confirmation a expiré ou a déjà été utilisé.'
: 'Une erreur est survenue lors de la confirmation.'); : 'Une erreur est survenue lors de la confirmation.');
} }

View File

@ -1,78 +1,61 @@
// ============================================ // ============================================
// MVA Global Fret — Form Handler // MVA Global Fret — Form Handler
// HubSpot Portal ID : 148163754 // ============================================
// HubSpot Form GUID : 1d9b75c9-8b60-4966-aa18-4bf503452e9a // Frontend logic for contact.html (= inscription form):
// - validate inputs + Cloudflare Turnstile token
// - call mva-api /leads/* routes for dedup check + double opt-in flow
// (= verification email + welcome / welcome-back emails via Resend)
// - reset Turnstile widget after each API call (= tokens are
// single-use server-side; without reset, a re-submit silently
// 403s from Cloudflare's siteverify endpoint)
//
// Migration 2026-05-10 : remplace l'ancien Cloudflare Worker
// `mva-hubspot-proxy.sergemind4s.workers.dev` (= décommissionné) par
// les routes mva-api Fastify. La DB Postgres remplace HubSpot Contacts.
// ============================================ // ============================================
const HUBSPOT_PORTAL_ID = '148163754'; // ── MVA API BASE URL ─────────────────────────────────────────────
const HUBSPOT_FORM_GUID = '1d9b75c9-8b60-4966-aa18-4bf503452e9a'; // Routes leads servies par mva-api derrière Caddy. CORS strict :
const FORMSPREE_ID = 'mojrvokp'; // le serveur whitelist explicitement https://mva-globalfret.com.
const API_BASE_URL = 'https://api.mva.mind4solutions.com';
// ── EMAILJS (email de bienvenue au client) ────────────────────────────────────
const EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
const EMAILJS_SERVICE_ID = 'service_aeamo3x';
const EMAILJS_TEMPLATE_ID = 'template_s1kr2et';
// Template pour les clients déjà inscrits ("Ravis de te revoir")
// ⚠️ À créer dans EmailJS puis remplacer la valeur ci-dessous
const EMAILJS_TEMPLATE_WELCOME_BACK = 'template_welcome_back';
// Initialisation EmailJS (une seule fois au chargement)
if (typeof emailjs !== 'undefined') {
emailjs.init({ publicKey: EMAILJS_PUBLIC_KEY });
}
// ── PROXY CLOUDFLARE WORKER ───────────────────────────────────────────────────
// URL du Worker qui proxifie l'API HubSpot CRM (contourne le CORS).
// Après déploiement du Worker (voir cloudflare-worker/hubspot-proxy.js),
// remplacer la chaîne vide par l'URL obtenue, ex :
// 'https://mva-hubspot-proxy.moncompte.workers.dev'
// Tant que cette constante est vide, la vérification doublon est désactivée
// (le formulaire s'envoie normalement — aucun blocage).
const WORKER_PROXY_URL = 'https://mva-hubspot-proxy.mvaglobalfret.workers.dev';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contactForm'); const form = document.getElementById('contactForm');
if (form) setupContactForm(form); if (form) setupContactForm(form);
}); });
// Génération séquentielle via le Worker HubSpot : MVA-001, MVA-002, etc. // ── TURNSTILE TOKEN MANAGEMENT ───────────────────────────────────
// Fallback sur un timestamp court si le Worker est indisponible. // Reset the Turnstile widget + global token after each Worker call.
async function generateRefNumber() { // Cloudflare Turnstile tokens are single-use server-side: a token
if (WORKER_PROXY_URL) { // already submitted to siteverify cannot be re-used. Without an
try { // explicit reset, a re-submit (= same form, same widget) would send
const res = await fetch(WORKER_PROXY_URL, { // the now-consumed token and Cloudflare would 403 silently.
method: 'POST', function resetTurnstile() {
headers: { 'Content-Type': 'application/json' }, window.turnstileToken = null;
body: JSON.stringify({ action: 'nextRef' }), if (window.turnstile && typeof window.turnstile.reset === 'function') {
}); try { window.turnstile.reset(); } catch (_) { /* widget absent */ }
if (res.ok) {
const data = await res.json();
if (data.nextRef) return data.nextRef;
} }
} catch { /* fallback ci-dessous */ }
}
// Fallback : numéro aléatoire court pour éviter les doublons en cas d'indisponibilité
const rand = String(Math.floor(Math.random() * 900) + 100);
return `MVA-F${rand}`;
} }
// Vérifie si l'email existe déjà dans HubSpot via le proxy Cloudflare Worker. // Vérifie si l'email existe déjà dans la table leads via mva-api.
// Retourne les propriétés du contact existant, ou null si nouveau client / proxy non configuré. // Retourne les propriétés du lead existant, ou null si nouveau
// client / API indisponible.
async function checkExistingContact(email) { async function checkExistingContact(email) {
// Si le proxy n'est pas encore déployé, on laisse passer sans bloquer
if (!WORKER_PROXY_URL) return null;
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/check-email`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.toLowerCase().trim() }), body: JSON.stringify({ email: email.toLowerCase().trim() }),
}); });
if (!res.ok) return null; if (!res.ok) return null;
const data = await res.json(); const data = await res.json();
return data.total > 0 ? data.results[0].properties : null; if (!data.exists) return null;
// Forme attendue par showAlreadyRegistered : { firstname, reference_client }
return {
firstname: data.firstname || '',
reference_client: data.reference_client || '',
};
} catch { } catch {
// Erreur réseau ou Worker indisponible : on laisse passer
return null; return null;
} }
} }
@ -82,7 +65,7 @@ function setupContactForm(form) {
e.preventDefault(); e.preventDefault();
if (!validateForm(form)) return; if (!validateForm(form)) return;
// ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ──────────────────────────── // ── VÉRIFICATION TURNSTILE (CAPTCHA anti-bot) ────────────────
if (!window.turnstileToken) { if (!window.turnstileToken) {
const errEl = document.getElementById('formErrorGlobal'); const errEl = document.getElementById('formErrorGlobal');
if (errEl) { if (errEl) {
@ -91,23 +74,20 @@ function setupContactForm(form) {
} }
return; return;
} }
// ─────────────────────────────────────────────────────────────────────────
setLoading(true); setLoading(true);
const email = form.email.value.trim(); const email = form.email.value.trim();
// ── VÉRIFICATION DOUBLON ────────────────────────────────────────────────── // ── VÉRIFICATION DOUBLON ─────────────────────────────────────
// Vérifie HubSpot. Comme les contacts ne sont créés QU'APRÈS confirmation // Comme les contacts ne sont créés QU'APRÈS confirmation email,
// email, ce check ne retourne que les vrais clients déjà inscrits (pas // ce check ne retourne que les vrais clients déjà inscrits.
// les inscriptions en attente de confirmation).
const existing = await checkExistingContact(email); const existing = await checkExistingContact(email);
if (existing) { if (existing) {
setLoading(false); setLoading(false);
showAlreadyRegistered(existing); showAlreadyRegistered(existing);
return; return;
} }
// ─────────────────────────────────────────────────────────────────────────
const data = { const data = {
firstname: form.firstname.value.trim(), firstname: form.firstname.value.trim(),
@ -117,17 +97,17 @@ function setupContactForm(form) {
address: form.address.value.trim(), address: form.address.value.trim(),
}; };
// ── ENVOI VERS LE WORKER ────────────────────────────────────────────────── // ── ENVOI VERS MVA-API ────────────────────────────────────────
// Le Worker stocke les données en KV (24h), envoie un email de validation // L'API stocke les données en `leads_pending` (24h TTL) et envoie un
// via Brevo. Le contact n'est créé dans HubSpot QUE quand l'utilisateur // email de validation via Resend. Le lead n'est INSERT en `leads` QUE
// clique sur le lien de confirmation (anti-pollution du CRM). // quand l'utilisateur clique sur le lien de confirmation
// (anti-pollution DB + anti-bot complémentaire à Turnstile).
let ok = false; let ok = false;
try { try {
const res = await fetch(WORKER_PROXY_URL, { const res = await fetch(`${API_BASE_URL}/leads/request-verification`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
action: 'requestVerification',
...data, ...data,
turnstile_token: window.turnstileToken || '', turnstile_token: window.turnstileToken || '',
}), }),
@ -138,6 +118,8 @@ function setupContactForm(form) {
console.warn('[requestVerification]', err); console.warn('[requestVerification]', err);
} }
// Reset Turnstile after the Worker call (= regardless of result)
resetTurnstile();
setLoading(false); setLoading(false);
if (ok) { if (ok) {
@ -148,73 +130,7 @@ function setupContactForm(form) {
}); });
} }
// ── SOUMISSION HUBSPOT ──────────────────────────────────────────────────────── // ── VALIDATION ───────────────────────────────────────────────────
async function submitToHubSpot(data) {
const payload = {
fields: [
{ name: 'firstname', value: data.firstname },
{ name: 'lastname', value: data.lastname },
{ name: 'phone', value: data.phone },
{ name: 'email', value: data.email },
{ name: 'address', value: data.address },
{ name: 'reference_client', value: data.reference_client },
],
context: {
pageUri: window.location.href,
pageName: document.title,
},
};
const res = await fetch(
`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_GUID}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
if (!res.ok) throw new Error(`HubSpot error: ${res.status}`);
return res.json();
}
// ── SOUMISSION FORMSPREE (email de backup) ────────────────────────────────────
async function submitToFormspree(data) {
if (FORMSPREE_ID === 'YOUR_FORMSPREE_ID') return;
const res = await fetch(`https://formspree.io/f/${FORMSPREE_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
nom: data.lastname,
prenom: data.firstname,
telephone: data.phone,
email: data.email,
adresse_livraison: data.address,
reference_client: data.reference_client,
}),
});
if (!res.ok) throw new Error(`Formspree error: ${res.status}`);
return res.json();
}
// ── NOTIFICATION DOUBLON (email interne seulement, sans toucher aux données) ──
async function notifyDuplicateViaFormspree(contact) {
if (FORMSPREE_ID === 'YOUR_FORMSPREE_ID') return;
try {
await fetch(`https://formspree.io/f/${FORMSPREE_ID}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
_subject: `[MVA] Tentative double inscription — ${contact.firstname || ''} ${contact.lastname || ''}`,
message: `Le client ${contact.firstname || ''} ${contact.lastname || ''} (${contact.email}) a tenté de s'inscrire à nouveau. Référence existante : ${contact.reference_client || 'non définie'}.`,
}),
});
} catch { /* Ne pas bloquer l'interface si la notification échoue */ }
}
// ── VALIDATION ────────────────────────────────────────────────────────────────
function validateForm(form) { function validateForm(form) {
let valid = true; let valid = true;
const lang = localStorage.getItem('mva-lang') || 'fr'; const lang = localStorage.getItem('mva-lang') || 'fr';
@ -263,7 +179,7 @@ function isValidPhone(phone) {
return /^[+\d][\d\s\-().]{6,20}$/.test(phone); return /^[+\d][\d\s\-().]{6,20}$/.test(phone);
} }
// ── AFFICHAGE ───────────────────────────────────────────────────────────────── // ── AFFICHAGE ────────────────────────────────────────────────────
function showFieldError(name, msg) { function showFieldError(name, msg) {
const el = document.getElementById(`error-${name}`); const el = document.getElementById(`error-${name}`);
const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`); const input = document.getElementById(name) || document.querySelector(`[name="${name}"]`);
@ -296,9 +212,6 @@ function setLoading(isLoading) {
} }
function showSuccess(_refNumber, _clientData) { function showSuccess(_refNumber, _clientData) {
// L'envoi de l'email de validation est déjà fait dans setupContactForm
// via l'appel Worker requestVerification — on n'a plus rien à faire ici
// sauf afficher la confirmation à l'écran.
const successEl = document.getElementById('formSuccess'); const successEl = document.getElementById('formSuccess');
const form = document.getElementById('contactForm'); const form = document.getElementById('contactForm');
if (successEl) { if (successEl) {
@ -308,25 +221,32 @@ function showSuccess(_refNumber, _clientData) {
if (form) form.style.display = 'none'; if (form) form.style.display = 'none';
} }
// ── EMAIL "RAVIS DE TE REVOIR" (client déjà inscrit) ────────────────────────── // ── EMAIL "RAVIS DE VOUS REVOIR" (client déjà inscrit) ───────────
// Rappelle au client son numéro de référence existant — n'écrit RIEN dans HubSpot. // Rappelle au client son numéro de référence existant — zéro write DB.
// Passe par mva-api /leads/welcome-back qui délègue à Resend.
// Anti-bot via Turnstile : transmet le token déjà validé au moment du
// submit du formulaire.
async function sendWelcomeBackEmail(contact) { async function sendWelcomeBackEmail(contact) {
if (typeof emailjs === 'undefined') return;
if (!contact || !contact.email) return; if (!contact || !contact.email) return;
if (!window.turnstileToken) return;
try { try {
await emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_WELCOME_BACK, { await fetch(`${API_BASE_URL}/leads/welcome-back`, {
firstname: contact.firstname || '', method: 'POST',
email: contact.email, headers: { 'Content-Type': 'application/json' },
reference_client: contact.reference_client || '', body: JSON.stringify({
email : contact.email,
turnstile_token : window.turnstileToken,
}),
}); });
} catch (err) { } catch (err) {
// Si le template n'existe pas encore ou erreur réseau : on n'interrompt rien // Erreur réseau : on n'interrompt pas l'UX (le client voit
console.warn('EmailJS welcome-back email failed:', err); // déjà sa référence dans le UI).
console.warn('welcome-back failed:', err);
} }
} }
// Affiche le message "déjà client" — ne modifie AUCUNE donnée HubSpot // Affiche le message "déjà client" — ne modifie AUCUNE donnée HubSpot
function showAlreadyRegistered(contact) { async function showAlreadyRegistered(contact) {
const lang = localStorage.getItem('mva-lang') || 'fr'; const lang = localStorage.getItem('mva-lang') || 'fr';
const t = translations?.[lang]?.contact || {}; const t = translations?.[lang]?.contact || {};
@ -348,11 +268,11 @@ function showAlreadyRegistered(contact) {
if (form) form.style.display = 'none'; if (form) form.style.display = 'none';
// Envoi d'une notification interne à MVA (sans modifier les données du client) // Email "Ravis de vous revoir" via Worker + Resend (= footer 2026
notifyDuplicateViaFormspree(contact); // cohérent avec les autres emails transactionnels).
await sendWelcomeBackEmail(contact);
// Envoi d'un email "Ravis de te revoir" au client avec son n° de référence // Reset Turnstile after the Worker call (= prevent reuse).
sendWelcomeBackEmail(contact); resetTurnstile();
} }
function showError() { function showError() {

View File

@ -118,7 +118,12 @@ function setupAnimations() {
observer.unobserve(entry.target); observer.unobserve(entry.target);
} }
}); });
}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); // threshold: 0 (= fire dès qu'1px du bloc entre dans le viewport).
// Avant : threshold 0.1 (= 10%) ne fire jamais sur mobile portrait pour
// les pages cgv.html + politique-confidentialite.html dont les blocs
// .animate-on-scroll font 2000-3000px de hauteur (contenu trilingue
// FR/EN/MG verbose) — la viewport mobile ~600px n'atteint jamais 10%.
}, { threshold: 0, rootMargin: '0px 0px -50px 0px' });
elements.forEach(el => observer.observe(el)); elements.forEach(el => observer.observe(el));
} }

View File

@ -21,7 +21,7 @@ const translations = {
pricing: "Tarifs", pricing: "Tarifs",
serviceCommande: "Service Commande", serviceCommande: "Service Commande",
guide: "Guide d'envoi", guide: "Guide d'envoi",
contact: "Contact", contact: "Inscription",
app: "Prochainement" app: "Prochainement"
}, },
home: { home: {
@ -134,8 +134,8 @@ const translations = {
delivery2Note: "Retrait au bureau Cotisse de votre ville" delivery2Note: "Retrait au bureau Cotisse de votre ville"
}, },
contact: { contact: {
heroTitle: "Contactez-Nous", heroTitle: "Inscrivez-vous",
heroSubtitle: "Inscrivez-vous et commencez à envoyer vos colis dès aujourd'hui", heroSubtitle: "Commencez à envoyer vos colis dès aujourd'hui",
formTitle: "Formulaire d'inscription", formTitle: "Formulaire d'inscription",
formSubtitle: "Remplissez ce formulaire pour recevoir votre numéro de référence client et l'adresse de dépôt à Paris.", formSubtitle: "Remplissez ce formulaire pour recevoir votre numéro de référence client et l'adresse de dépôt à Paris.",
labelNom: "Nom", labelNom: "Nom",
@ -189,7 +189,7 @@ const translations = {
prohibitedTitle: "Articles Interdits", prohibitedTitle: "Articles Interdits",
prohibitedSubtitle: "Ces articles ne peuvent pas être transportés par fret aérien (réglementation IATA)", prohibitedSubtitle: "Ces articles ne peuvent pas être transportés par fret aérien (réglementation IATA)",
cat1Title: "Explosifs", cat1Title: "Explosifs",
cat1Desc: "Dynamite, munitions, feux d'artifice, pétards", cat1Desc: "Dynamite, munitions, feux d'artifice, pétards, armes à feu",
cat2Title: "Gaz comprimés", cat2Title: "Gaz comprimés",
cat2Desc: "Bouteilles de gaz, aérosols pressurisés, butane, propane", cat2Desc: "Bouteilles de gaz, aérosols pressurisés, butane, propane",
cat3Title: "Liquides inflammables", cat3Title: "Liquides inflammables",
@ -199,7 +199,7 @@ const translations = {
cat5Title: "Batteries lithium", cat5Title: "Batteries lithium",
cat5Desc: "Piles lithium seules, hoverboards, certains appareils", cat5Desc: "Piles lithium seules, hoverboards, certains appareils",
cat6Title: "Substances toxiques", cat6Title: "Substances toxiques",
cat6Desc: "Poisons, pesticides, substances infectieuses", cat6Desc: "Poisons, pesticides, substances infectieuses, substances illégales",
cat7Title: "Matières corrosives", cat7Title: "Matières corrosives",
cat7Desc: "Acides, mercure, soude caustique", cat7Desc: "Acides, mercure, soude caustique",
cat8Title: "Matières radioactives", cat8Title: "Matières radioactives",
@ -383,7 +383,7 @@ const translations = {
pricing: "Pricing", pricing: "Pricing",
serviceCommande: "Order Service", serviceCommande: "Order Service",
guide: "Shipping Guide", guide: "Shipping Guide",
contact: "Contact", contact: "Sign Up",
app: "Coming Soon" app: "Coming Soon"
}, },
home: { home: {
@ -496,8 +496,8 @@ const translations = {
ctaBtn: "Contact Us" ctaBtn: "Contact Us"
}, },
contact: { contact: {
heroTitle: "Contact Us", heroTitle: "Sign Up",
heroSubtitle: "Register and start sending your parcels today", heroSubtitle: "Start sending your parcels today",
formTitle: "Registration Form", formTitle: "Registration Form",
formSubtitle: "Fill out this form to receive your client reference number and the Paris drop-off address.", formSubtitle: "Fill out this form to receive your client reference number and the Paris drop-off address.",
labelNom: "Last Name", labelNom: "Last Name",
@ -551,7 +551,7 @@ const translations = {
prohibitedTitle: "Prohibited Items", prohibitedTitle: "Prohibited Items",
prohibitedSubtitle: "These items cannot be transported by air freight (IATA regulations)", prohibitedSubtitle: "These items cannot be transported by air freight (IATA regulations)",
cat1Title: "Explosives", cat1Title: "Explosives",
cat1Desc: "Dynamite, ammunition, fireworks, firecrackers", cat1Desc: "Dynamite, ammunition, fireworks, firecrackers, firearms",
cat2Title: "Compressed Gases", cat2Title: "Compressed Gases",
cat2Desc: "Gas cylinders, pressurized aerosols, butane, propane", cat2Desc: "Gas cylinders, pressurized aerosols, butane, propane",
cat3Title: "Flammable Liquids", cat3Title: "Flammable Liquids",
@ -561,7 +561,7 @@ const translations = {
cat5Title: "Lithium Batteries", cat5Title: "Lithium Batteries",
cat5Desc: "Loose lithium batteries, hoverboards, certain devices", cat5Desc: "Loose lithium batteries, hoverboards, certain devices",
cat6Title: "Toxic Substances", cat6Title: "Toxic Substances",
cat6Desc: "Poisons, pesticides, infectious substances", cat6Desc: "Poisons, pesticides, infectious substances, illegal substances",
cat7Title: "Corrosive Materials", cat7Title: "Corrosive Materials",
cat7Desc: "Acids, mercury, caustic soda", cat7Desc: "Acids, mercury, caustic soda",
cat8Title: "Radioactive Materials", cat8Title: "Radioactive Materials",
@ -745,7 +745,7 @@ const translations = {
pricing: "Sarany", pricing: "Sarany",
serviceCommande: "Tolotra Fividianana", serviceCommande: "Tolotra Fividianana",
guide: "Toromarika fandefasana", guide: "Toromarika fandefasana",
contact: "Fifandraisana", contact: "Fisoratana anarana",
app: "Avy tsy ho ela" app: "Avy tsy ho ela"
}, },
home: { home: {
@ -858,8 +858,8 @@ const translations = {
ctaBtn: "Mifandraisa aminay" ctaBtn: "Mifandraisa aminay"
}, },
contact: { contact: {
heroTitle: "Mifandraisa Aminay", heroTitle: "Misoratra anarana",
heroSubtitle: "Misoratra anarana ary manomboha mandefa ny entanareo anio", heroSubtitle: "Manomboha mandefa ny entanareo anio",
formTitle: "Taratasy fisoratana anarana", formTitle: "Taratasy fisoratana anarana",
formSubtitle: "Fenoy ity taratasy ity mba handraisana ny laharan'ny mpanjifa sy ny adiresy fametrahana any Paris.", formSubtitle: "Fenoy ity taratasy ity mba handraisana ny laharan'ny mpanjifa sy ny adiresy fametrahana any Paris.",
labelNom: "Anarana", labelNom: "Anarana",
@ -913,7 +913,7 @@ const translations = {
prohibitedTitle: "Entana Voarara", prohibitedTitle: "Entana Voarara",
prohibitedSubtitle: "Ireto entana ireto dia tsy azo alefa amin'ny fandefasana entana an'habakabaka (fitsipika IATA)", prohibitedSubtitle: "Ireto entana ireto dia tsy azo alefa amin'ny fandefasana entana an'habakabaka (fitsipika IATA)",
cat1Title: "Zavatra mipoaka", cat1Title: "Zavatra mipoaka",
cat1Desc: "Dynamita, bala, afo artifisialy, petarada", cat1Desc: "Dynamita, bala, afo artifisialy, petarada, basy",
cat2Title: "Gazy voatery", cat2Title: "Gazy voatery",
cat2Desc: "Tavoahangy gazy, aérosol misy tsindry, butane, propane", cat2Desc: "Tavoahangy gazy, aérosol misy tsindry, butane, propane",
cat3Title: "Ranoka mampirehitra", cat3Title: "Ranoka mampirehitra",
@ -923,7 +923,7 @@ const translations = {
cat5Title: "Bateria lithium", cat5Title: "Bateria lithium",
cat5Desc: "Bateria lithium irery, hoverboards, fitaovana sasany", cat5Desc: "Bateria lithium irery, hoverboards, fitaovana sasany",
cat6Title: "Zava-mahapoizina", cat6Title: "Zava-mahapoizina",
cat6Desc: "Poizina, fanafody bibikely, zava-mifindra", cat6Desc: "Poizina, fanafody bibikely, zava-mifindra, zavatra tsy ara-dalàna",
cat7Title: "Zava-mandevona", cat7Title: "Zava-mandevona",
cat7Desc: "Asida, mercure, soude caustique", cat7Desc: "Asida, mercure, soude caustique",
cat8Title: "Zava-misy radioactivité", cat8Title: "Zava-misy radioactivité",

View File

@ -47,7 +47,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -67,13 +67,8 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -278,7 +273,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
<li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li> <li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li>
</ul> </ul>
</div> </div>
@ -305,7 +300,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@ -56,7 +56,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -76,13 +76,8 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -449,7 +444,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
<li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li> <li><a href="cgv.html" style="color:rgba(255,255,255,0.7);" data-i18n="footer.cgv">Conditions Générales de Vente</a></li>
</ul> </ul>
</div> </div>
@ -476,7 +471,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

81
reset-password.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Réinitialisation du mot de passe — MVA Global Fret</title>
<link rel="icon" type="image/png" href="PNG MVA GLOBAL FRET.png">
<script>
// Bridge mobile deep link MVA : redirect vers le custom scheme natif
// mvaglobalfret://reset-password pour ouvrir le flow in-app de l'app Expo.
// Le lien email porte token_hash + type (recovery) ; l'app fait verifyOtp.
(function() {
var params = new URLSearchParams(window.location.search);
var tokenHash = params.get('token_hash');
var type = params.get('type') || 'recovery';
if (tokenHash) {
window.location.replace(
'mvaglobalfret://reset-password?token_hash=' +
encodeURIComponent(tokenHash) + '&type=' + encodeURIComponent(type)
);
}
})();
</script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
padding: 2rem;
color: #333;
max-width: 480px;
margin: 0 auto;
}
h1 { color: #2d3748; font-size: 1.5rem; }
p { line-height: 1.6; }
a.cta {
display: inline-block;
background: #c5a55a;
color: #fff;
padding: 0.75rem 1.5rem;
border-radius: 6px;
text-decoration: none;
font-weight: 600;
margin-top: 1rem;
}
a.cta:hover { background: #b3954a; }
.hint { color: #666; font-size: 0.9rem; margin-top: 2rem; }
.error { color: #b91c1c; font-size: 0.95rem; line-height: 1.6; margin-top: 1rem; }
</style>
</head>
<body>
<h1>Ouvrir l'app MVA Global Fret</h1>
<p id="intro">Pour réinitialiser votre mot de passe, ouvrez le lien ci-dessous dans l'application MVA Global Fret installée sur votre téléphone.</p>
<p id="link-wrap">
<a id="manual-link" class="cta" href="#">Réinitialiser mon mot de passe</a>
</p>
<p id="hint" class="hint">Si rien ne se passe, vérifiez que l'application MVA Global Fret est bien installée sur votre appareil.</p>
<p id="error" class="error" style="display:none;">Ce lien de réinitialisation est invalide ou incomplet. Veuillez relancer la procédure « Mot de passe oublié » depuis l'application MVA Global Fret.</p>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var tokenHash = params.get('token_hash');
var type = params.get('type') || 'recovery';
if (!tokenHash) {
// Lien invalide : pas de token_hash → on masque le bouton et on prévient.
document.getElementById('intro').style.display = 'none';
document.getElementById('link-wrap').style.display = 'none';
document.getElementById('hint').style.display = 'none';
document.getElementById('error').style.display = 'block';
return;
}
// Deep link vers l'app : l'app lit token_hash + type et fait verifyOtp.
document.getElementById('manual-link').href =
'mvaglobalfret://reset-password?token_hash=' +
encodeURIComponent(tokenHash) + '&type=' + encodeURIComponent(type);
})();
</script>
</body>
</html>

10
robots.txt Normal file
View File

@ -0,0 +1,10 @@
User-agent: *
Allow: /
# Disallow non-indexable pages
Disallow: /confirmation.html
Disallow: /reset-password.html
Disallow: /cloudflare-worker/
Disallow: /.github/
Sitemap: https://mva-globalfret.com/sitemap.xml

View File

@ -25,7 +25,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -45,13 +45,8 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -247,7 +242,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -273,7 +268,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>

286
setup-password.html Normal file
View File

@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Créez votre compte — MVA Global Fret</title>
<link rel="icon" type="image/png" href="PNG MVA GLOBAL FRET.png">
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #1a1a3e 0%, #2d2d5e 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.card {
background: #fff;
border-radius: 16px;
max-width: 480px;
width: 100%;
padding: 2.5rem 2rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.header {
text-align: center;
margin-bottom: 1.5rem;
}
.header .brand {
color: #c5a55a;
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 0.25rem;
}
.header .subtitle {
color: #7a7a8a;
font-size: 13px;
}
h1 {
color: #1a1a3e;
font-size: 1.5rem;
margin: 1.5rem 0 0.5rem;
text-align: center;
}
.ref-badge {
background: #f0ead8;
border-left: 4px solid #c5a55a;
padding: 12px 16px;
border-radius: 4px;
margin: 1rem 0 1.5rem;
font-size: 14px;
color: #1a1a3e;
}
.ref-badge strong { letter-spacing: 1px; }
label {
display: block;
color: #1a1a3e;
font-size: 13px;
font-weight: 600;
margin: 1rem 0 0.4rem;
}
input[type="email"], input[type="password"] {
width: 100%;
padding: 0.7rem 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 8px;
font-size: 15px;
font-family: inherit;
}
input[disabled] { background: #f7f7f9; color: #6b6b75; cursor: not-allowed; }
input:focus { outline: none; border-color: #c5a55a; box-shadow: 0 0 0 3px rgba(197, 165, 90, 0.18); }
.hint {
color: #7a7a8a;
font-size: 12px;
margin-top: 0.35rem;
line-height: 1.4;
}
button {
width: 100%;
margin-top: 1.5rem;
padding: 0.85rem;
background: #c5a55a;
color: #1a1a3e;
border: 0;
border-radius: 50px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
}
button:hover:not(:disabled) { background: #b3954a; }
button:disabled { opacity: 0.6; cursor: wait; }
.alert {
padding: 0.85rem 1rem;
border-radius: 8px;
font-size: 14px;
margin: 1rem 0;
line-height: 1.5;
}
.alert-error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
.alert-success { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
.footer {
margin-top: 1.75rem;
text-align: center;
font-size: 12px;
color: #9ca3af;
}
.footer a { color: #c5a55a; text-decoration: none; }
.loading {
text-align: center;
padding: 2rem;
color: #7a7a8a;
}
.hidden { display: none; }
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="brand">MVA GLOBAL FRET</div>
<div class="subtitle">Fret Aérien Paris — Antananarivo</div>
</div>
<div id="loading" class="loading">Chargement du lien…</div>
<div id="error-state" class="hidden">
<h1>Lien invalide</h1>
<div id="error-message" class="alert alert-error"></div>
<p class="footer">
Si le problème persiste, contactez-nous via <a href="contact.html">la page contact</a>.
</p>
</div>
<div id="success-state" class="hidden">
<h1>Compte créé ✓</h1>
<div class="alert alert-success">
Votre compte MVA Global Fret a été créé avec succès.
Téléchargez l'application sur votre téléphone pour commander :
</div>
<p style="text-align:center;margin:1rem 0;">
<a href="application.html" style="color:#c5a55a;font-weight:600;text-decoration:none;">
📱 Télécharger l'application
</a>
</p>
</div>
<form id="setup-form" class="hidden" novalidate>
<h1>Créez votre compte</h1>
<p id="welcome-text" style="color:#1a1a3e;text-align:center;font-size:14px;margin-bottom:0;"></p>
<div id="ref-display" class="ref-badge hidden">
Référence client : <strong id="ref-value"></strong>
</div>
<label for="email-display">Email</label>
<input id="email-display" type="email" disabled>
<label for="password">Mot de passe</label>
<input id="password" type="password" autocomplete="new-password" required minlength="8" maxlength="72">
<div class="hint">Minimum 8 caractères, avec au moins une majuscule et un chiffre.</div>
<label for="password-confirm">Confirmer le mot de passe</label>
<input id="password-confirm" type="password" autocomplete="new-password" required>
<div id="form-error" class="alert alert-error hidden"></div>
<button type="submit" id="submit-btn">Créer mon compte</button>
<p class="footer">
En créant votre compte, vous acceptez nos <a href="cgv.html">CGV</a>
et notre <a href="politique-confidentialite.html">politique de confidentialité</a>.
</p>
</form>
</div>
<script>
(function() {
const API_BASE = "https://api.mva.mind4solutions.com";
const token = new URLSearchParams(window.location.search).get('token');
const $ = (id) => document.getElementById(id);
const show = (id) => $(id).classList.remove('hidden');
const hide = (id) => $(id).classList.add('hidden');
function showError(message) {
hide('loading');
hide('setup-form');
$('error-message').textContent = message;
show('error-state');
}
function showFormError(message) {
$('form-error').textContent = message;
show('form-error');
}
async function init() {
if (!token || token.length !== 96 || !/^[a-f0-9]+$/i.test(token)) {
showError("Lien invalide ou incomplet.");
return;
}
try {
const res = await fetch(API_BASE + "/auth/lookup-setup-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const data = await res.json();
if (!res.ok) {
showError(data.message || "Lien invalide.");
return;
}
$('email-display').value = data.email || '';
$('welcome-text').textContent = data.firstname
? `Bonjour ${data.firstname}, choisissez votre mot de passe pour activer votre compte.`
: "Choisissez votre mot de passe pour activer votre compte.";
if (data.reference_client) {
$('ref-value').textContent = data.reference_client;
show('ref-display');
}
hide('loading');
show('setup-form');
} catch (err) {
showError("Impossible de joindre le serveur. Vérifiez votre connexion.");
}
}
$('setup-form').addEventListener('submit', async (e) => {
e.preventDefault();
hide('form-error');
const password = $('password').value;
const confirm = $('password-confirm').value;
if (password.length < 8) {
showFormError("Le mot de passe doit contenir au moins 8 caractères.");
return;
}
if (!/[A-Z]/.test(password) || !/[0-9]/.test(password)) {
showFormError("Le mot de passe doit contenir au moins une majuscule et un chiffre.");
return;
}
if (password !== confirm) {
showFormError("Les deux mots de passe ne correspondent pas.");
return;
}
const btn = $('submit-btn');
btn.disabled = true;
btn.textContent = "Création en cours…";
try {
const res = await fetch(API_BASE + "/auth/setup-from-lead", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, password }),
});
const data = await res.json();
if (!res.ok) {
btn.disabled = false;
btn.textContent = "Créer mon compte";
showFormError(data.message || "Erreur lors de la création du compte.");
return;
}
hide('setup-form');
show('success-state');
} catch (err) {
btn.disabled = false;
btn.textContent = "Créer mon compte";
showFormError("Impossible de joindre le serveur. Réessayez dans un instant.");
}
});
init();
})();
</script>
</body>
</html>

69
sitemap.xml Normal file
View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://mva-globalfret.com/</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://mva-globalfret.com/accueil.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://mva-globalfret.com/about.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://mva-globalfret.com/tarifs.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://mva-globalfret.com/service-commande.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://mva-globalfret.com/guide-envoi.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://mva-globalfret.com/contact.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://mva-globalfret.com/application.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://mva-globalfret.com/cgv.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://mva-globalfret.com/mentions-legales.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://mva-globalfret.com/politique-confidentialite.html</loc>
<lastmod>2026-05-07</lastmod>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@ -25,7 +25,7 @@
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a> <a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
</nav> </nav>
<div class="header-right"> <div class="header-right">
@ -43,14 +43,10 @@
<a href="accueil.html" data-i18n="nav.home">Accueil</a> <a href="accueil.html" data-i18n="nav.home">Accueil</a>
<a href="about.html" data-i18n="nav.about">Qui sommes-nous</a> <a href="about.html" data-i18n="nav.about">Qui sommes-nous</a>
<a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a> <a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a>
<a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a>
<a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a> <a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a>
<a href="contact.html" data-i18n="nav.contact">Contact</a> <a href="contact.html" data-i18n="nav.contact">Inscription</a>
<a href="application.html" data-i18n="nav.app">Prochainement</a> <a href="application.html" data-i18n="nav.app">Prochainement</a>
<div class="lang-switcher" style="margin-top:16px">
<button data-lang="fr" class="active">FR</button>
<button data-lang="en">EN</button>
<button data-lang="mg">MG</button>
</div>
</nav> </nav>
<div class="overlay" id="overlay"></div> <div class="overlay" id="overlay"></div>
@ -262,7 +258,7 @@
<li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li> <li><a href="tarifs.html" data-i18n="nav.pricing">Tarifs</a></li>
<li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li> <li><a href="service-commande.html" data-i18n="nav.serviceCommande">Service Commande</a></li>
<li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li> <li><a href="guide-envoi.html" data-i18n="nav.guide">Guide d'envoi</a></li>
<li><a href="contact.html" data-i18n="nav.contact">Contact</a></li> <li><a href="contact.html" data-i18n="nav.contact">Inscription</a></li>
</ul> </ul>
</div> </div>
<div> <div>
@ -288,7 +284,7 @@
</div> </div>
</footer> </footer>
<script src="js/translations.js"></script> <script src="js/translations.js?v=20260603"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>