🧑💻 (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 */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const { withSentryConfig } = require('@sentry/nextjs')
|
const { withSentryConfig } = require('@sentry/nextjs')
|
||||||
const path = require('path')
|
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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"minio": "7.0.32",
|
"minio": "7.0.32",
|
||||||
"next": "12.3.0",
|
"next": "12.3.0",
|
||||||
"next-auth": "4.10.3",
|
"next-auth": "4.10.3",
|
||||||
"nodemailer": "^6.7.8",
|
"nodemailer": "^6.8.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"papaparse": "^5.3.2",
|
"papaparse": "^5.3.2",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
@@ -80,7 +80,9 @@
|
|||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"tinycolor2": "^1.4.2",
|
"tinycolor2": "^1.4.2",
|
||||||
"typebot-js": "workspace:*",
|
"typebot-js": "workspace:*",
|
||||||
"use-debounce": "8.0.4"
|
"use-debounce": "8.0.4",
|
||||||
|
"emails": "workspace:*",
|
||||||
|
"@faire/mjml-react": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.19.0",
|
"@babel/core": "7.19.0",
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
/* eslint-disable @next/next/no-sync-scripts */
|
/* eslint-disable @next/next/no-sync-scripts */
|
||||||
import Document, {
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
Html,
|
|
||||||
Head,
|
|
||||||
Main,
|
|
||||||
NextScript,
|
|
||||||
DocumentContext,
|
|
||||||
} from 'next/document'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
const Document = () => (
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
<Html>
|
||||||
const initialProps = await Document.getInitialProps(ctx)
|
<Head>
|
||||||
return { ...initialProps }
|
<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() {
|
export default Document
|
||||||
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
|
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export function CustomAdapter(p: PrismaClient): Adapter {
|
|||||||
},
|
},
|
||||||
updateUser: (data) => p.user.update({ where: { id: data.id }, data }),
|
updateUser: (data) => p.user.update({ where: { id: data.id }, data }),
|
||||||
deleteUser: (id) => p.user.delete({ where: { id } }),
|
deleteUser: (id) => p.user.delete({ where: { id } }),
|
||||||
linkAccount: (data) => {
|
linkAccount: async (data) => {
|
||||||
return p.account.create({
|
await p.account.create({
|
||||||
data: {
|
data: {
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
@@ -105,10 +105,11 @@ export function CustomAdapter(p: PrismaClient): Adapter {
|
|||||||
oauth_token: data.oauth_token as string,
|
oauth_token: data.oauth_token as string,
|
||||||
refresh_token_expires_in: data.refresh_token_expires_in as number,
|
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) {
|
async getSessionAndUser(sessionToken) {
|
||||||
const userAndSession = await p.session.findUnique({
|
const userAndSession = await p.session.findUnique({
|
||||||
where: { sessionToken },
|
where: { sessionToken },
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { invitationToCollaborate } from 'assets/emails/invitationToCollaborate'
|
|
||||||
import { CollaborationType, WorkspaceRole } from 'db'
|
import { CollaborationType, WorkspaceRole } from 'db'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules'
|
import { canReadTypebot, canWriteTypebot } from 'services/api/dbRules'
|
||||||
import {
|
import {
|
||||||
sendEmailNotification,
|
|
||||||
badRequest,
|
badRequest,
|
||||||
forbidden,
|
forbidden,
|
||||||
methodNotAllowed,
|
methodNotAllowed,
|
||||||
@@ -13,6 +11,7 @@ import {
|
|||||||
} from 'utils/api'
|
} from 'utils/api'
|
||||||
import { getAuthenticatedUser } from 'services/api/utils'
|
import { getAuthenticatedUser } from 'services/api/utils'
|
||||||
import { env } from 'utils'
|
import { env } from 'utils'
|
||||||
|
import { sendGuestInvitationEmail } from 'emails'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req)
|
const user = await getAuthenticatedUser(req)
|
||||||
@@ -68,16 +67,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
data: { email: email.toLowerCase().trim(), type, typebotId },
|
data: { email: email.toLowerCase().trim(), type, typebotId },
|
||||||
})
|
})
|
||||||
if (env('E2E_TEST') !== 'true')
|
if (env('E2E_TEST') !== 'true')
|
||||||
await sendEmailNotification({
|
await sendGuestInvitationEmail({
|
||||||
to: email,
|
to: email,
|
||||||
subject: "You've been invited to collaborate 🤝",
|
hostEmail: user.email ?? '',
|
||||||
html: invitationToCollaborate({
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
|
||||||
hostEmail: user.email ?? '',
|
guestEmail: email.toLowerCase(),
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${typebot.workspaceId}`,
|
typebotName: typebot.name,
|
||||||
guestEmail: email.toLowerCase(),
|
workspaceName: typebot.workspace?.name ?? '',
|
||||||
typebotName: typebot.name,
|
|
||||||
workspaceName: typebot.workspace?.name ?? '',
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
return res.send({
|
return res.send({
|
||||||
message: 'success',
|
message: 'success',
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { workspaceMemberInvitationEmail } from 'assets/emails/workspaceMemberInvitation'
|
|
||||||
import { Workspace, WorkspaceInvitation, WorkspaceRole } from 'db'
|
import { Workspace, WorkspaceInvitation, WorkspaceRole } from 'db'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import {
|
import { forbidden, methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||||
sendEmailNotification,
|
|
||||||
forbidden,
|
|
||||||
methodNotAllowed,
|
|
||||||
notAuthenticated,
|
|
||||||
} from 'utils/api'
|
|
||||||
import { getAuthenticatedUser } from 'services/api/utils'
|
import { getAuthenticatedUser } from 'services/api/utils'
|
||||||
import { env, seatsLimit } from 'utils'
|
import { env, seatsLimit } from 'utils'
|
||||||
|
import { sendWorkspaceMemberInvitationEmail } from 'emails'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req)
|
const user = await getAuthenticatedUser(req)
|
||||||
@@ -39,15 +34,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (env('E2E_TEST') !== 'true')
|
if (env('E2E_TEST') !== 'true')
|
||||||
await sendEmailNotification({
|
await sendWorkspaceMemberInvitationEmail({
|
||||||
to: data.email,
|
to: data.email,
|
||||||
subject: "You've been invited to collaborate 🤝",
|
workspaceName: workspace.name,
|
||||||
html: workspaceMemberInvitationEmail({
|
guestEmail: data.email,
|
||||||
workspaceName: workspace.name,
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||||
guestEmail: data.email,
|
hostEmail: user.email ?? '',
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
|
||||||
hostEmail: user.email ?? '',
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
return res.send({
|
return res.send({
|
||||||
member: {
|
member: {
|
||||||
@@ -61,15 +53,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
} else {
|
} else {
|
||||||
const invitation = await prisma.workspaceInvitation.create({ data })
|
const invitation = await prisma.workspaceInvitation.create({ data })
|
||||||
if (env('E2E_TEST') !== 'true')
|
if (env('E2E_TEST') !== 'true')
|
||||||
await sendEmailNotification({
|
await sendWorkspaceMemberInvitationEmail({
|
||||||
to: data.email,
|
to: data.email,
|
||||||
subject: "You've been invited to collaborate 🤝",
|
workspaceName: workspace.name,
|
||||||
html: workspaceMemberInvitationEmail({
|
guestEmail: data.email,
|
||||||
workspaceName: workspace.name,
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
||||||
guestEmail: data.email,
|
hostEmail: user.email ?? '',
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspace.id}`,
|
|
||||||
hostEmail: user.email ?? '',
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
return res.send({ invitation })
|
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 */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
const { withSentryConfig } = require('@sentry/nextjs')
|
const { withSentryConfig } = require('@sentry/nextjs')
|
||||||
const path = require('path')
|
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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "5.36.2",
|
"@typescript-eslint/eslint-plugin": "5.36.2",
|
||||||
"@typescript-eslint/parser": "5.36.2",
|
"@typescript-eslint/parser": "5.36.2",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
|
"emails": "workspace:*",
|
||||||
"eslint": "8.23.0",
|
"eslint": "8.23.0",
|
||||||
"eslint-config-next": "12.3.0",
|
"eslint-config-next": "12.3.0",
|
||||||
"eslint-plugin-react": "^7.31.8",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^9.0.0",
|
||||||
"papaparse": "^5.3.2",
|
"papaparse": "^5.3.2",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
|
"uglify-js": "^3.17.2",
|
||||||
"utils": "workspace:*"
|
"utils": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,16 @@
|
|||||||
import Document, {
|
/* eslint-disable @next/next/no-sync-scripts */
|
||||||
Html,
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
Head,
|
|
||||||
Main,
|
|
||||||
NextScript,
|
|
||||||
DocumentContext,
|
|
||||||
} from 'next/document'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
const Document = () => (
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
<Html>
|
||||||
const initialProps = await Document.getInitialProps(ctx)
|
<Head>
|
||||||
return { ...initialProps }
|
<script src="/__env.js" />
|
||||||
}
|
</Head>
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
|
||||||
render() {
|
export default Document
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { almostReachedStorageLimitEmail } from 'assets/emails/almostReachedStorageLimitEmail'
|
|
||||||
import { reachedStorageLimitEmail } from 'assets/emails/reachedStorageLimitEmail'
|
|
||||||
import { WorkspaceRole } from 'db'
|
import { WorkspaceRole } from 'db'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { InputBlockType, PublicTypebot } from 'models'
|
import { InputBlockType, PublicTypebot } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import {
|
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils/api'
|
||||||
badRequest,
|
|
||||||
generatePresignedUrl,
|
|
||||||
methodNotAllowed,
|
|
||||||
sendEmailNotification,
|
|
||||||
} from 'utils/api'
|
|
||||||
import { byId, getStorageLimit, isDefined, env } from 'utils'
|
import { byId, getStorageLimit, isDefined, env } from 'utils'
|
||||||
|
import {
|
||||||
|
sendAlmostReachedStorageLimitEmail,
|
||||||
|
sendReachedStorageLimitEmail,
|
||||||
|
} from 'emails'
|
||||||
|
|
||||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||||
|
|
||||||
@@ -60,6 +57,7 @@ const handler = async (
|
|||||||
return methodNotAllowed(res)
|
return methodNotAllowed(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const checkStorageLimit = async (typebotId: string) => {
|
const checkStorageLimit = async (typebotId: string) => {
|
||||||
const typebot = await prisma.typebot.findFirst({
|
const typebot = await prisma.typebot.findFirst({
|
||||||
where: { id: typebotId },
|
where: { id: typebotId },
|
||||||
@@ -102,7 +100,7 @@ const checkStorageLimit = async (typebotId: string) => {
|
|||||||
!hasSentFirstEmail &&
|
!hasSentFirstEmail &&
|
||||||
env('E2E_TEST') !== 'true'
|
env('E2E_TEST') !== 'true'
|
||||||
)
|
)
|
||||||
await sendAlmostReachStorageLimitEmail({
|
await sendAlmostReachStorageLimitNotification({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
})
|
})
|
||||||
@@ -111,14 +109,14 @@ const checkStorageLimit = async (typebotId: string) => {
|
|||||||
!hasSentSecondEmail &&
|
!hasSentSecondEmail &&
|
||||||
env('E2E_TEST') !== 'true'
|
env('E2E_TEST') !== 'true'
|
||||||
)
|
)
|
||||||
await sendReachStorageLimitEmail({
|
await sendReachStorageLimitNotification({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
})
|
})
|
||||||
return totalStorageUsed >= storageLimitBytes
|
return totalStorageUsed >= storageLimitBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAlmostReachStorageLimitEmail = async ({
|
const sendAlmostReachStorageLimitNotification = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
}: {
|
}: {
|
||||||
@@ -129,22 +127,20 @@ const sendAlmostReachStorageLimitEmail = async ({
|
|||||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||||
include: { user: { select: { email: true } } },
|
include: { user: { select: { email: true } } },
|
||||||
})
|
})
|
||||||
const readableStorageLimit = `${storageLimit}GB`
|
|
||||||
await sendEmailNotification({
|
await sendAlmostReachedStorageLimitEmail({
|
||||||
to: members.map((member) => member.user.email).filter(isDefined),
|
to: members.map((member) => member.user.email).filter(isDefined),
|
||||||
subject: "You're close to your storage limit",
|
storageLimit,
|
||||||
html: almostReachedStorageLimitEmail({
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||||
readableStorageLimit,
|
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await prisma.workspace.update({
|
await prisma.workspace.update({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
data: { storageLimitFirstEmailSentAt: new Date() },
|
data: { storageLimitFirstEmailSentAt: new Date() },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendReachStorageLimitEmail = async ({
|
const sendReachStorageLimitNotification = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
storageLimit,
|
storageLimit,
|
||||||
}: {
|
}: {
|
||||||
@@ -155,15 +151,13 @@ const sendReachStorageLimitEmail = async ({
|
|||||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||||
include: { user: { select: { email: true } } },
|
include: { user: { select: { email: true } } },
|
||||||
})
|
})
|
||||||
const readableStorageLimit = `${storageLimit}GB`
|
|
||||||
await sendEmailNotification({
|
await sendReachedStorageLimitEmail({
|
||||||
to: members.map((member) => member.user.email).filter(isDefined),
|
to: members.map((member) => member.user.email).filter(isDefined),
|
||||||
subject: "You've hit your storage limit",
|
storageLimit,
|
||||||
html: reachedStorageLimitEmail({
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||||
readableStorageLimit,
|
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await prisma.workspace.update({
|
await prisma.workspace.update({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
data: { storageLimitSecondEmailSentAt: new Date() },
|
data: { storageLimitSecondEmailSentAt: new Date() },
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
saveSuccessLog,
|
saveSuccessLog,
|
||||||
} from 'services/api/utils'
|
} from 'services/api/utils'
|
||||||
import Mail from 'nodemailer/lib/mailer'
|
import Mail from 'nodemailer/lib/mailer'
|
||||||
import { newLeadEmailContent } from 'assets/emails/newLeadEmailContent'
|
import { DefaultBotNotificationEmail, render } from 'emails'
|
||||||
|
|
||||||
const cors = initMiddleware(Cors())
|
const cors = initMiddleware(Cors())
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
typebotId,
|
typebotId,
|
||||||
resultValues,
|
resultValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!emailBody) {
|
if (!emailBody) {
|
||||||
await saveErrorLog(resultId, 'Email not sent', {
|
await saveErrorLog(resultId, 'Email not sent', {
|
||||||
transportConfig,
|
transportConfig,
|
||||||
@@ -121,6 +122,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
await saveErrorLog(resultId, 'Email not sent', {
|
await saveErrorLog(resultId, 'Email not sent', {
|
||||||
transportConfig,
|
transportConfig,
|
||||||
email,
|
email,
|
||||||
|
error: err,
|
||||||
})
|
})
|
||||||
return res.status(500).send({
|
return res.status(500).send({
|
||||||
message: `Email not sent. Error: ${err}`,
|
message: `Email not sent. Error: ${err}`,
|
||||||
@@ -177,10 +179,12 @@ const getEmailBody = async ({
|
|||||||
],
|
],
|
||||||
})(resultValues)
|
})(resultValues)
|
||||||
return {
|
return {
|
||||||
html: newLeadEmailContent(
|
html: render(
|
||||||
`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`,
|
<DefaultBotNotificationEmail
|
||||||
omit(answers, 'submittedAt')
|
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 { Workspace, WorkspaceRole } from 'db'
|
||||||
|
import {
|
||||||
|
sendAlmostReachedChatsLimitEmail,
|
||||||
|
sendReachedChatsLimitEmail,
|
||||||
|
} from 'emails'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { ResultWithAnswers } from 'models'
|
import { ResultWithAnswers } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { env, getChatsLimit, isDefined, parseNumberWithCommas } from 'utils'
|
import { env, getChatsLimit, isDefined } from 'utils'
|
||||||
import { sendEmailNotification, methodNotAllowed } from 'utils/api'
|
import { methodNotAllowed } from 'utils/api'
|
||||||
|
|
||||||
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
const LIMIT_EMAIL_TRIGGER_PERCENT = 0.8
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
methodNotAllowed(res)
|
methodNotAllowed(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const checkChatsUsage = async (
|
const checkChatsUsage = async (
|
||||||
workspace: Pick<
|
workspace: Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -96,87 +99,64 @@ const checkChatsUsage = async (
|
|||||||
!hasSentFirstEmail &&
|
!hasSentFirstEmail &&
|
||||||
env('E2E_TEST') !== 'true'
|
env('E2E_TEST') !== 'true'
|
||||||
)
|
)
|
||||||
await sendAlmostReachChatsLimitEmail({
|
await sendAlmostReachChatsLimitNotification({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
chatLimit: chatsLimit,
|
chatsLimit,
|
||||||
firstDayOfNextMonth,
|
|
||||||
})
|
})
|
||||||
if (
|
if (
|
||||||
chatsCount >= chatsLimit &&
|
chatsCount >= chatsLimit &&
|
||||||
!hasSentSecondEmail &&
|
!hasSentSecondEmail &&
|
||||||
env('E2E_TEST') !== 'true'
|
env('E2E_TEST') !== 'true'
|
||||||
)
|
)
|
||||||
await sendReachedAlertEmail({
|
await sendReachedAlertNotification({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
chatLimit: chatsLimit,
|
chatsLimit,
|
||||||
firstDayOfNextMonth,
|
|
||||||
})
|
})
|
||||||
return chatsCount >= chatsLimit
|
return chatsCount >= chatsLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendAlmostReachChatsLimitEmail = async ({
|
const sendAlmostReachChatsLimitNotification = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
chatLimit,
|
chatsLimit,
|
||||||
firstDayOfNextMonth,
|
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
chatLimit: number
|
chatsLimit: number
|
||||||
firstDayOfNextMonth: Date
|
|
||||||
}) => {
|
}) => {
|
||||||
const members = await prisma.memberInWorkspace.findMany({
|
const members = await prisma.memberInWorkspace.findMany({
|
||||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||||
include: { user: { select: { email: true } } },
|
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),
|
to: members.map((member) => member.user.email).filter(isDefined),
|
||||||
subject: "You're close to your chats limit",
|
chatsLimit,
|
||||||
html: almostReachedChatsLimitEmail({
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||||
readableChatsLimit,
|
|
||||||
readableResetDate,
|
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await prisma.workspace.update({
|
await prisma.workspace.update({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
data: { chatsLimitFirstEmailSentAt: new Date() },
|
data: { chatsLimitFirstEmailSentAt: new Date() },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendReachedAlertEmail = async ({
|
const sendReachedAlertNotification = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
chatLimit,
|
chatsLimit,
|
||||||
firstDayOfNextMonth,
|
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
chatLimit: number
|
chatsLimit: number
|
||||||
firstDayOfNextMonth: Date
|
|
||||||
}) => {
|
}) => {
|
||||||
const members = await prisma.memberInWorkspace.findMany({
|
const members = await prisma.memberInWorkspace.findMany({
|
||||||
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
where: { role: WorkspaceRole.ADMIN, workspaceId },
|
||||||
include: { user: { select: { email: true } } },
|
include: { user: { select: { email: true } } },
|
||||||
})
|
})
|
||||||
const readableChatsLimit = parseNumberWithCommas(chatLimit)
|
|
||||||
const readableResetDate = firstDayOfNextMonth
|
await sendReachedChatsLimitEmail({
|
||||||
.toDateString()
|
|
||||||
.split(' ')
|
|
||||||
.slice(1, 4)
|
|
||||||
.join(' ')
|
|
||||||
await sendEmailNotification({
|
|
||||||
to: members.map((member) => member.user.email).filter(isDefined),
|
to: members.map((member) => member.user.email).filter(isDefined),
|
||||||
subject: "You've hit your monthly chats limit",
|
chatsLimit,
|
||||||
html: reachedSChatsLimitEmail({
|
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
||||||
readableChatsLimit,
|
|
||||||
readableResetDate,
|
|
||||||
url: `${process.env.NEXTAUTH_URL}/typebots?workspaceId=${workspaceId}`,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await prisma.workspace.update({
|
await prisma.workspace.update({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
data: { chatsLimitSecondEmailSentAt: new Date() },
|
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 { 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({
|
const transporter = createTransport({
|
||||||
host: process.env.SMTP_HOST,
|
host: process.env.SMTP_HOST,
|
||||||
port: Number(process.env.SMTP_PORT),
|
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 { PrismaClient } from 'db'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { injectFakeResults } from 'utils'
|
|
||||||
|
|
||||||
require('dotenv').config({
|
require('dotenv').config({
|
||||||
path: path.join(
|
path: path.join(
|
||||||
@@ -11,13 +10,6 @@ require('dotenv').config({
|
|||||||
|
|
||||||
const prisma = new PrismaClient()
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {}
|
||||||
await injectFakeResults(prisma)({
|
|
||||||
count: 200,
|
|
||||||
typebotId: 'cl8hl08xt009909l6pwqenf63',
|
|
||||||
isChronological: false,
|
|
||||||
fakeStorage: 3 * 1024 * 1024 * 1024,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
main().then()
|
main().then()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"models": "workspace:*",
|
"models": "workspace:*",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"utils": "workspace:*"
|
"utils": "workspace:*",
|
||||||
|
"emails": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './storage'
|
export * from './storage'
|
||||||
export * from './sendEmailNotification'
|
|
||||||
export * from './encryption'
|
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