Set up WebSocket for email sending

The actual sending doesn't happen yet, but all the frontend
infrastructure is set up all right!
This commit is contained in:
Badri Sunderarajan 2021-05-29 00:10:16 +05:30
parent 66056cbf0b
commit 9b630ae021
4 changed files with 220 additions and 75 deletions

View file

@ -21,6 +21,7 @@
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"importabular": "^0.2.9",
"showdown": "^1.9.1",
"sirv-cli": "^1.0.0"

View file

@ -3,6 +3,8 @@ const express = require('express')
require('dotenv').config()
const app = express()
var expressWs = require('express-ws')(app)
const port = process.env.port || 5000
const cors = require('cors')
@ -18,10 +20,43 @@ app.get('/air', (req, res) => {
})
})
app.ws('/hit-send', (ws, request) => {
console.log('Hitting send on some emails!')
ws.on('message', (message) => {
try {
message = JSON.parse(message)
} catch(err) {
ws.send(JSON.stringify({
success: false,
error: "We can't understand what you're saying! Me speak JSON only.",
}))
return
}
console.log(`\n\n-------------------------- BEGIN EMAIL --------------------------\n`)
console.log(`From: ${message.from}\nTo:${message.to}\n\n${message.text}`)
console.log(`\n--------------------------- END EMAIL ---------------------------`)
setTimeout(() => {
ws.send(JSON.stringify({
success: true,
from: message.from,
to: message.to,
}))
}, 2000)
})
ws.on('close', () => {
console.log('socket closed')
})
})
app.use(express.static('public'))
app.get('*', (req, res) => {
app.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})
app.listen(port, () => {
console.log(`Server is up at port ${port}`)
})

View file

