512 lines
12 KiB
Svelte
512 lines
12 KiB
Svelte
<script>
|
|
import Importabular from "importabular"
|
|
import showdown from "showdown"
|
|
|
|
let converter = new showdown.Converter({
|
|
simplifiedAutoLink: true, // automatically convert links...
|
|
excludeTrailingPunctuationFromURLs: true, // ...but not the trailing punctuation
|
|
openLinksInNewWindow: true, // don't forget to open in new window!
|
|
simpleLineBreaks: true, // use two line breaks to start a new paragraph
|
|
emoji: true, // because why not 😉
|
|
strikethrough: true, // ~~that also~~
|
|
})
|
|
|
|
let editor
|
|
|
|
let emailContent = 'Hi [TK name],\n\nIt\'s been a while since we heard from you. How are you doing? Your last article [TK article] did quite well, and we were wondering if you\'d be interested in writing a sequel?\n\nCheers, \nThe Snipette Team[TK PS]'
|
|
|
|
let sheetOne
|
|
let step = 0
|
|
let tkList = []
|
|
|
|
let fromEmail = ''
|
|
let subject = ''
|
|
|
|
let cookiePreviewData = { text: '', email: ''}
|
|
let cookiePreviewRow = 0
|
|
|
|
function showEmail() {
|
|
step = 1
|
|
}
|
|
|
|
function detectFields(value) {
|
|
if (!value) return []
|
|
|
|
let matches = value.match(/\[TK (\w+?)\]/gm)
|
|
let tkList = []
|
|
|
|
if (!!matches) {
|
|
let propertyRegex = new RegExp(/\[TK (\w+)\]/)
|
|
for (let m of matches) {
|
|
tkList.push(propertyRegex.exec(m)[1])
|
|
}
|
|
}
|
|
|
|
// remove duplicates
|
|
tkList = [...new Set(tkList)]
|
|
|
|
return tkList
|
|
}
|
|
|
|
$: tkList = detectFields(emailContent)
|
|
|
|
function showSheet() {
|
|
// Figure out columns
|
|
let columns = []
|
|
|
|
if (!tkList.includes('email')) {
|
|
columns.push({
|
|
label: 'email',
|
|
description: 'Email address of the recipient',
|
|
placeholder: 'someone@members.snipettemag.com',
|
|
})
|
|
}
|
|
|
|
for (let tk of tkList) {
|
|
columns.push({
|
|
label: tk,
|
|
})
|
|
}
|
|
|
|
// First sheet
|
|
sheetOne = new Importabular({
|
|
node: editor,
|
|
columns: columns,
|
|
onChange(data) {
|
|
cookiePreviewData = getPreview()
|
|
}
|
|
})
|
|
|
|
step = 2
|
|
|
|
// for debugging only
|
|
window.sheetOne = sheetOne
|
|
}
|
|
|
|
/**
|
|
* Refresh sheet
|
|
*
|
|
* Destroys the current sheet, but saves the data first so if
|
|
* there are any columns in the new sheet that matches the old
|
|
* ones, it'll add them back. (Columns meaning TKs, of course)
|
|
*/
|
|
|
|
function refreshSheet() {
|
|
let oldData = sheetOne.getData()
|
|
let oldColumns = sheetOne.columns.map(c => c.label)
|
|
|
|
sheetOne.destroy()
|
|
|
|
// now, make it up again with the new TKs
|
|
showSheet()
|
|
|
|
// figure out the new data
|
|
let newData = []
|
|
let tkOrder = []
|
|
|
|
// first, we figure out how the old column order corresponds to
|
|
// the new one
|
|
|
|
for (let col of sheetOne.columns.map(c => c.label)) {
|
|
tkOrder.push(oldColumns.indexOf(col))
|
|
}
|
|
|
|
// now we fill this, row by row
|
|
for (let oldRow of oldData) {
|
|
|
|
// skip empty rows
|
|
if (oldRow.every(v => !v)) continue
|
|
|
|
// now, we figure out what to put for the new row
|
|
let newRow = []
|
|
for (let tkIndex of tkOrder) {
|
|
if (tkIndex <= -1) {
|
|
// new TK; leave it blank
|
|
newRow.push('')
|
|
} else {
|
|
// old TK; keep it filled!
|
|
newRow.push(oldRow[tkIndex])
|
|
}
|
|
}
|
|
|
|
// and finally, when all is done, we save it.
|
|
newData.push(newRow)
|
|
}
|
|
|
|
// fill it with the new data
|
|
sheetOne.setData(newData)
|
|
|
|
// don't forget to refresh the preview!
|
|
cookiePreviewData = getPreview()
|
|
}
|
|
|
|
function incrementPreviewRow() {
|
|
let sheetLength = sheetOne.getData().length
|
|
|
|
// make sure it's not too big...
|
|
if (cookiePreviewRow >= sheetLength-1) {
|
|
cookiePreviewRow = sheetLength - 1
|
|
} else if (cookiePreviewRow < 0) {
|
|
// ...and not too small
|
|
cookiePreviewRow = 0
|
|
} else {
|
|
// if not, then make it "just right"! :)
|
|
cookiePreviewRow++
|
|
}
|
|
}
|
|
|
|
function decrementPreviewRow() {
|
|
let sheetLength = sheetOne.getData().length
|
|
// make sure it's not too small...
|
|
if (cookiePreviewRow >= sheetLength) {
|
|
// ...and not too big
|
|
cookiePreviewRow = sheetLength - 1
|
|
} else if (cookiePreviewRow <= 0) {
|
|
// if not, then make it "just right"! :)
|
|
cookiePreviewRow = 0
|
|
} else {
|
|
cookiePreviewRow--
|
|
}
|
|
}
|
|
|
|
function getPreview(row) {
|
|
if (!row) row = cookiePreviewRow
|
|
let previewText = emailContent
|
|
|
|
// get the row we're working on
|
|
if (!sheetOne) return '' // no sheet to get data from :(
|
|
let data = sheetOne.getData()
|
|
if (row < 0 || data.length < row) return previewText // no row to apply :(
|
|
let r = data[row]
|
|
|
|
for (let tk of tkList) {
|
|
// figure out which column holds values for this TK
|
|
let tkIndex = sheetOne.columns.findIndex(t => t.label == tk)
|
|
|
|
// replace it!
|
|
previewText = previewText.replace(new RegExp(`\\[TK ${tk}\\]`, 'g'), r[tkIndex])
|
|
}
|
|
|
|
// save the email
|
|
let previewEmail = r[sheetOne.columns.findIndex(t => t.label == 'email')]
|
|
|
|
return { text: previewText, email: previewEmail }
|
|
}
|
|
|
|
// automatically update
|
|
$: cookiePreviewData = getPreview(cookiePreviewRow)
|
|
|
|
function next() {
|
|
step += 1
|
|
}
|
|
|
|
function prev() {
|
|
step -= 1
|
|
}
|
|
|
|
function stepOne() {
|
|
step = 1
|
|
}
|
|
|
|
function stepTwo() {
|
|
step = 2
|
|
}
|
|
|
|
// This is to display which email is sending next
|
|
let nowSending
|
|
|
|
// And this is for the progress bar, mainly
|
|
let emailsSoFar = 0
|
|
let emailsToSend = 0
|
|
|
|
/**
|
|
* 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,
|
|
subject: subject,
|
|
text: previewData.text,
|
|
html: converter.makeHtml(previewData.text),
|
|
})
|
|
}
|
|
|
|
// keep track of how many we had to start witth
|
|
emailsToSend = emails.length
|
|
emailsSoFar = 0
|
|
|
|
// 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.shift()
|
|
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.shift()
|
|
if (!!nowSending) {
|
|
socket.send(JSON.stringify(nowSending))
|
|
console.log(`Sending: ${nowSending.to}`)
|
|
|
|
// update the counter for the progress bar
|
|
emailsSoFar += 1
|
|
} else {
|
|
socket.close()
|
|
step = 5
|
|
}
|
|
} else {
|
|
console.error('Something went wrong :(')
|
|
step = 3
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<main>
|
|
{#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">
|
|
<div class="header">
|
|
<div class="form-group">
|
|
From
|
|
<input type="text" placeholder="chipchoc@example.com" bind:value={fromEmail}/>
|
|
</div>
|
|
<div class="form-group">
|
|
Subject
|
|
<input type="text" placeholder="Hi there" bind:value={subject}/>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
<p>Subject: {subject || '[NO SUBJECT #notgood]'}</p>
|
|
<hr />
|
|
</div>
|
|
{@html converter.makeHtml(cookiePreviewData.text)}
|
|
</div>
|
|
<button on:click={decrementPreviewRow}>< Prev</button>
|
|
<button on:click={incrementPreviewRow}>Next ></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}
|
|
<img src="assets/email-baking.png" class="main-pic" alt="Email Oven with cookies inside"/>
|
|
<h1><span class={emailsSoFar % 2 == 0 ? 'highlight' : ''}>Chip</span> <span class={emailsSoFar % 2 == 0 ? '' : 'highlight'}>Choc</span></h1>
|
|
<h2>Your cookies are being cut...</h2>
|
|
<progress value={emailsSoFar} max={emailsToSend}>{emailsToSend ? emailsSoFar/emailsToSend*100 : 0} %</progress>
|
|
|
|
{#if !!nowSending }
|
|
<div class="cookie-preview">
|
|
<div class="preview-header">
|
|
<p>From: {nowSending.from || '[NOBODY!!!]'}</p>
|
|
<p>To: {nowSending.to || '[NOBODY!!!]'}</p>
|
|
<p>Subject: {subject || '[NO SUBJECT #notgood]'}</p>
|
|
<hr />
|
|
{@html nowSending.html}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if step == 5}
|
|
<img src="assets/cookies.png" class="main-pic" alt="A Pile of Cookies"/>
|
|
<h1>Wheee<span class="highlight">!</span></h1>
|
|
<h2>All emails sent successfully.</h2>
|
|
<button on:click={stepTwo}>Back to Draft</button>
|
|
<button on:click={stepOne}>Start Over</button>
|
|
{/if}
|
|
|
|
</main>
|
|
|
|
<style>
|
|
main {
|
|
text-align: center;
|
|
padding: 1em;
|
|
max-width: 240px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
h1 + h2 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.main-pic {
|
|
max-width: 100%;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
main {
|
|
max-width: none;
|
|
}
|
|
|
|
.main-pic {
|
|
max-width: 240px;
|
|
}
|
|
}
|
|
|
|
.instructions {
|
|
max-width: 640px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.email-content {
|
|
margin: 4em auto;
|
|
}
|
|
|
|
.email-content .header {
|
|
max-width: 640px;
|
|
text-align: left;
|
|
margin: auto;
|
|
}
|
|
|
|
.email-content .form-group {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.email-content .form-group input {
|
|
margin-left: 1em;
|
|
width: 100%;
|
|
border: 0
|
|
}
|
|
|
|
.email-content textarea {
|
|
width: 100%;
|
|
max-width: 640px;
|
|
font-family: inherit;
|
|
min-height: 15em;
|
|
}
|
|
|
|
.tag {
|
|
margin: 0.2em;
|
|
padding: 0.5em 1em 0.5em 1em;
|
|
background: #ffe3ff;
|
|
border-radius: 1em;
|
|
}
|
|
|
|
.cookie-preview {
|
|
text-align: left;
|
|
max-width: 640px;
|
|
margin: auto;
|
|
padding: 1em;
|
|
border: 1px solid black;
|
|
}
|
|
|
|
progress {
|
|
width: 100%;
|
|
max-width: 640px;
|
|
margin: 1em;
|
|
color: #800020;
|
|
}
|
|
|
|
progress::-moz-progress-bar,
|
|
progress::-webkit-progress-value {
|
|
background-color: #800020;
|
|
}
|
|
</style>
|