Send email on RSVP
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"isomorphic-style-loader": "^1.1.0",
|
"isomorphic-style-loader": "^1.1.0",
|
||||||
"jsonwebtoken": "^7.2.1",
|
"jsonwebtoken": "^7.2.1",
|
||||||
"node-fetch": "^1.6.3",
|
"node-fetch": "^1.6.3",
|
||||||
|
"nodemailer": "^3.1.3",
|
||||||
"normalize.css": "^5.0.0",
|
"normalize.css": "^5.0.0",
|
||||||
"passport": "^0.3.2",
|
"passport": "^0.3.2",
|
||||||
"passport-facebook": "^2.1.1",
|
"passport-facebook": "^2.1.1",
|
||||||
|
|||||||
62
src/core/emailer.js
Normal file
62
src/core/emailer.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Nodemailer from 'nodemailer';
|
||||||
|
import { email, starters, mains, getRsvpEnd } from '../config';
|
||||||
|
import { handleError } from '../core/rollbar';
|
||||||
|
|
||||||
|
const transporter = Nodemailer.createTransport({
|
||||||
|
service: 'gmail',
|
||||||
|
auth: {
|
||||||
|
user: email.senderAddress,
|
||||||
|
pass: email.password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debounce sending the email to avoid spamming
|
||||||
|
const emailTimers = {};
|
||||||
|
|
||||||
|
function getEmailText(person) {
|
||||||
|
return `Dear ${person.firstname}.\n\n
|
||||||
|
Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.\n
|
||||||
|
${person.attending ? `Your meal choices are ${starters[person.starter]} and ${mains[person.main]}.\n` : ''}
|
||||||
|
${person.attending && person.dietary.length > 0 ? `Your dietary requirements are: ${person.dietary}.\n` : ''}
|
||||||
|
You can modify your RSVP up until ${getRsvpEnd()} here: https://danielandhana.com/${person.password}.\n
|
||||||
|
${person.attending ? 'See you at our wedding,' : 'See you soon,'}\n
|
||||||
|
Daniel and Hana
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEmailHTML(person) {
|
||||||
|
return `<p>Dear ${person.firstname}.</p>
|
||||||
|
<p>Thanks for your RSVP, ${person.attending ? 'we\'re delighted to hear you will be attending' : 'we\'re sorry to hear you will not be attending'}.</p>
|
||||||
|
${person.attending ? `<p>Your meal choices are <b>${starters[person.starter]}</b> and <b>${mains[person.main]}</b>.</p>` : ''}
|
||||||
|
${person.attending && person.dietary.length > 0 ? `<p>Your dietary requirements are: <b>${person.dietary}</b>.</p>` : ''}
|
||||||
|
<p>You can modify your RSVP up until <b>${getRsvpEnd()}</b> <a href="https://danielandhana.com/${person.password}">here</a>.</p>
|
||||||
|
<p>${person.attending ? 'See you at our wedding,' : 'See you soon,'}</p>
|
||||||
|
<p>Daniel and Hana</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendRSVPEmail = function sendRSVPEmail(person) {
|
||||||
|
const mailOptions = {
|
||||||
|
from: `"${email.senderName}" <${email.senderAddress}>`,
|
||||||
|
to: person.email,
|
||||||
|
subject: 'Thank you for your RSVP',
|
||||||
|
text: getEmailText(person),
|
||||||
|
html: getEmailHTML(person),
|
||||||
|
};
|
||||||
|
// send mail with defined transport object
|
||||||
|
transporter.sendMail(mailOptions, (error) => {
|
||||||
|
if (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendRSVPEmailWithDebounce = function sendRSVPEmailWithDebounce(person) {
|
||||||
|
if (person.key) {
|
||||||
|
// Stop previous email
|
||||||
|
clearTimeout(emailTimers[person.key]);
|
||||||
|
emailTimers[person.key] = setTimeout(() => {
|
||||||
|
sendRSVPEmail(person);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,40 +3,14 @@ import Layout from '../../components/Layout';
|
|||||||
import fetchQL from '../../core/fetchQL';
|
import fetchQL from '../../core/fetchQL';
|
||||||
import Rsvp from './Rsvp';
|
import Rsvp from './Rsvp';
|
||||||
import NotFound from '../notFound/NotFound';
|
import NotFound from '../notFound/NotFound';
|
||||||
import { rsvpEndDate } from '../../config';
|
import { rsvpEndDate, getRsvpEnd } from '../../config';
|
||||||
|
|
||||||
let title = 'RSVP';
|
let title = 'RSVP';
|
||||||
|
|
||||||
const months = [
|
|
||||||
'January',
|
|
||||||
'February',
|
|
||||||
'March',
|
|
||||||
'April',
|
|
||||||
'May',
|
|
||||||
'June',
|
|
||||||
'July',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'October',
|
|
||||||
'November',
|
|
||||||
'December',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
path: '/rsvp/:password',
|
path: '/rsvp/:password',
|
||||||
|
|
||||||
getRsvpEnd() {
|
|
||||||
const date = rsvpEndDate.getDate();
|
|
||||||
let dateSuffix = 'th';
|
|
||||||
if (date === 1 || date === 21 || date === 31) { dateSuffix = 'st'; }
|
|
||||||
if (date === 2 || date === 22) { dateSuffix = 'nd'; }
|
|
||||||
if (date === 3 || date === 23) { dateSuffix = 'rd'; }
|
|
||||||
const month = months[rsvpEndDate.getMonth()];
|
|
||||||
const year = rsvpEndDate.getFullYear();
|
|
||||||
return `${date}${dateSuffix} ${month} ${year}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
isEditable() {
|
isEditable() {
|
||||||
return new Date() < rsvpEndDate;
|
return new Date() < rsvpEndDate;
|
||||||
},
|
},
|
||||||
@@ -68,7 +42,7 @@ export default {
|
|||||||
<Rsvp
|
<Rsvp
|
||||||
title={title}
|
title={title}
|
||||||
people={people}
|
people={people}
|
||||||
rsvpEnd={this.getRsvpEnd()}
|
rsvpEnd={getRsvpEnd()}
|
||||||
isEditable={this.isEditable()}
|
isEditable={this.isEditable()}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import Person from './data/models/Person';
|
|||||||
import Rollbar from './core/rollbar';
|
import Rollbar from './core/rollbar';
|
||||||
import { sendSlackMsg, sendSlackMsgWithDebounce } from './core/slack';
|
import { sendSlackMsg, sendSlackMsgWithDebounce } from './core/slack';
|
||||||
import sequelize from './data/sequelize';
|
import sequelize from './data/sequelize';
|
||||||
|
import { sendRSVPEmailWithDebounce } from './core/emailer';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -170,6 +171,7 @@ app.post('/rsvp/save', (req, res) => {
|
|||||||
const dietary = (data.attending && data.dietary) ? `\nDietary requirements: ${data.dietary}` : '';
|
const dietary = (data.attending && data.dietary) ? `\nDietary requirements: ${data.dietary}` : '';
|
||||||
|
|
||||||
sendSlackMsgWithDebounce(`${data.firstname} ${data.lastname} has saved their RSVP:${attending}${starter}${main}${dietary}`, '#wedding-rsvps', req.body.key);
|
sendSlackMsgWithDebounce(`${data.firstname} ${data.lastname} has saved their RSVP:${attending}${starter}${main}${dietary}`, '#wedding-rsvps', req.body.key);
|
||||||
|
sendRSVPEmailWithDebounce(data);
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -4423,6 +4423,10 @@ node-uuid@1.4.x, node-uuid@~1.4.4, node-uuid@~1.4.7:
|
|||||||
version "1.4.7"
|
version "1.4.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f"
|
resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.7.tgz#6da5a17668c4b3dd59623bda11cf7fa4c1f60a6f"
|
||||||
|
|
||||||
|
nodemailer@^3.1.3:
|
||||||
|
version "3.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-3.1.3.tgz#331b3265522f52b128bfc2dcaba9fe317b82f018"
|
||||||
|
|
||||||
"nopt@2 || 3", nopt@3.0.x, nopt@~3.0.6:
|
"nopt@2 || 3", nopt@3.0.x, nopt@~3.0.6:
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||||
|
|||||||
Reference in New Issue
Block a user