feat(contact): ajouter un backend pour le traitement des formulaires de contact avec validation et envoi d'e-mails
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
# Configuration SMTP pour contact-backend.js
|
||||||
|
SMTP_HOST=HOST
|
||||||
|
SMTP_PORT=PORT
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=USER
|
||||||
|
SMTP_FROM=FROM
|
||||||
|
SMTP_PASS=PASSWORD
|
||||||
|
PORT=PORT
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Backend minimal Node.js pour traiter le formulaire de contact Hugo
|
||||||
|
// Place ce fichier à la racine du projet ou dans un dossier backend/
|
||||||
|
// Nécessite: npm install express nodemailer cors
|
||||||
|
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const cors = require('cors');
|
||||||
|
const multer = require('multer');
|
||||||
|
const upload = multer();
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
// NE PAS utiliser express.urlencoded/json pour /contact, sinon req.body sera vide avec FormData
|
||||||
|
|
||||||
|
// Configurer le transporteur SMTP depuis .env
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: parseInt(process.env.SMTP_PORT, 10),
|
||||||
|
secure: process.env.SMTP_SECURE === 'true',
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/contact', upload.none(), async (req, res) => {
|
||||||
|
// Vérification de la présence de req.body
|
||||||
|
if (!req.body) {
|
||||||
|
console.log(req.body);
|
||||||
|
return res.status(400).send('Aucune donnée reçue.');
|
||||||
|
}
|
||||||
|
// Anti-robot honeypot
|
||||||
|
if (req.body.website && req.body.website.trim() !== '') {
|
||||||
|
return res.status(400).send('Bot détecté.');
|
||||||
|
}
|
||||||
|
const { email, sujet, message } = req.body;
|
||||||
|
if (!email || !sujet || !message) {
|
||||||
|
return res.status(400).send('Champs manquants.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: process.env.SMTP_USER,
|
||||||
|
to: process.env.SMTP_USER,
|
||||||
|
replyTo: email,
|
||||||
|
subject: `Contact site: ${sujet}`,
|
||||||
|
text: `Email: ${email}\nSujet: ${sujet}\nMessage:\n${message}`
|
||||||
|
});
|
||||||
|
res.status(200).send('Message envoyé !');
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send("Erreur d'envoi: " + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Serveur contact en écoute sur http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
+11
-3
@@ -2,11 +2,19 @@
|
|||||||
title: Contact
|
title: Contact
|
||||||
layout: "simple"
|
layout: "simple"
|
||||||
contactForm:
|
contactForm:
|
||||||
|
action: "http://localhost:3001/contact"
|
||||||
fields:
|
fields:
|
||||||
- name: "name"
|
- name: "sujet"
|
||||||
label: "Nom complet"
|
label: "Sujet"
|
||||||
type: "text"
|
type: "select"
|
||||||
required: true
|
required: true
|
||||||
|
options:
|
||||||
|
- value: "support"
|
||||||
|
label: "Support technique"
|
||||||
|
- value: "vente"
|
||||||
|
label: "Demande commerciale"
|
||||||
|
- value: "autre"
|
||||||
|
label: "Autre"
|
||||||
- name: "email"
|
- name: "email"
|
||||||
label: "Adresse email"
|
label: "Adresse email"
|
||||||
type: "email"
|
type: "email"
|
||||||
|
|||||||
+8
-1
@@ -18,5 +18,12 @@
|
|||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-cli": "^10.1.0",
|
"postcss-cli": "^10.1.0",
|
||||||
"tailwindcss": "^3.3.5"
|
"tailwindcss": "^3.3.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.6",
|
||||||
|
"dotenv": "^17.2.4",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"nodemailer": "^8.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2902,6 +2902,11 @@ body {
|
|||||||
background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1));
|
background-color: rgb(234 179 8 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-blue-500 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-gradient-to-b {
|
.bg-gradient-to-b {
|
||||||
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
|
background-image: linear-gradient(to bottom, var(--tw-gradient-stops));
|
||||||
}
|
}
|
||||||
@@ -4399,6 +4404,11 @@ body {
|
|||||||
background-color: rgb(140 41 8 / var(--tw-bg-opacity, 1));
|
background-color: rgb(140 41 8 / var(--tw-bg-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:bg-blue-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:text-gray-900:hover {
|
.hover\:text-gray-900:hover {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||||
|
|||||||
@@ -1,18 +1,60 @@
|
|||||||
{{ $form := .Page.Params.contactForm }}
|
{{ $form := .Page.Params.contactForm }}
|
||||||
<form class="max-w-lg mx-auto p-6 bg-white rounded shadow" method="POST" action="/contact">
|
|
||||||
|
<div id="contact-result" class="max-w-lg mx-auto mb-4"></div>
|
||||||
|
<form id="contactForm" class="max-w-lg mx-auto p-6 bg-white rounded shadow" method="POST" action="{{ $form.action | default "http://localhost:3001/contact" }}">
|
||||||
{{ range $form.fields }}
|
{{ range $form.fields }}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="{{ .name }}" class="block text-gray-700 font-bold mb-2">{{ .label }}</label>
|
<label for="{{ .name }}" class="block text-gray-700 font-bold mb-2">{{ .label }}</label>
|
||||||
{{ if eq .type "textarea" }}
|
{{ if eq .type "textarea" }}
|
||||||
<textarea id="{{ .name }}" name="{{ .name }}" rows="5" {{ if .required }}required{{ end }} class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300"></textarea>
|
<textarea id="{{ .name }}" name="{{ .name }}" rows="5" {{ if .required }}required{{ end }} class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300"></textarea>
|
||||||
|
{{ else if eq .type "select" }}
|
||||||
|
<select id="{{ .name }}" name="{{ .name }}" {{ if .required }}required{{ end }} class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300">
|
||||||
|
{{ range .options }}
|
||||||
|
<option value="{{ .value }}">{{ .label | default .value }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<input type="{{ .type }}" id="{{ .name }}" name="{{ .name }}" {{ if .required }}required{{ end }} class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300" />
|
<input type="{{ .type }}" id="{{ .name }}" name="{{ .name }}" {{ if .required }}required{{ end }} class="w-full px-3 py-2 border rounded focus:outline-none focus:ring focus:border-blue-300" />
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="flex justify-end mt-4">
|
<!-- Champ honeypot invisible pour les robots -->
|
||||||
<button type="submit" class="px-6 py-3 rounded-lg font-bold transition duration-200 ease-in-out border-2 text-primary-400 border-primary-400 hover:border-primary-400 hover:text-primary-400 hover:scale-105">
|
<div style="position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden;">
|
||||||
{{ $form.button.label | default "Envoyer" }}
|
<label for="website">Ne pas remplir ce champ</label>
|
||||||
</button>
|
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- (Plus de champ caché Formspree, tout est géré côté backend) -->
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<button type="submit" class="px-6 py-3 rounded-lg font-bold transition duration-200 ease-in-out border-2 text-primary-400 border-primary-400 hover:border-primary-400 hover:text-primary-400 hover:scale-105">
|
||||||
|
{{ $form.button.label | default "Envoyer" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const form = document.getElementById('contactForm');
|
||||||
|
const resultDiv = document.getElementById('contact-result');
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
resultDiv.innerHTML = '';
|
||||||
|
const formData = new FormData(form);
|
||||||
|
console.log('Submitting form to:', formData);
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(async response => {
|
||||||
|
const text = await response.text();
|
||||||
|
if (response.status === 200) {
|
||||||
|
resultDiv.innerHTML = `<div style='background:#e6ffed;color:#228B22;padding:1em;border-radius:6px;border:1px solid #228B22;'>${text}</div>`;
|
||||||
|
form.reset();
|
||||||
|
} else {
|
||||||
|
resultDiv.innerHTML = `<div style='background:#ffe6e6;color:#b22222;padding:1em;border-radius:6px;border:1px solid #b22222;'>${text}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
resultDiv.innerHTML = `<div style='background:#ffe6e6;color:#b22222;padding:1em;border-radius:6px;border:1px solid #b22222;'>Erreur réseau</div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user