5078 ability to invite team members (#5750)
## Added features - update team member setting page - add a section to send invitation by email - add a new invitation email - update email font to 'Trebuchet MS' as Google Inter font is not working, we need to use a web safe font https://templates.mailchimp.com/design/typography/ ## Demo https://github.com/twentyhq/twenty/assets/29927851/c731d883-1599-4281-87e3-0671f36994ae ## Invitation Email 
This commit is contained in:
@ -27,17 +27,25 @@ export const emailTheme = {
|
||||
colors: {
|
||||
highlighted: grayScale.gray60,
|
||||
primary: grayScale.gray50,
|
||||
tertiary: grayScale.gray40,
|
||||
tertiary: grayScale.gray35,
|
||||
inverted: grayScale.gray0,
|
||||
},
|
||||
family: 'Trebuchet MS', // Google Inter not working, we need to use a web safe font, see https://templates.mailchimp.com/design/typography/
|
||||
weight: {
|
||||
regular: 400,
|
||||
bold: 600,
|
||||
},
|
||||
size: {
|
||||
sm: '12px',
|
||||
md: '13px',
|
||||
lg: '16px',
|
||||
xl: '24px',
|
||||
},
|
||||
lineHeight: '20px',
|
||||
},
|
||||
border: {
|
||||
radius: { sm: '4px', md: '8px' },
|
||||
color: { highlighted: grayScale.gray20 },
|
||||
},
|
||||
background: {
|
||||
colors: { highlight: grayScale.gray15 },
|
||||
|
||||
@ -7,12 +7,8 @@ export const BaseHead = () => {
|
||||
<Head>
|
||||
<title>Twenty email</title>
|
||||
<Font
|
||||
fontFamily="Inter"
|
||||
fontFamily={emailTheme.font.family}
|
||||
fallbackFontFamily="sans-serif"
|
||||
webFont={{
|
||||
url: 'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap',
|
||||
format: 'woff2',
|
||||
}}
|
||||
fontStyle="normal"
|
||||
fontWeight={emailTheme.font.weight.regular}
|
||||
/>
|
||||
|
||||
@ -6,7 +6,7 @@ import { emailTheme } from 'src/common-style';
|
||||
const callToActionStyle = {
|
||||
display: 'flex',
|
||||
padding: '8px 32px',
|
||||
borderRadius: '8px',
|
||||
borderRadius: emailTheme.border.radius.md,
|
||||
border: `1px solid ${emailTheme.background.transparent.light}`,
|
||||
background: emailTheme.background.radialGradient,
|
||||
boxShadow: `0px 2px 4px 0px ${emailTheme.background.transparent.light}, 0px 0px 4px 0px ${emailTheme.background.transparent.medium}`,
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Container } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
type HighlightedContainerProps = PropsWithChildren;
|
||||
|
||||
const highlightedContainerStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: emailTheme.background.colors.highlight,
|
||||
border: `1px solid ${emailTheme.border.color.highlighted}`,
|
||||
borderRadius: emailTheme.border.radius.md,
|
||||
padding: '24px 48px',
|
||||
gap: '24px',
|
||||
};
|
||||
|
||||
const divStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
} as React.CSSProperties;
|
||||
|
||||
export const HighlightedContainer = ({
|
||||
children,
|
||||
}: HighlightedContainerProps) => {
|
||||
return (
|
||||
<Container style={highlightedContainerStyle}>
|
||||
<div style={divStyle}>{children}</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@ -1,34 +1,30 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Column } from '@react-email/components';
|
||||
import { Row } from '@react-email/row';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Text } from '@react-email/text';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
const rowStyle = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const highlightedStyle = {
|
||||
borderRadius: '4px',
|
||||
borderRadius: emailTheme.border.radius.sm,
|
||||
background: emailTheme.background.colors.highlight,
|
||||
padding: '4px 8px',
|
||||
margin: 0,
|
||||
fontSize: emailTheme.font.size.lg,
|
||||
fontWeight: emailTheme.font.weight.bold,
|
||||
color: emailTheme.font.colors.highlighted,
|
||||
};
|
||||
|
||||
const divStyle = {
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
type HighlightedTextProps = {
|
||||
value: ReactNode;
|
||||
centered?: boolean;
|
||||
};
|
||||
|
||||
export const HighlightedText = ({ value }: HighlightedTextProps) => {
|
||||
return (
|
||||
<Row style={rowStyle}>
|
||||
<Column>
|
||||
<Text style={highlightedStyle}>{value}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
<div style={divStyle}>
|
||||
<Text style={highlightedStyle}>{value}</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,9 +4,12 @@ import { Text } from '@react-email/text';
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
const mainTextStyle = {
|
||||
fontFamily: emailTheme.font.family,
|
||||
fontSize: emailTheme.font.size.md,
|
||||
fontWeight: emailTheme.font.weight.regular,
|
||||
color: emailTheme.font.colors.primary,
|
||||
margin: '0 0 12px 0',
|
||||
lineHeight: emailTheme.font.lineHeight,
|
||||
};
|
||||
|
||||
export const MainText = ({ children }: MainTextProps) => {
|
||||
|
||||
16
packages/twenty-emails/src/components/ShadowText.tsx
Normal file
16
packages/twenty-emails/src/components/ShadowText.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { PropsWithChildren as ShadowTextProps } from 'react';
|
||||
import { Text } from '@react-email/text';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
const shadowTextStyle = {
|
||||
fontSize: emailTheme.font.size.sm,
|
||||
fontWeight: emailTheme.font.weight.regular,
|
||||
color: emailTheme.font.colors.tertiary,
|
||||
margin: '0 0 12px 0',
|
||||
lineHeight: emailTheme.font.lineHeight,
|
||||
};
|
||||
|
||||
export const ShadowText = ({ children }: ShadowTextProps) => {
|
||||
return <Text style={shadowTextStyle}>{children}</Text>;
|
||||
};
|
||||
23
packages/twenty-emails/src/components/SubTitle.tsx
Normal file
23
packages/twenty-emails/src/components/SubTitle.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Heading } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
type SubTitleProps = {
|
||||
value: ReactNode;
|
||||
};
|
||||
|
||||
const subTitleStyle = {
|
||||
fontFamily: emailTheme.font.family,
|
||||
fontSize: emailTheme.font.size.lg,
|
||||
fontWeight: emailTheme.font.weight.bold,
|
||||
color: emailTheme.font.colors.highlighted,
|
||||
};
|
||||
|
||||
export const SubTitle = ({ value }: SubTitleProps) => {
|
||||
return (
|
||||
<Heading style={subTitleStyle} as="h3">
|
||||
{value}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,23 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Heading } from '@react-email/components';
|
||||
|
||||
import { emailTheme } from 'src/common-style';
|
||||
|
||||
type TitleProps = {
|
||||
value: ReactNode;
|
||||
};
|
||||
|
||||
export const Title = ({ value }: TitleProps) => {
|
||||
return <Heading as="h1">{value}</Heading>;
|
||||
const titleStyle = {
|
||||
fontFamily: emailTheme.font.family,
|
||||
fontSize: emailTheme.font.size.xl,
|
||||
fontWeight: emailTheme.font.weight.bold,
|
||||
color: emailTheme.font.colors.highlighted,
|
||||
};
|
||||
|
||||
export const Title = ({ value }: TitleProps) => {
|
||||
return (
|
||||
<Heading style={titleStyle} as="h1">
|
||||
{value}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
||||
47
packages/twenty-emails/src/components/WhatIsTwenty.tsx
Normal file
47
packages/twenty-emails/src/components/WhatIsTwenty.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { Column, Row } from '@react-email/components';
|
||||
|
||||
import { Link } from 'src/components/Link';
|
||||
import { MainText } from 'src/components/MainText';
|
||||
import { ShadowText } from 'src/components/ShadowText';
|
||||
import { SubTitle } from 'src/components/SubTitle';
|
||||
|
||||
export const WhatIsTwenty = () => {
|
||||
return (
|
||||
<>
|
||||
<SubTitle value="What is Twenty?" />
|
||||
<MainText>
|
||||
A software to help businesses manage their customer data and
|
||||
relationships efficiently.
|
||||
</MainText>
|
||||
<Row>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link href="https://twenty.com/" value="Website" />
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link href="https://github.com/twentyhq/twenty" value="Github" />
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link href="https://twenty.com/user-guide" value="User guide" />
|
||||
</ShadowText>
|
||||
</Column>
|
||||
<Column>
|
||||
<ShadowText>
|
||||
<Link href="https://docs.twenty.com/" value="Developers" />
|
||||
</ShadowText>
|
||||
</Column>
|
||||
</Row>
|
||||
<ShadowText>
|
||||
Twenty.com Public Benefit Corporation
|
||||
<br />
|
||||
2261 Market Street #5275
|
||||
<br />
|
||||
San Francisco, CA 94114
|
||||
</ShadowText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
44
packages/twenty-emails/src/emails/send-invite-link.email.tsx
Normal file
44
packages/twenty-emails/src/emails/send-invite-link.email.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Img } from '@react-email/components';
|
||||
|
||||
import { BaseEmail } from 'src/components/BaseEmail';
|
||||
import { CallToAction } from 'src/components/CallToAction';
|
||||
import { HighlightedContainer } from 'src/components/HighlightedContainer';
|
||||
import { HighlightedText } from 'src/components/HighlightedText';
|
||||
import { Link } from 'src/components/Link';
|
||||
import { MainText } from 'src/components/MainText';
|
||||
import { Title } from 'src/components/Title';
|
||||
import { WhatIsTwenty } from 'src/components/WhatIsTwenty';
|
||||
import { capitalize } from 'src/utils/capitalize';
|
||||
|
||||
type SendInviteLinkEmailProps = {
|
||||
link: string;
|
||||
workspace: { name: string | undefined; logo: string | undefined };
|
||||
sender: {
|
||||
email: string;
|
||||
firstName: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const SendInviteLinkEmail = ({
|
||||
link,
|
||||
workspace,
|
||||
sender,
|
||||
}: SendInviteLinkEmailProps) => {
|
||||
return (
|
||||
<BaseEmail width={333}>
|
||||
<Title value="Join your team on Twenty" />
|
||||
<MainText>
|
||||
{capitalize(sender.firstName)} (
|
||||
<Link href={sender.email} value={sender.email} />) has invited you to
|
||||
join a workspace called <b>{workspace.name}</b>
|
||||
<br />
|
||||
</MainText>
|
||||
<HighlightedContainer>
|
||||
{workspace.logo && <Img src={workspace.logo} width={40} height={40} />}
|
||||
{workspace.name && <HighlightedText value={workspace.name} />}
|
||||
<CallToAction href={link} value="Accept invite" />
|
||||
</HighlightedContainer>
|
||||
<WhatIsTwenty />
|
||||
</BaseEmail>
|
||||
);
|
||||
};
|
||||
@ -2,3 +2,4 @@ export * from './emails/clean-inactive-workspaces.email';
|
||||
export * from './emails/delete-inactive-workspaces.email';
|
||||
export * from './emails/password-reset-link.email';
|
||||
export * from './emails/password-update-notify.email';
|
||||
export * from './emails/send-invite-link.email';
|
||||
|
||||
7
packages/twenty-emails/src/utils/capitalize.ts
Normal file
7
packages/twenty-emails/src/utils/capitalize.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
export const capitalize = (stringToCapitalize: string) => {
|
||||
if (!isNonEmptyString(stringToCapitalize)) return '';
|
||||
|
||||
return stringToCapitalize[0].toUpperCase() + stringToCapitalize.slice(1);
|
||||
};
|
||||
Reference in New Issue
Block a user