mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 05:47:09 +08:00
feat(server): new email template (#9528)
use `yarn af server dev:mail` to preview all mail template fix CLOUD-93
This commit is contained in:
207
packages/backend/server/src/mails/components/template.tsx
Normal file
207
packages/backend/server/src/mails/components/template.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
Body,
|
||||
Button as EmailButton,
|
||||
Container,
|
||||
Head,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Row,
|
||||
Section,
|
||||
Text as EmailText,
|
||||
} from '@react-email/components';
|
||||
import type { CSSProperties, PropsWithChildren } from 'react';
|
||||
|
||||
import { Footer } from './footer';
|
||||
|
||||
const BasicTextStyle: CSSProperties = {
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
lineHeight: '24px',
|
||||
fontFamily: 'Inter, Arial, Helvetica, sans-serif',
|
||||
margin: '24px 0 0',
|
||||
color: '#141414',
|
||||
};
|
||||
|
||||
export function Title(props: PropsWithChildren) {
|
||||
return (
|
||||
<EmailText
|
||||
style={{
|
||||
...BasicTextStyle,
|
||||
fontSize: '20px',
|
||||
fontWeight: '600',
|
||||
lineHeight: '28px',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</EmailText>
|
||||
);
|
||||
}
|
||||
|
||||
export function P(props: PropsWithChildren) {
|
||||
return <EmailText style={BasicTextStyle}>{props.children}</EmailText>;
|
||||
}
|
||||
|
||||
export function Text(props: PropsWithChildren) {
|
||||
return <span style={BasicTextStyle}>{props.children}</span>;
|
||||
}
|
||||
|
||||
export function Bold(props: PropsWithChildren) {
|
||||
return <span style={{ fontWeight: 600 }}>{props.children}</span>;
|
||||
}
|
||||
|
||||
export const Avatar = (props: {
|
||||
img: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}) => {
|
||||
return (
|
||||
<img
|
||||
src={props.img}
|
||||
alt="avatar"
|
||||
style={{
|
||||
width: props.width || '20px',
|
||||
height: props.height || '20px',
|
||||
borderRadius: '12px',
|
||||
objectFit: 'cover',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Name = (props: PropsWithChildren) => {
|
||||
return <Bold>{props.children}</Bold>;
|
||||
};
|
||||
|
||||
export const AvatarWithName = (props: {
|
||||
img?: string;
|
||||
name: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{props.img && (
|
||||
<Avatar img={props.img} width={props.width} height={props.height} />
|
||||
)}
|
||||
<Name>{props.name}</Name>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function Content(props: PropsWithChildren) {
|
||||
return typeof props.children === 'string' ? (
|
||||
<EmailText>{props.children}</EmailText>
|
||||
) : (
|
||||
props.children
|
||||
);
|
||||
}
|
||||
|
||||
export function Button(
|
||||
props: PropsWithChildren<{ type?: 'primary' | 'secondary'; href: string }>
|
||||
) {
|
||||
const style = {
|
||||
...BasicTextStyle,
|
||||
backgroundColor: props.type === 'secondary' ? '#FFFFFF' : '#1E96EB',
|
||||
color: props.type === 'secondary' ? '#141414' : '#FFFFFF',
|
||||
textDecoration: 'none',
|
||||
fontWeight: '600',
|
||||
padding: '8px 18px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(0,0,0,.1)',
|
||||
marginRight: '4px',
|
||||
};
|
||||
|
||||
return (
|
||||
<EmailButton style={style} href={props.href}>
|
||||
{props.children}
|
||||
</EmailButton>
|
||||
);
|
||||
}
|
||||
|
||||
function fetchTitle(
|
||||
children: React.ReactElement<PropsWithChildren>[]
|
||||
): React.ReactElement {
|
||||
const title = children.find(child => child.type === Title);
|
||||
|
||||
if (!title || !title.props.children) {
|
||||
throw new Error('<Title /> is required for an email.');
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
function fetchContent(
|
||||
children: React.ReactElement<PropsWithChildren>[]
|
||||
): React.ReactElement | React.ReactElement[] {
|
||||
const content = children.find(child => child.type === Content);
|
||||
|
||||
if (!content || !content.props.children) {
|
||||
throw new Error('<Content /> is required for an email.');
|
||||
}
|
||||
|
||||
if (Array.isArray(content.props.children)) {
|
||||
return content.props.children.map((child, i) => {
|
||||
/* oxlint-disable-next-line eslint-plugin-react/no-array-index-key */
|
||||
return <Row key={i}>{child}</Row>;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function assertChildrenIsArray(
|
||||
children: React.ReactNode
|
||||
): asserts children is React.ReactElement<PropsWithChildren>[] {
|
||||
if (!Array.isArray(children) || !children.every(child => 'type' in child)) {
|
||||
throw new Error(
|
||||
'Children of `Template` element must be an array of [<Title />, <Content />, ...]'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function Template(props: PropsWithChildren) {
|
||||
assertChildrenIsArray(props.children);
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Section>{fetchTitle(props.children)}</Section>
|
||||
<Section>{fetchContent(props.children)}</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
if (typeof AFFiNE !== 'undefined' && AFFiNE.node.test) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={{ backgroundColor: '#f6f7fb', overflow: 'hidden' }}>
|
||||
<Container
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
maxWidth: '450px',
|
||||
margin: '32px auto 0',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
boxShadow: '0px 0px 20px 0px rgba(66, 65, 73, 0.04)',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<Section>
|
||||
<Link href="https://affine.pro">
|
||||
<Img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/affine-logo.png"
|
||||
alt="AFFiNE logo"
|
||||
height="32px"
|
||||
/>
|
||||
</Link>
|
||||
</Section>
|
||||
{content}
|
||||
</Container>
|
||||
<Footer />
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user