Contact form: split name into first/last, add ntfy notification
- First and last name are now separate required fields - Server combines them into a full name for emails - Sends a push notification to NTFY_URL on new inquiry (fire-and-forget) - NTFY_URL env var wired through docker-compose Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cd18bd3937
commit
77318fb477
@ -25,3 +25,4 @@ SMTP_SECURE=false
|
|||||||
SMTP_USER=info@beachpartyballoons.com
|
SMTP_USER=info@beachpartyballoons.com
|
||||||
SMTP_PASS=
|
SMTP_PASS=
|
||||||
CONTACT_TO=info@beachpartyballoons.com
|
CONTACT_TO=info@beachpartyballoons.com
|
||||||
|
NTFY_URL=https://ntfy.example.com/your-topic
|
||||||
|
|||||||
@ -31,6 +31,7 @@ services:
|
|||||||
SMTP_USER: ${SMTP_USER}
|
SMTP_USER: ${SMTP_USER}
|
||||||
SMTP_PASS: ${SMTP_PASS}
|
SMTP_PASS: ${SMTP_PASS}
|
||||||
CONTACT_TO: ${CONTACT_TO}
|
CONTACT_TO: ${CONTACT_TO}
|
||||||
|
NTFY_URL: ${NTFY_URL:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ./main-site/update.json:/usr/src/app/update.json
|
- ./main-site/update.json:/usr/src/app/update.json
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@ -47,9 +47,13 @@
|
|||||||
function validate() {
|
function validate() {
|
||||||
let ok = true;
|
let ok = true;
|
||||||
|
|
||||||
const name = form.querySelector('[name="name"]');
|
const firstName = form.querySelector('[name="firstName"]');
|
||||||
if (!name.value.trim()) { setErr(name, 'Please enter your name.'); ok = false; }
|
if (!firstName.value.trim()) { setErr(firstName, 'Please enter your first name.'); ok = false; }
|
||||||
else clearErr(name);
|
else clearErr(firstName);
|
||||||
|
|
||||||
|
const lastName = form.querySelector('[name="lastName"]');
|
||||||
|
if (!lastName.value.trim()) { setErr(lastName, 'Please enter your last name.'); ok = false; }
|
||||||
|
else clearErr(lastName);
|
||||||
|
|
||||||
const email = form.querySelector('[name="email"]');
|
const email = form.querySelector('[name="email"]');
|
||||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) {
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) {
|
||||||
@ -131,7 +135,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('name', form.querySelector('[name="name"]').value.trim());
|
fd.append('firstName', form.querySelector('[name="firstName"]').value.trim());
|
||||||
|
fd.append('lastName', form.querySelector('[name="lastName"]').value.trim());
|
||||||
fd.append('email', form.querySelector('[name="email"]').value.trim());
|
fd.append('email', form.querySelector('[name="email"]').value.trim());
|
||||||
fd.append('phone', form.querySelector('[name="phone"]').value.trim());
|
fd.append('phone', form.querySelector('[name="phone"]').value.trim());
|
||||||
fd.append('message', form.querySelector('[name="message"]').value.trim());
|
fd.append('message', form.querySelector('[name="message"]').value.trim());
|
||||||
|
|||||||
@ -47,12 +47,25 @@
|
|||||||
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Name <span class="has-text-danger">*</span></label>
|
<label class="label">First name <span class="has-text-danger">*</span></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="name" placeholder="Your name" required autocomplete="name">
|
<input class="input" type="text" name="firstName" placeholder="Jane" required autocomplete="given-name">
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" id="err-firstName" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Last name <span class="has-text-danger">*</span></label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="lastName" placeholder="Smith" required autocomplete="family-name">
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" id="err-lastName" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" id="err-name" style="display:none;"></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@ -79,12 +79,25 @@
|
|||||||
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Name <span class="has-text-danger">*</span></label>
|
<label class="label">First name <span class="has-text-danger">*</span></label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<input class="input" type="text" name="name" placeholder="Your name" required autocomplete="name">
|
<input class="input" type="text" name="firstName" placeholder="Jane" required autocomplete="given-name">
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" id="err-firstName" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Last name <span class="has-text-danger">*</span></label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" name="lastName" placeholder="Smith" required autocomplete="family-name">
|
||||||
|
</div>
|
||||||
|
<p class="help is-danger" id="err-lastName" style="display:none;"></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help is-danger" id="err-name" style="display:none;"></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@ -117,9 +117,10 @@ apiRouter.post('/contact', upload.array('photos', 3), async (req, res) => {
|
|||||||
return res.json({ success: true });
|
return res.json({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, email, phone, message, eventType, eventDate } = req.body;
|
const { firstName, lastName, email, phone, message, eventType, eventDate } = req.body;
|
||||||
|
const name = [firstName, lastName].filter(Boolean).join(' ');
|
||||||
|
|
||||||
if (!name || !email || !phone || !message) {
|
if (!firstName || !lastName || !email || !phone || !message) {
|
||||||
return res.status(400).json({ success: false, message: 'Please fill in all required fields.' });
|
return res.status(400).json({ success: false, message: 'Please fill in all required fields.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +183,23 @@ apiRouter.post('/contact', upload.array('photos', 3), async (req, res) => {
|
|||||||
transporter.sendMail(autoReply).catch(err =>
|
transporter.sendMail(autoReply).catch(err =>
|
||||||
console.error(`[${new Date().toISOString()}] Auto-reply failed:`, err)
|
console.error(`[${new Date().toISOString()}] Auto-reply failed:`, err)
|
||||||
);
|
);
|
||||||
|
if (process.env.NTFY_URL) {
|
||||||
|
const ntfyBody = [
|
||||||
|
phone,
|
||||||
|
eventDateFormatted || null,
|
||||||
|
eventType || null,
|
||||||
|
message.slice(0, 100) + (message.length > 100 ? '…' : ''),
|
||||||
|
].filter(Boolean).join(' · ');
|
||||||
|
fetch(process.env.NTFY_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Title': `🎈 New inquiry — ${name}`,
|
||||||
|
'Priority': 'default',
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
},
|
||||||
|
body: ntfyBody,
|
||||||
|
}).catch(err => console.error(`[${new Date().toISOString()}] ntfy failed:`, err));
|
||||||
|
}
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[${new Date().toISOString()}] Contact form mail error:`, err);
|
console.error(`[${new Date().toISOString()}] Contact form mail error:`, err);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user