🧑💻 (emails) Add decent emails management
Use mjml-react to generate emails. Put all emails in a independent package.
This commit is contained in:
@ -1,37 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/invitation-banner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>You have been invited by ${hostEmail} to collaborate on his typebot <strong>${typebotName}</strong></mj-text>
|
||||
<mj-text>From now on you will see this typebot in your dashboard under the his workspace "${workspaceName}" 👍</mj-text>
|
||||
<mj-text>Make sure to log in as ${guestEmail}</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Go to typebot</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
File diff suppressed because one or more lines are too long
@ -1,97 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/typebot2-banner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Hey 👋</mj-text>
|
||||
<mj-text>I’m super excited to announce that Typebot 2.0 is finally available after 3 months of deep work. </mj-text>
|
||||
<mj-text>It comes with a brand new building experience and it has been implemented from the ground up with “Freedom” in mind, allowing you to create truly customized conversational applications.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/newsletter-builder.gif"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text>Typebot 2.0 also comes with new advanced features, including:</mj-text>
|
||||
<mj-text>- Data fetching from a Google Sheet</mj-text>
|
||||
<mj-text>- Custom emails</mj-text>
|
||||
<mj-text>- Custom CSS</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text>Typebot still allows you to create unique conversational experiences for your user. This leads to:</mj-text>
|
||||
<mj-text>😍 An increase in customer satisfaction and retention.</mj-text>
|
||||
<mj-text>🚀 A 3x conversion increase on average on your lead qualification forms.</mj-text>
|
||||
<mj-text font-weight="bold">And it still comes with a generous unlimited free plan 💙</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="https://app.typebot.io/register" font-weight="500" border-radius="5px">Try it out</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>I’m inviting you to join our Facebook community where you’ll get updates on the product.</mj-text>
|
||||
<mj-text>It’s also a place where we share our metrics and forms templates 💪</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#ffffff" color="#0042da" border="1px solid #0042da" href="https://app.typebot.io/register" font-weight="500" border-radius="5px">Join the community</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image width="500px" src="https://typebot.s3.eu-west-3.amazonaws.com/assets/facebook-post2.jpeg" border-radius="5px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>If you have any question, just hit "reply"! I'm super approachable 😁. Look, here is a selfie 👇</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/selfie-newsletter.jpeg" width="300px" border-radius="5px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text padding-bottom="0px">Baptiste,</mj-text>
|
||||
<mj-text padding-top="0px">Founder of Typebot.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="13px" text-decoration="underline">
|
||||
<a class="footer-link" href="%unsubscribe_url%" color="#fffff">Unsubscribe from all future emails</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
@ -1,50 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/typebot2-banner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>You receive this email because you are the owner of a {{PLAN}} plan on Typebot. Which means you can keep all your perks on Typebot 2.0 as well 😍</mj-text>
|
||||
<mj-text>All you have to do is to Create your new account by clicking on the button below.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="https://app.typebot.io/register" font-weight="500" border-radius="5px">Create my account</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Your plan should automatically be applied. If you have any issue, hit reply 😃</mj-text>
|
||||
<mj-text>I'm eagerly waiting for your feedback! Let's make this tool amazing.</mj-text>
|
||||
<mj-text padding-bottom="0px">Baptiste,</mj-text>
|
||||
<mj-text padding-top="0px">Founder of Typebot.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="13px" text-decoration="underline">
|
||||
<a class="footer-link" href="%unsubscribe_url%" color="#fffff">Unsubscribe from all future emails</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
@ -1,37 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.eu-west-3.amazonaws.com/assets/invitation-banner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>You have been invited by ${hostEmail} to collaborate on his workspace "${workspaceName}" as a team member.</mj-text>
|
||||
<mj-text>From now on you will have access to this workspace in your dashboard 👍</mj-text>
|
||||
<mj-text>Make sure to log in as ${guestEmail}</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Go the workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
@ -1,536 +0,0 @@
|
||||
type Props = {
|
||||
workspaceName: string
|
||||
url: string,
|
||||
hostEmail: string,
|
||||
guestEmail: string
|
||||
}
|
||||
|
||||
export const workspaceMemberInvitationEmail = ({workspaceName, url, hostEmail, guestEmail}: Props) => `<!DOCTYPE html>
|
||||
<html
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
>
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="word-spacing: normal; background-color: #ffffff">
|
||||
<div style="background-color: #ffffff">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 600px">
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
border: 1px solid #e2e8f0;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 598px;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0;
|
||||
padding-bottom: 0px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div
|
||||
class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 598px">
|
||||
<img
|
||||
alt="header image"
|
||||
height="auto"
|
||||
src="https://typebot.s3.eu-west-3.amazonaws.com/assets/invitation-banner.png"
|
||||
style="
|
||||
border: 0;
|
||||
display: block;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
"
|
||||
width="598"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 598px;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0;
|
||||
padding-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div
|
||||
class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 40px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica Neue,
|
||||
Helvetica, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
You have been invited by
|
||||
${hostEmail} to collaborate on his
|
||||
workspace "${workspaceName}" as a
|
||||
team member.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 40px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica Neue,
|
||||
Helvetica, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
From now on you will have access
|
||||
to this workspace in your
|
||||
dashboard üëç
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 10px 40px;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
font-family: Helvetica Neue,
|
||||
Helvetica, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
text-align: left;
|
||||
color: #000000;
|
||||
"
|
||||
>
|
||||
Make sure to log in as
|
||||
${guestEmail}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 598px;
|
||||
"
|
||||
>
|
||||
<table
|
||||
align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0;
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div
|
||||
class="mj-column-per-100 mj-outlook-group-fix"
|
||||
style="
|
||||
font-size: 0px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0">
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
width="100%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
vertical-align="middle"
|
||||
style="
|
||||
font-size: 0px;
|
||||
padding: 0;
|
||||
word-break: break-word;
|
||||
"
|
||||
>
|
||||
<table
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="
|
||||
border-collapse: separate;
|
||||
line-height: 100%;
|
||||
"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
bgcolor="#0042da"
|
||||
role="presentation"
|
||||
style="
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: auto;
|
||||
mso-padding-alt: 10px 25px;
|
||||
background: #0042da;
|
||||
"
|
||||
valign="middle"
|
||||
>
|
||||
<a
|
||||
href="${url}"
|
||||
style="
|
||||
display: inline-block;
|
||||
background: #0042da;
|
||||
color: white;
|
||||
font-family: Helvetica
|
||||
Neue,
|
||||
Helvetica, Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 23px;
|
||||
margin: 0;
|
||||
text-decoration: none;
|
||||
text-transform: none;
|
||||
padding: 10px 25px;
|
||||
mso-padding-alt: 0px;
|
||||
border-radius: 5px;
|
||||
"
|
||||
target="_blank"
|
||||
>Go the workspace</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
const path = require('path')
|
||||
const withTM = require('next-transpile-modules')(['utils', 'models'])
|
||||
const withTM = require('next-transpile-modules')(['utils', 'models', 'emails'])
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
|
@ -62,7 +62,7 @@
|
||||
"minio": "7.0.32",
|
||||
"next": "12.3.0",
|
||||
"next-auth": "4.10.3",
|
||||
"nodemailer": "^6.7.8",
|
||||
"nodemailer": "^6.8.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"papaparse": "^5.3.2",
|
||||
"prettier": "2.7.1",
|
||||
@ -80,7 +80,9 @@
|
||||
"swr": "^1.3.0",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"typebot-js": "workspace:*",
|
||||
"use-debounce": "8.0.4"
|
||||
"use-debounce": "8.0.4",
|
||||
"emails": "workspace:*",
|
||||
"@faire/mjml-react": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.19.0",
|
||||
|
@ -1,37 +1,22 @@
|
||||
/* eslint-disable @next/next/no-sync-scripts */
|
||||
import Document, {
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
} from 'next/document'
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
return { ...initialProps }
|
||||
}
|
||||
const Document = () => (
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="google" content="notranslate" />
|
||||
<script src="/__env.js" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Open+Sans:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="google" content="notranslate" />
|
||||
<script src="/__env.js" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
export default Document
|
||||
|
@ -87,8 +87,8 @@ export function CustomAdapter(p: PrismaClient): Adapter {
|
||||
},
|
||||
updateUser: (data) => p.user.update({ where: { id: data.id }, data }),
|
||||
deleteUser: (id) => p.user.delete({ where: { id } }),
|
||||
linkAccount: (data) => {
|
||||
return p.account.create({
|
||||
linkAccount: async (data) => {
|
||||
await p.account.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
type: data.type,
|
||||
@ -105,10 +105,11 @@ export function CustomAdapter(p: PrismaClient): Adapter {
|
||||
oauth_token: data.oauth_token as string,
|
||||
refresh_token_expires_in: data.refresh_token_expires_in as number,
|
||||
},
|
||||
}) as any
|
||||
})
|
||||
},
|
||||
unlinkAccount: async (provider_providerAccountId) => {
|
||||
await p.account.delete({ where: { provider_providerAccountId } })
|
||||
},
|
||||
unlinkAccount: (provider_providerAccountId) =>
|
||||
p.account.delete({ where: { provider_providerAccountId } }) as any,
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const userAndSession = await p.session.findUnique({
|
||||
where: { sessionToken },
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { invitationToCollaborate } from 'assets/emails/invitationToCollaborate'
|
||||
import { CollaborationType, WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules'
|
||||
import {
|
||||
sendEmailNotification,
|
||||
badRequest,
|
||||
forbidden,
|
||||
methodNotAllowed,
|
||||
@ -13,6 +11,7 @@ import {
|
||||
} from 'utils/api'
|
||||
import { getAuthenticatedUser } from 'services/api/utils'
|
||||
import { env } from 'utils'
|
||||
import { sendGuestInvitationEmail } from 'emails'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req)
|
||||
@ -68,16 +67,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
data: { email: email.toLowerCase().trim(), type, typebotId },
|
||||
})
|
||||
if (env('E2E_TEST') !== 'true')
|
||||
await sendEmailNotification({
|
||||
await sendGuestInvitationEmail({
|
||||
to: email,
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: invitationToCollaborate({
|
||||
hostEmail: user.email ?? '',
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
|
||||
guestEmail: email.toLowerCase(),
|
||||
typebotName: typebot.name,
|
||||
workspaceName: typebot.workspace?.name ?? '',
|
||||
}),
|
||||
hostEmail: user.email ?? '',
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
|
||||
guestEmail: email.toLowerCase(),
|
||||
typebotName: typebot.name,
|
||||
workspaceName: typebot.workspace?.name ?? '',
|
||||
})
|
||||
return res.send({
|
||||
message: 'success',
|
||||
|
@ -1,16 +1,11 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { workspaceMemberInvitationEmail } from 'assets/emails/workspaceMemberInvitation'
|
||||
import { Workspace, WorkspaceInvitation, WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import {
|
||||
sendEmailNotification,
|
||||
forbidden,
|
||||
methodNotAllowed,
|
||||
notAuthenticated,
|
||||
} from 'utils/api'
|
||||
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||
import { getAuthenticatedUser } from 'services/api/utils'
|
||||
import { env, seatsLimit } from 'utils'
|
||||
import { sendWorkspaceMemberInvitationEmail } from 'emails'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req)
|
||||
@ -39,15 +34,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
},
|
||||
})
|
||||
if (env('E2E_TEST') !== 'true')
|
||||
await sendEmailNotification({
|
||||
await sendWorkspaceMemberInvitationEmail({
|
||||
to: data.email,
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: workspaceMemberInvitationEmail({
|
||||
workspaceName: workspace.name,
|
||||
guestEmail: data.email,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||
hostEmail: user.email ?? '',
|
||||
}),
|
||||
workspaceName: workspace.name,
|
||||
guestEmail: data.email,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||
hostEmail: user.email ?? '',
|
||||
})
|
||||
return res.send({
|
||||
member: {
|
||||
@ -61,15 +53,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
} else {
|
||||
const invitation = await prisma.workspaceInvitation.create({ data })
|
||||
if (env('E2E_TEST') !== 'true')
|
||||
await sendEmailNotification({
|
||||
await sendWorkspaceMemberInvitationEmail({
|
||||
to: data.email,
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: workspaceMemberInvitationEmail({
|
||||
workspaceName: workspace.name,
|
||||
guestEmail: data.email,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||
hostEmail: user.email ?? '',
|
||||
}),
|
||||
workspaceName: workspace.name,
|
||||
guestEmail: data.email,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||
hostEmail: user.email ?? '',
|
||||
})
|
||||
return res.send({ invitation })
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Your bots are chatting a lot. That's amazing. ❤️</mj-text>
|
||||
<mj-text>This means you've almost reached your monthly chats limit. You currently reached 80% of ${readableChatsLimit}.</mj-text>
|
||||
<mj-text>This limit will be reset on ${readableResetDate}.</mj-text>
|
||||
<mj-text>Your bots won't start the chat if you reach the limit before this date. ⚠️</mj-text>
|
||||
<mj-text>If you need more monthly responses, you will need to upgrade your plan.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
File diff suppressed because one or more lines are too long
@ -1,38 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>Your bots are working a lot. That's amazing. 🤖</mj-text>
|
||||
<mj-text>This means you've almost reached your storage limit. You currently reached 80% of your ${readableStorageLimit} limit.</mj-text>
|
||||
<mj-text>Your bots won't collect new files once you reach the limit. ⚠️</mj-text>
|
||||
<mj-text>To make sure it won't happen, you need to upgrade your plan or delete existing results to free up space.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
File diff suppressed because one or more lines are too long
@ -1,188 +0,0 @@
|
||||
const emailRegex =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
const parseResult = (val?: string) =>
|
||||
val && emailRegex.test(val) ? `<a href="mailto:${val}">${val}</a>` : val ?? ''
|
||||
|
||||
export const newLeadEmailContent = (
|
||||
resultUrl: string,
|
||||
answers: { [key: string]: string }
|
||||
) => `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;background-color:#ffffff;">
|
||||
<div style="background-color:#ffffff;">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border:1px solid #E2E8F0;direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;padding-top:20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">Your typebot has collected a <strong>new lead!</strong> 🥳</div>
|
||||
</td>
|
||||
</tr>
|
||||
${Object.keys(answers)
|
||||
.map(
|
||||
(key) => `<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">${key}: ${parseResult(
|
||||
answers[key]
|
||||
)}</div>
|
||||
</td>
|
||||
</tr>`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="vertical-align:top;padding:0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" vertical-align="middle" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#0042da" role="presentation" style="border:none;border-radius:5px;cursor:auto;mso-padding-alt:10px 25px;background:#0042da;" valign="middle">
|
||||
<a href="${resultUrl}" style="display:inline-block;background:#0042da;color:white;font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:23px;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:5px;" target="_blank"> Check results </a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
`
|
@ -1,37 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>It just happened, you've reached your monthly ${readableChatsLimit} chats limit 😮</mj-text>
|
||||
<mj-text>It means your bots are closed until ${readableResetDate}.</mj-text>
|
||||
<mj-text>If you'd like to continue chatting with your users this month, then you need to upgrade your plan. 🚀</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
File diff suppressed because one or more lines are too long
@ -1,37 +0,0 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif" font-size="16px" padding="0" line-height="23px"></mj-all>
|
||||
<mj-section background-color="#ffffff" padding-bottom="20px"></mj-section>
|
||||
<mj-text padding="10px 40px"></mj-text>
|
||||
</mj-attributes>
|
||||
<mj-style inline="inline">
|
||||
.footer-link {
|
||||
color: #A0AEC0
|
||||
}
|
||||
</mj-style>
|
||||
</mj-head>
|
||||
<mj-body background-color="#ffffff">
|
||||
<mj-wrapper border="1px solid #E2E8F0">
|
||||
<mj-section padding-bottom="0px">
|
||||
<mj-column width="100%">
|
||||
<mj-image src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" alt="header image" padding="0px"></mj-image>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="20px">
|
||||
<mj-column>
|
||||
<mj-text>It just happened, you've reached your ${readableStorageLimit} storage limit 😮</mj-text>
|
||||
<mj-text>It means your bots won't collect new files from your users.</mj-text>
|
||||
<mj-text>If you'd like to continue collecting files, then you need to upgrade your plan or remove existing results to free up space. 🚀</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-button background-color="#0042da" color="white" href="${url}" font-weight="500" border-radius="5px">Upgrade workspace</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
</mj-wrapper>
|
||||
|
||||
</mj-body>
|
||||
</mjml>
|
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
const path = require('path')
|
||||
const withTM = require('next-transpile-modules')(['utils', 'models'])
|
||||
const withTM = require('next-transpile-modules')(['utils', 'models', 'emails'])
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
|
@ -41,6 +41,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "5.36.2",
|
||||
"@typescript-eslint/parser": "5.36.2",
|
||||
"dotenv": "^16.0.1",
|
||||
"emails": "workspace:*",
|
||||
"eslint": "8.23.0",
|
||||
"eslint-config-next": "12.3.0",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
@ -52,6 +53,7 @@
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"papaparse": "^5.3.2",
|
||||
"typescript": "^4.8.3",
|
||||
"uglify-js": "^3.17.2",
|
||||
"utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,16 @@
|
||||
import Document, {
|
||||
Html,
|
||||
Head,
|
||||
Main,
|
||||
NextScript,
|
||||
DocumentContext,
|
||||
} from 'next/document'
|
||||
/* eslint-disable @next/next/no-sync-scripts */
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
return { ...initialProps }
|
||||
}
|
||||
const Document = () => (
|
||||
<Html>
|
||||
<Head>
|
||||
<script src="/__env.js" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
||||
<script src="/__env.js" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument
|
||||
export default Document
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import { almostReachedStorageLimitEmail } from 'assets/emails/almostReachedStorageLimitEmail'
|
||||
import { reachedStorageLimitEmail } from 'assets/emails/reachedStorageLimitEmail'
|
||||
import { WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { InputBlockType, PublicTypebot } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import {
|
||||
badRequest,
|
||||
generatePresignedUrl,
|
||||
methodNotAllowed,
|
||||
sendEmailNotification,
|
||||
} from 'utils/api'
|
||||
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
|
||||
import { byId, getStorageLimit, isDefined, env } from 'utils'
|
||||
import {
|
||||
sendAlmostReachedStorageLimitEmail,
|
||||
sendReachedStorageLimitEmail,
|
||||
} from 'emails'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
@ -60,6 +57,7 @@ const handler = async (
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const checkStorageLimit = async (typebotId: string) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: { id: typebotId },
|
||||
@ -102,7 +100,7 @@ const checkStorageLimit = async (typebotId: string) => {
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendAlmostReachStorageLimitEmail({
|
||||
await sendAlmostReachStorageLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
@ -111,14 +109,14 @@ const checkStorageLimit = async (typebotId: string) => {
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendReachStorageLimitEmail({
|
||||
await sendReachStorageLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
storageLimit,
|
||||
})
|
||||
return totalStorageUsed >= storageLimitBytes
|
||||
}
|
||||
|
||||
const sendAlmostReachStorageLimitEmail = async ({
|
||||
const sendAlmostReachStorageLimitNotification = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
@ -129,22 +127,20 @@ const sendAlmostReachStorageLimitEmail = async ({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableStorageLimit = `${storageLimit}GB`
|
||||
await sendEmailNotification({
|
||||
|
||||
await sendAlmostReachedStorageLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You're close to your storage limit",
|
||||
html: almostReachedStorageLimitEmail({
|
||||
readableStorageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
storageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachStorageLimitEmail = async ({
|
||||
const sendReachStorageLimitNotification = async ({
|
||||
workspaceId,
|
||||
storageLimit,
|
||||
}: {
|
||||
@ -155,15 +151,13 @@ const sendReachStorageLimitEmail = async ({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableStorageLimit = `${storageLimit}GB`
|
||||
await sendEmailNotification({
|
||||
|
||||
await sendReachedStorageLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You've hit your storage limit",
|
||||
html: reachedStorageLimitEmail({
|
||||
readableStorageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
storageLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { storageLimitSecondEmailSentAt: new Date() },
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
saveSuccessLog,
|
||||
} from 'services/api/utils'
|
||||
import Mail from 'nodemailer/lib/mailer'
|
||||
import { newLeadEmailContent } from 'assets/emails/newLeadEmailContent'
|
||||
import { DefaultBotNotificationEmail, render } from 'emails'
|
||||
|
||||
const cors = initMiddleware(Cors())
|
||||
|
||||
@ -84,6 +84,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
typebotId,
|
||||
resultValues,
|
||||
})
|
||||
|
||||
if (!emailBody) {
|
||||
await saveErrorLog(resultId, 'Email not sent', {
|
||||
transportConfig,
|
||||
@ -121,6 +122,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await saveErrorLog(resultId, 'Email not sent', {
|
||||
transportConfig,
|
||||
email,
|
||||
error: err,
|
||||
})
|
||||
return res.status(500).send({
|
||||
message: `Email not sent. Error: ${err}`,
|
||||
@ -177,10 +179,12 @@ const getEmailBody = async ({
|
||||
],
|
||||
})(resultValues)
|
||||
return {
|
||||
html: newLeadEmailContent(
|
||||
`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`,
|
||||
omit(answers, 'submittedAt')
|
||||
),
|
||||
html: render(
|
||||
<DefaultBotNotificationEmail
|
||||
resultsUrl={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
|
||||
answers={omit(answers, 'submittedAt')}
|
||||
/>
|
||||
).html,
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { almostReachedChatsLimitEmail } from 'assets/emails/almostReachedChatsLimitEmail'
|
||||
import { reachedSChatsLimitEmail } from 'assets/emails/reachedChatsLimitEmail'
|
||||
import { Workspace, WorkspaceRole } from 'db'
|
||||
import {
|
||||
sendAlmostReachedChatsLimitEmail,
|
||||
sendReachedChatsLimitEmail,
|
||||
} from 'emails'
|
||||
import prisma from 'libs/prisma'
|
||||
import { ResultWithAnswers } from 'models'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { authenticateUser } from 'services/api/utils'
|
||||
import { env, getChatsLimit, isDefined, parseNumberWithCommas } from 'utils'
|
||||
import { sendEmailNotification, methodNotAllowed } from 'utils/api'
|
||||
import { env, getChatsLimit, isDefined } from 'utils'
|
||||
import { methodNotAllowed } from 'utils/api'
|
||||
|
||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||
|
||||
@ -60,6 +62,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const checkChatsUsage = async (
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
@ -96,87 +99,64 @@ const checkChatsUsage = async (
|
||||
!hasSentFirstEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendAlmostReachChatsLimitEmail({
|
||||
await sendAlmostReachChatsLimitNotification({
|
||||
workspaceId: workspace.id,
|
||||
chatLimit: chatsLimit,
|
||||
firstDayOfNextMonth,
|
||||
chatsLimit,
|
||||
})
|
||||
if (
|
||||
chatsCount >= chatsLimit &&
|
||||
!hasSentSecondEmail &&
|
||||
env('E2E_TEST') !== 'true'
|
||||
)
|
||||
await sendReachedAlertEmail({
|
||||
await sendReachedAlertNotification({
|
||||
workspaceId: workspace.id,
|
||||
chatLimit: chatsLimit,
|
||||
firstDayOfNextMonth,
|
||||
chatsLimit,
|
||||
})
|
||||
return chatsCount >= chatsLimit
|
||||
}
|
||||
|
||||
const sendAlmostReachChatsLimitEmail = async ({
|
||||
const sendAlmostReachChatsLimitNotification = async ({
|
||||
workspaceId,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
chatsLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatLimit: number
|
||||
firstDayOfNextMonth: Date
|
||||
chatsLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableChatsLimit = parseNumberWithCommas(chatLimit)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
|
||||
await sendEmailNotification({
|
||||
await sendAlmostReachedChatsLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You're close to your chats limit",
|
||||
html: almostReachedChatsLimitEmail({
|
||||
readableChatsLimit,
|
||||
readableResetDate,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
chatsLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitFirstEmailSentAt: new Date() },
|
||||
})
|
||||
}
|
||||
|
||||
const sendReachedAlertEmail = async ({
|
||||
const sendReachedAlertNotification = async ({
|
||||
workspaceId,
|
||||
chatLimit,
|
||||
firstDayOfNextMonth,
|
||||
chatsLimit,
|
||||
}: {
|
||||
workspaceId: string
|
||||
chatLimit: number
|
||||
firstDayOfNextMonth: Date
|
||||
chatsLimit: number
|
||||
}) => {
|
||||
const members = await prisma.memberInWorkspace.findMany({
|
||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||
include: { user: { select: { email: true } } },
|
||||
})
|
||||
const readableChatsLimit = parseNumberWithCommas(chatLimit)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
await sendEmailNotification({
|
||||
|
||||
await sendReachedChatsLimitEmail({
|
||||
to: members.map((member) => member.user.email).filter(isDefined),
|
||||
subject: "You've hit your monthly chats limit",
|
||||
html: reachedSChatsLimitEmail({
|
||||
readableChatsLimit,
|
||||
readableResetDate,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
}),
|
||||
chatsLimit,
|
||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||
})
|
||||
|
||||
await prisma.workspace.update({
|
||||
where: { id: workspaceId },
|
||||
data: { chatsLimitSecondEmailSentAt: new Date() },
|
||||
|
26
packages/emails/components/Button.tsx
Normal file
26
packages/emails/components/Button.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { MjmlButton } from '@faire/mjml-react'
|
||||
import { blue, grayLight } from '../theme'
|
||||
import { leadingTight, textBase, borderBase } from '../theme'
|
||||
|
||||
type ButtonProps = {
|
||||
link: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Button = ({ link, children }: ButtonProps) => (
|
||||
<MjmlButton
|
||||
lineHeight={leadingTight}
|
||||
fontSize={textBase}
|
||||
fontWeight="700"
|
||||
height={32}
|
||||
padding="0"
|
||||
align="left"
|
||||
href={link}
|
||||
backgroundColor={blue}
|
||||
color={grayLight}
|
||||
borderRadius={borderBase}
|
||||
>
|
||||
{children}
|
||||
</MjmlButton>
|
||||
)
|
81
packages/emails/components/Head.tsx
Normal file
81
packages/emails/components/Head.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import {
|
||||
MjmlHead,
|
||||
MjmlFont,
|
||||
MjmlAttributes,
|
||||
MjmlAll,
|
||||
MjmlStyle,
|
||||
MjmlRaw,
|
||||
} from '@faire/mjml-react'
|
||||
import { black, grayDark } from '../theme'
|
||||
|
||||
type HeadProps = { children?: ReactElement }
|
||||
|
||||
export const Head = ({ children }: HeadProps) => (
|
||||
<MjmlHead>
|
||||
<>
|
||||
<MjmlRaw>
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="supported-color-schemes" content="light" />
|
||||
</MjmlRaw>
|
||||
<MjmlFont
|
||||
name="Inter"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700"
|
||||
/>
|
||||
<MjmlStyle>{`
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
.smooth {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.paragraph a {
|
||||
color: ${black} !important;
|
||||
}
|
||||
.li {
|
||||
text-indent: -18px;
|
||||
margin-left: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
.footer a {
|
||||
text-decoration: none !important;
|
||||
color: ${grayDark} !important;
|
||||
}
|
||||
.dark-mode {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width:480px) {
|
||||
td.hero {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: ${black};
|
||||
}
|
||||
.logo > * {
|
||||
filter: invert(1) !important;
|
||||
}
|
||||
.paragraph > *, .paragraph a, .li > div {
|
||||
color: #fff !important;
|
||||
}
|
||||
.dark-mode {
|
||||
display: inherit;
|
||||
}
|
||||
.light-mode {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</MjmlStyle>
|
||||
<MjmlAttributes>
|
||||
<MjmlAll
|
||||
font-family='-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
|
||||
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
|
||||
font-weight="400"
|
||||
/>
|
||||
</MjmlAttributes>
|
||||
{children}
|
||||
</>
|
||||
</MjmlHead>
|
||||
)
|
15
packages/emails/components/HeroImage.tsx
Normal file
15
packages/emails/components/HeroImage.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { MjmlImageProps, MjmlImage } from '@faire/mjml-react'
|
||||
import React from 'react'
|
||||
import { borderBase } from '../theme'
|
||||
|
||||
export const HeroImage = (props: MjmlImageProps) => (
|
||||
<MjmlImage
|
||||
cssClass="hero"
|
||||
padding="0"
|
||||
align="left"
|
||||
borderRadius={borderBase}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</MjmlImage>
|
||||
)
|
15
packages/emails/components/Text.tsx
Normal file
15
packages/emails/components/Text.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { MjmlText, MjmlTextProps, PaddingProps } from '@faire/mjml-react'
|
||||
import React from 'react'
|
||||
import { leadingRelaxed, textBase } from '../theme'
|
||||
|
||||
export const Text = (props: MjmlTextProps & PaddingProps) => (
|
||||
<MjmlText
|
||||
padding="24px 0 0"
|
||||
fontSize={textBase}
|
||||
lineHeight={leadingRelaxed}
|
||||
cssClass="paragraph"
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</MjmlText>
|
||||
)
|
4
packages/emails/components/index.ts
Normal file
4
packages/emails/components/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { Button } from './Button'
|
||||
export { Text } from './Text'
|
||||
export { HeroImage } from './HeroImage'
|
||||
export { Head } from './Head'
|
78
packages/emails/emails/AlmostReachedChatsLimitEmail.tsx
Normal file
78
packages/emails/emails/AlmostReachedChatsLimitEmail.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { Button, Head, HeroImage, Text } from '../components'
|
||||
import { parseNumberWithCommas } from 'utils'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type AlmostReachedChatsLimitEmailProps = {
|
||||
chatsLimit: number
|
||||
url: string
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
|
||||
export const AlmostReachedChatsLimitEmail = ({
|
||||
chatsLimit,
|
||||
url,
|
||||
}: AlmostReachedChatsLimitEmailProps) => {
|
||||
const readableChatsLimit = parseNumberWithCommas(chatsLimit)
|
||||
|
||||
return (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>Your bots are chatting a lot. That's amazing. 💙</Text>
|
||||
<Text>
|
||||
This means you've almost reached your monthly chats limit. You
|
||||
currently reached 80% of {readableChatsLimit} chats.
|
||||
</Text>
|
||||
<Text>This limit will be reset on {readableResetDate}.</Text>
|
||||
<Text fontWeight={800}>
|
||||
Your bots won't start the chat if you reach the limit before this
|
||||
date❗
|
||||
</Text>
|
||||
<Text>
|
||||
If you need more monthly responses, you will need to upgrade your
|
||||
plan.
|
||||
</Text>
|
||||
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Upgrade workspace</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
}
|
||||
|
||||
export const sendAlmostReachedChatsLimitEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> &
|
||||
ComponentProps<typeof AlmostReachedChatsLimitEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You're close to your chats limit",
|
||||
html: render(<AlmostReachedChatsLimitEmail {...props} />).html,
|
||||
})
|
66
packages/emails/emails/AlmostReachedStorageLimitEmail.tsx
Normal file
66
packages/emails/emails/AlmostReachedStorageLimitEmail.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { Button, Head, HeroImage, Text } from '../components'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type AlmostReachedStorageLimitEmailProps = {
|
||||
storageLimit: number
|
||||
url: string
|
||||
}
|
||||
|
||||
export const AlmostReachedStorageLimitEmail = ({
|
||||
storageLimit,
|
||||
url,
|
||||
}: AlmostReachedStorageLimitEmailProps) => {
|
||||
const readableStorageLimit = `${storageLimit} GB`
|
||||
|
||||
return (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.fr-par.scw.cloud/public/assets/yourBotIsFlyingEmailBanner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>Your bots are working a lot. That's amazing. 🤖</Text>
|
||||
<Text>
|
||||
This means you've almost reached your storage limit. You currently
|
||||
reached 80% of your {readableStorageLimit} storage limit.
|
||||
</Text>
|
||||
<Text fontWeight={800}>
|
||||
Your bots won't collect new files once you reach the limit❗
|
||||
</Text>
|
||||
<Text>
|
||||
To make sure it won't happen, you need to upgrade your plan or
|
||||
delete existing results to free up space.
|
||||
</Text>
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Upgrade workspace</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
}
|
||||
|
||||
export const sendAlmostReachedStorageLimitEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> &
|
||||
ComponentProps<typeof AlmostReachedStorageLimitEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You're close to your storage limit",
|
||||
html: render(<AlmostReachedStorageLimitEmail {...props} />).html,
|
||||
})
|
49
packages/emails/emails/DefaultBotNotificationEmail.tsx
Normal file
49
packages/emails/emails/DefaultBotNotificationEmail.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { Head, Text, Button } from '../components'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
} from '@faire/mjml-react'
|
||||
|
||||
const emailRegex =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
type DefaultBotNotificationEmailProps = {
|
||||
resultsUrl: string
|
||||
answers: { [key: string]: string }
|
||||
}
|
||||
|
||||
export const DefaultBotNotificationEmail = ({
|
||||
resultsUrl,
|
||||
answers,
|
||||
}: DefaultBotNotificationEmailProps) => (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="32px" cssClass="smooth" border="1px solid #e2e8f0">
|
||||
<MjmlColumn>
|
||||
<Text padding="0">Your typebot has collected a new response! 🥳</Text>
|
||||
{Object.keys(answers).map((key) => {
|
||||
const isEmail = emailRegex.test(answers[key])
|
||||
|
||||
return (
|
||||
<Text key={key}>
|
||||
<b>{key}</b>:{' '}
|
||||
{isEmail ? (
|
||||
<a href={answers[key]}>{answers[key]}</a>
|
||||
) : (
|
||||
answers[key]
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={resultsUrl}>Go to results</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
66
packages/emails/emails/GuestInvitationEmail.tsx
Normal file
66
packages/emails/emails/GuestInvitationEmail.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { HeroImage, Text, Button, Head } from '../components'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type GuestInvitationEmailProps = {
|
||||
workspaceName: string
|
||||
typebotName: string
|
||||
url: string
|
||||
hostEmail: string
|
||||
guestEmail: string
|
||||
}
|
||||
|
||||
export const GuestInvitationEmail = ({
|
||||
workspaceName,
|
||||
typebotName,
|
||||
url,
|
||||
hostEmail,
|
||||
guestEmail,
|
||||
}: GuestInvitationEmailProps) => (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.eu-west-3.amazonaws.com/assets/invitation-banner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>
|
||||
You have been invited by {hostEmail} to collaborate on his typebot{' '}
|
||||
<strong>{typebotName}</strong>.
|
||||
</Text>
|
||||
<Text>
|
||||
From now on you will see this typebot in your dashboard under his
|
||||
workspace "{workspaceName}" 👍
|
||||
</Text>
|
||||
<Text>
|
||||
Make sure to log in as <i>{guestEmail}</i>.
|
||||
</Text>
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Go to typebot</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
|
||||
export const sendGuestInvitationEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> & ComponentProps<typeof GuestInvitationEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: render(<GuestInvitationEmail {...props} />).html,
|
||||
})
|
75
packages/emails/emails/ReachedChatsLimitEmail.tsx
Normal file
75
packages/emails/emails/ReachedChatsLimitEmail.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { Button, Head, HeroImage, Text } from '../components'
|
||||
import { parseNumberWithCommas } from 'utils'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type ReachedChatsLimitEmailProps = {
|
||||
chatsLimit: number
|
||||
url: string
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1)
|
||||
const readableResetDate = firstDayOfNextMonth
|
||||
.toDateString()
|
||||
.split(' ')
|
||||
.slice(1, 4)
|
||||
.join(' ')
|
||||
|
||||
export const ReachedChatsLimitEmail = ({
|
||||
chatsLimit,
|
||||
url,
|
||||
}: ReachedChatsLimitEmailProps) => {
|
||||
const readableChatsLimit = parseNumberWithCommas(chatsLimit)
|
||||
|
||||
return (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>
|
||||
It just happened, you've reached your monthly {readableChatsLimit}{' '}
|
||||
chats limit 😮
|
||||
</Text>
|
||||
<Text fontWeight={800}>
|
||||
It means your bots are closed until {readableResetDate}❗
|
||||
</Text>
|
||||
<Text>
|
||||
If you'd like to continue chatting with your users this month,
|
||||
then you need to upgrade your plan. 🚀
|
||||
</Text>
|
||||
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Upgrade workspace</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
}
|
||||
|
||||
export const sendReachedChatsLimitEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> &
|
||||
ComponentProps<typeof ReachedChatsLimitEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You've reached your chats limit",
|
||||
html: render(<ReachedChatsLimitEmail {...props} />).html,
|
||||
})
|
66
packages/emails/emails/ReachedStorageLimitEmail.tsx
Normal file
66
packages/emails/emails/ReachedStorageLimitEmail.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { Button, Head, HeroImage, Text } from '../components'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type ReachedStorageLimitEmailProps = {
|
||||
storageLimit: number
|
||||
url: string
|
||||
}
|
||||
|
||||
export const ReachedStorageLimitEmail = ({
|
||||
storageLimit,
|
||||
url,
|
||||
}: ReachedStorageLimitEmailProps) => {
|
||||
const readableStorageLimit = `${storageLimit} GB`
|
||||
|
||||
return (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.fr-par.scw.cloud/public/assets/actionRequiredEmailBanner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>
|
||||
It just happened, you've reached your {readableStorageLimit}{' '}
|
||||
storage limit 😮
|
||||
</Text>
|
||||
<Text fontWeight={800}>
|
||||
It means your bots won't collect new files from your users❗
|
||||
</Text>
|
||||
<Text>
|
||||
If you'd like to continue collecting files, then you need to
|
||||
upgrade your plan or remove existing results to free up space. 🚀
|
||||
</Text>
|
||||
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Upgrade workspace</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
}
|
||||
|
||||
export const sendReachedStorageLimitEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> &
|
||||
ComponentProps<typeof ReachedStorageLimitEmail>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You've reached your storage limit",
|
||||
html: render(<ReachedStorageLimitEmail {...props} />).html,
|
||||
})
|
64
packages/emails/emails/WorkspaceMemberInvitationEmail.tsx
Normal file
64
packages/emails/emails/WorkspaceMemberInvitationEmail.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { ComponentProps } from 'react'
|
||||
import {
|
||||
Mjml,
|
||||
MjmlBody,
|
||||
MjmlSection,
|
||||
MjmlColumn,
|
||||
MjmlSpacer,
|
||||
render,
|
||||
} from '@faire/mjml-react'
|
||||
import { HeroImage, Text, Button, Head } from '../components'
|
||||
import { SendMailOptions } from 'nodemailer'
|
||||
import { sendEmail } from '../sendEmail'
|
||||
|
||||
type WorkspaceMemberInvitationProps = {
|
||||
workspaceName: string
|
||||
url: string
|
||||
hostEmail: string
|
||||
guestEmail: string
|
||||
}
|
||||
|
||||
export const WorkspaceMemberInvitation = ({
|
||||
workspaceName,
|
||||
url,
|
||||
hostEmail,
|
||||
guestEmail,
|
||||
}: WorkspaceMemberInvitationProps) => (
|
||||
<Mjml>
|
||||
<Head />
|
||||
<MjmlBody width={600}>
|
||||
<MjmlSection padding="0">
|
||||
<MjmlColumn>
|
||||
<HeroImage src="https://typebot.s3.eu-west-3.amazonaws.com/assets/invitation-banner.png" />
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
<MjmlSection padding="0 24px" cssClass="smooth">
|
||||
<MjmlColumn>
|
||||
<Text>
|
||||
You have been invited by {hostEmail} to collaborate on his workspace{' '}
|
||||
<strong>{workspaceName}</strong> as a team member.
|
||||
</Text>
|
||||
<Text>
|
||||
From now on you will see this workspace in your dashboard 👍
|
||||
</Text>
|
||||
<Text>
|
||||
Make sure to log in as <i>{guestEmail}</i>.
|
||||
</Text>
|
||||
<MjmlSpacer height="24px" />
|
||||
<Button link={url}>Go to workspace</Button>
|
||||
</MjmlColumn>
|
||||
</MjmlSection>
|
||||
</MjmlBody>
|
||||
</Mjml>
|
||||
)
|
||||
|
||||
export const sendWorkspaceMemberInvitationEmail = ({
|
||||
to,
|
||||
...props
|
||||
}: Pick<SendMailOptions, 'to'> &
|
||||
ComponentProps<typeof WorkspaceMemberInvitation>) =>
|
||||
sendEmail({
|
||||
to,
|
||||
subject: "You've been invited to collaborate 🤝",
|
||||
html: render(<WorkspaceMemberInvitation {...props} />).html,
|
||||
})
|
7
packages/emails/emails/index.ts
Normal file
7
packages/emails/emails/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './AlmostReachedChatsLimitEmail'
|
||||
export * from './AlmostReachedStorageLimitEmail'
|
||||
export * from './DefaultBotNotificationEmail'
|
||||
export * from './GuestInvitationEmail'
|
||||
export * from './ReachedChatsLimitEmail'
|
||||
export * from './ReachedStorageLimitEmail'
|
||||
export * from './WorkspaceMemberInvitationEmail'
|
2
packages/emails/index.ts
Normal file
2
packages/emails/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './emails'
|
||||
export { render } from '@faire/mjml-react'
|
32
packages/emails/package.json
Normal file
32
packages/emails/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "emails",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"scripts": {
|
||||
"preview": "concurrently \"pnpm run watch\" \"sleep 5 && pnpm run serve\" -n \"watch,serve\" -c \"bgBlue.bold,bgMagenta.bold\"",
|
||||
"watch": "tsx watch ./preview.tsx --clear-screen=false",
|
||||
"serve": "http-server dist -a localhost -p 3223 -o"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Baptiste Arnaud",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@faire/mjml-react": "^2.1.4",
|
||||
"@types/node": "18.7.16",
|
||||
"@types/nodemailer": "6.4.5",
|
||||
"@types/react": "^18.0.21",
|
||||
"concurrently": "^7.4.0",
|
||||
"http-server": "^14.1.1",
|
||||
"nodemailer": "^6.8.0",
|
||||
"react": "^18.2.0",
|
||||
"serve": "^14.0.1",
|
||||
"tsx": "^3.9.0",
|
||||
"utils": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@faire/mjml-react": "^2.1.4",
|
||||
"nodemailer": "^6.7.8"
|
||||
}
|
||||
}
|
98
packages/emails/preview.tsx
Normal file
98
packages/emails/preview.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
import { render } from '@faire/mjml-react'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import {
|
||||
AlmostReachedChatsLimitEmail,
|
||||
AlmostReachedStorageLimitEmail,
|
||||
DefaultBotNotificationEmail,
|
||||
GuestInvitationEmail,
|
||||
ReachedChatsLimitEmail,
|
||||
ReachedStorageLimitEmail,
|
||||
WorkspaceMemberInvitation,
|
||||
} from './emails'
|
||||
|
||||
const createDistFolder = () => {
|
||||
const dist = path.resolve(__dirname, 'dist')
|
||||
if (!fs.existsSync(dist)) {
|
||||
fs.mkdirSync(dist)
|
||||
}
|
||||
}
|
||||
|
||||
const createHtmlFile = () => {
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'guestInvitation.html'),
|
||||
render(
|
||||
<GuestInvitationEmail
|
||||
workspaceName={'Typebot'}
|
||||
typebotName={'Lead Generation'}
|
||||
url={'https://app.typebot.io'}
|
||||
hostEmail={'baptiste@typebot.io'}
|
||||
guestEmail={'guest@typebot.io'}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'workspaceMemberInvitation.html'),
|
||||
render(
|
||||
<WorkspaceMemberInvitation
|
||||
workspaceName={'Typebot'}
|
||||
url={'https://app.typebot.io'}
|
||||
hostEmail={'baptiste@typebot.io'}
|
||||
guestEmail={'guest@typebot.io'}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'almostReachedChatsLimit.html'),
|
||||
render(
|
||||
<AlmostReachedChatsLimitEmail
|
||||
url={'https://app.typebot.io'}
|
||||
chatsLimit={2000}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'almostReachedStorageLimit.html'),
|
||||
render(
|
||||
<AlmostReachedStorageLimitEmail
|
||||
url={'https://app.typebot.io'}
|
||||
storageLimit={4}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'reachedChatsLimit.html'),
|
||||
render(
|
||||
<ReachedChatsLimitEmail
|
||||
url={'https://app.typebot.io'}
|
||||
chatsLimit={10000}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'reachedStorageLimit.html'),
|
||||
render(
|
||||
<ReachedStorageLimitEmail
|
||||
url={'https://app.typebot.io'}
|
||||
storageLimit={8}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, 'dist', 'defaultBotNotification.html'),
|
||||
render(
|
||||
<DefaultBotNotificationEmail
|
||||
resultsUrl={'https://app.typebot.io'}
|
||||
answers={{
|
||||
'Group #1': 'Answer #1',
|
||||
Name: 'Baptiste',
|
||||
Email: 'baptiste.arnaud95@gmail.com',
|
||||
}}
|
||||
/>
|
||||
).html
|
||||
)
|
||||
}
|
||||
|
||||
createDistFolder()
|
||||
createHtmlFile()
|
@ -1,7 +1,9 @@
|
||||
import { createTransport, SendMailOptions } from 'nodemailer'
|
||||
import { env } from '../utils'
|
||||
import { env } from 'utils'
|
||||
|
||||
export const sendEmailNotification = (props: Omit<SendMailOptions, 'from'>) => {
|
||||
export const sendEmail = (
|
||||
props: Pick<SendMailOptions, 'to' | 'html' | 'subject'>
|
||||
) => {
|
||||
const transporter = createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
17
packages/emails/theme.ts
Normal file
17
packages/emails/theme.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// Colors
|
||||
export const black = '#000'
|
||||
export const gold = '#fadf98'
|
||||
export const grayDark = '#888'
|
||||
export const grayLight = '#f5f5f5'
|
||||
export const blue = '#0042da'
|
||||
|
||||
// Typography
|
||||
export const textSm = 14
|
||||
export const textBase = 16
|
||||
export const textLg = 24
|
||||
export const textXl = 30
|
||||
export const leadingTight = '120%'
|
||||
export const leadingRelaxed = '160%'
|
||||
|
||||
// Borders
|
||||
export const borderBase = 6
|
6
packages/emails/tsconfig.json
Normal file
6
packages/emails/tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { PrismaClient } from 'db'
|
||||
import path from 'path'
|
||||
import { injectFakeResults } from 'utils'
|
||||
|
||||
require('dotenv').config({
|
||||
path: path.join(
|
||||
@ -11,13 +10,6 @@ require('dotenv').config({
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const main = async () => {
|
||||
await injectFakeResults(prisma)({
|
||||
count: 200,
|
||||
typebotId: 'cl8hl08xt009909l6pwqenf63',
|
||||
isChronological: false,
|
||||
fakeStorage: 3 * 1024 * 1024 * 1024,
|
||||
})
|
||||
}
|
||||
const main = async () => {}
|
||||
|
||||
main().then()
|
||||
|
@ -15,6 +15,7 @@
|
||||
"models": "workspace:*",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.3",
|
||||
"utils": "workspace:*"
|
||||
"utils": "workspace:*",
|
||||
"emails": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './utils'
|
||||
export * from './storage'
|
||||
export * from './sendEmailNotification'
|
||||
export * from './encryption'
|
||||
|
6512
pnpm-lock.yaml
generated
6512
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user