@ -195,88 +195,178 @@
function prev() {
step -= 1
}
let nowSending
/**
* Hit Send
*
* collates all the data for sending, and passes it
* on to the backend for actual processing. Note that here we
* cookie-cut the emails on the frontend itself; the backend
* just blindly takes what it gets. We're doing this so that we
* can use the same getPreview() function and be consistent: we
* don't want the final to suddenly end up looking very different
* from the preview!
*/
function hitSend() {
step = 4
let emails = []
// process the 'i'th row of the sheet, one by one
for (let i=0; i<sheetOne.getData().length; i++) {
let previewData = getPreview(i)
// skip if email is blank
if (!previewData.email) continue
// add it to the list of emails to be sent
emails.push({
from: fromEmail,
to: previewData.email,
text: previewData.text,
html: converter.makeHtml(previewData.text),
})
}
// okay, now let's open a socket to the server!
let socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
let socketUrl = socketProtocol + '//' + window.location.host + window.location.pathname + 'hit-send/'
let socket = new WebSocket(socketUrl)
socket.onopen = function() {
nowSending = emails.pop()
socket.send(JSON.stringify(nowSending))
}
socket.onmessage = (message) => {
try {
message = JSON.parse(message.data)
} catch (err) {
console.error(`Received malformed response from server: ${message.data}`)
return
}
if (message.success) {
console.log(`${message.to}'s email sent successfully!`)
// send the next one
nowSending = emails.pop()
if (!!nowSending) {
socket.send(JSON.stringify(nowSending))
console.log(`Sending: ${nowSending.to}`)
} else {
socket.close()
step = 2
}
} else {
console.error('Something went wrong :(')
step = 3
}
}
}
</script>
<main>
<img src="/assets/email-baker.png" class="main-pic" alt="Email Oven"/>
<h1>Chip <span class="highlight">Choc</span></h1>
<h2>Cookie-cutter emails made easy.</h2>
{#if step < 4}
<img src="/assets/email-baker.png" class="main-pic" alt="Email Oven"/>
<h1>Chip <span class="highlight">Choc</span></h1>
<h2>Cookie-cutter emails made easy.</h2>
{#if step >= 1}
<h3 class="mt-3">Step 1</h3>
{/if}
{#if step == 0}
<button on:click={showEmail}>Start Drafting</button>
{/if}
{#if step >= 1}
<div class="instructions">
<p>Compose your email below, leaving [TK stuff] to be replaced in the table. Make sure each TK is a single word: no spaces allowed!</p>
</div>
<div class="email-content">
<p>From: <input type="text" bind:value={fromEmail}/></p>
<textarea bind:value={emailContent}/>
<p>
<strong>Detected fields:</strong>
{#each tkList as tk (tk)}
<span class="tag">{tk}</span>
{/each}
</p>
</div>
{/if}
{#if step >= 2}
<h3 class="mt-3">Step 1</h3>
<div class="instructions">
<p>Now, fill in the fields for each user. (You can also copy-paste rows and columns directly from Air, if the ordering is right!)</p>
</div>
{/if}
{#if step == 1}
<button on:click={showSheet}>Next</button>
{/if}
{#if step > 1}
<button on:click={refreshSheet}>Refresh</button>
{/if}
<div id="editor" bind:this={editor}></div>
{#if step >= 2 && !!cookiePreviewData}
<h4 class="mt-3">Preview</h4>
<div class="cookie-preview">
<div class="preview-header">
<p>From: {fromEmail || '[NOBODY!!!]'}</p>
<p>To: {cookiePreviewData.email || '[NOBODY!!!]'}</p>
<hr />
</div>
{@html converter.makeHtml(cookiePreviewData.text)}
</div>
<button on:click={decrementPreviewRow}>&lt; Prev</button>
<button on:click={incrementPreviewRow}>Next &gt;</button>
<h3>Step 3</h3>
{#if step == 2}
<div class="instructions">
<p>If everything looks okay, then we're done! Click on the button below to put the cutter in action.</p>
</div>
<button on:click={next}>Yup, send 'em out!</button>
{#if step >= 1}
<h3 class="mt-3">Step 1</h3>
{/if}
{#if step == 3}
<div class="instructions">
<p>Are you <strong>sure</strong> you want to send them out?</p>
<p>Are you ready?</p>
<p>Did you look through all the previews?</p>
<p>Have you double-checked the names and email IDs to make sure they match?</p>
</div>
<button>Hit send!</button> <button on:click={prev}>Ummmm..</button>
{#if step == 0}
<button on:click={showEmail}>Start Drafting</button>
{/if}
{#if step >= 1}
<div class="instructions">
<p>Compose your email below, leaving [TK stuff] to be replaced in the table. Make sure each TK is a single word: no spaces allowed!</p>
</div>
<div class="email-content">
<p>From: <input type="text" bind:value={fromEmail}/></p>
<textarea bind:value={emailContent}/>
<p>
<strong>Detected fields:</strong>
{#each tkList as tk (tk)}
<span class="tag">{tk}</span>
{/each}
</p>
</div>
{/if}
{#if step >= 2}
<h3 class="mt-3">Step 2</h3>
<div class="instructions">
<p>Now, fill in the fields for each user. (You can also copy-paste rows and columns directly from Air, if the ordering is right!)</p>
</div>
{/if}
{#if step == 1}
<button on:click={showSheet}>Next</button>
{/if}
{#if step > 1}
<button on:click={refreshSheet}>Refresh</button>
{/if}
<div id="editor" bind:this={editor}></div>
{#if step >= 2 && !!cookiePreviewData}
<h4 class="mt-3">Preview</h4>
<div class="cookie-preview">
<div class="preview-header">
<p>From: {fromEmail || '[NOBODY!!!]'}</p>
<p>To: {cookiePreviewData.email || '[NOBODY!!!]'}</p>
<hr />
</div>
{@html converter.makeHtml(cookiePreviewData.text)}
</div>
<button on:click={decrementPreviewRow}>&lt; Prev</button>
<button on:click={incrementPreviewRow}>Next &gt;</button>
<h3>Step 3</h3>
{#if step == 2}
<div class="instructions">
<p>If everything looks okay, then we're done! Click on the button below to put the cutter in action.</p>
</div>
<button on:click={next}>Yup, send 'em out!</button>
{/if}
{#if step == 3}
<div class="instructions">
<p>Are you <strong>sure</strong> you want to send them out?</p>
<p>Are you ready?</p>
<p>Did you look through all the previews?</p>
<p>Have you double-checked the names and email IDs to make sure they match?</p>
</div>
<button on:click={hitSend}>Hit send!</button> <button on:click={prev}>Ummmm..</button>
{/if}
{/if}
{/if}
{#if step == 4}
<div class="instructions">
{#if !!nowSending }
<div class="cookie-preview">
<div class="preview-header">
<p>From: {nowSending.from || '[NOBODY!!!]'}</p>
<p>To: {nowSending.to || '[NOBODY!!!]'}</p>
<hr />
{@html nowSending.html}
</div>
</div>
{/if}
<h1>Chip <span class="highlight">Choc</span></h1>
<h2>Your cookies are being cut...</h2>
</div>
{/if}
</main>

View file

@ -125,6 +125,11 @@ array-flatten@1.1.1:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -358,6 +363,13 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express-ws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/express-ws/-/express-ws-4.0.0.tgz#dabd8dc974516418902a41fe6e30ed949b4d36c4"
integrity sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==
dependencies:
ws "^5.2.0"
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@ -1148,6 +1160,13 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
dependencies:
async-limiter "~1.0.0"
ws@^7.4.3:
version "7.4.5"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1"