🧑💻 (emails) Add decent emails management
Use mjml-react to generate emails. Put all emails in a independent package.
This commit is contained in:
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()
|
20
packages/emails/sendEmail.ts
Normal file
20
packages/emails/sendEmail.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { createTransport, SendMailOptions } from 'nodemailer'
|
||||
import { env } from 'utils'
|
||||
|
||||
export const sendEmail = (
|
||||
props: Pick<SendMailOptions, 'to' | 'html' | 'subject'>
|
||||
) => {
|
||||
const transporter = createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
auth: {
|
||||
user: process.env.SMTP_USERNAME,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
},
|
||||
})
|
||||
|
||||
return transporter.sendMail({
|
||||
from: process.env.SMTP_FROM ?? env('SMTP_FROM'),
|
||||
...props,
|
||||
})
|
||||
}
|
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user