mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +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:
@@ -0,0 +1,10 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
export const BasicTextStyle: CSSProperties = {
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
lineHeight: '24px',
|
||||
fontFamily: 'Inter, Arial, Helvetica, sans-serif',
|
||||
margin: '24px 0 0',
|
||||
color: '#141414',
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Container, Img, Link, Row, Section } from '@react-email/components';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
import { BasicTextStyle } from './common';
|
||||
|
||||
const TextStyles: CSSProperties = {
|
||||
...BasicTextStyle,
|
||||
color: '#8e8d91',
|
||||
margin: 1,
|
||||
marginTop: '8px',
|
||||
};
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<Container
|
||||
style={{
|
||||
backgroundColor: '#fafafa',
|
||||
maxWidth: '450px',
|
||||
margin: '0 auto 32px auto',
|
||||
borderRadius: '0 0 16px 16px',
|
||||
boxShadow: '0px 0px 20px 0px rgba(66, 65, 73, 0.04)',
|
||||
padding: '24px',
|
||||
}}
|
||||
>
|
||||
<Section align="center" width="auto" style={{ margin: '1px auto' }}>
|
||||
<Row>
|
||||
{[
|
||||
'Github',
|
||||
'Twitter',
|
||||
'Discord',
|
||||
'Youtube',
|
||||
'Telegram',
|
||||
'Reddit',
|
||||
].map(platform => (
|
||||
<td key={platform} style={{ padding: '0 10px' }}>
|
||||
<Link href={`https://affine.pro/${platform.toLowerCase()}`}>
|
||||
<Img
|
||||
src={`https://cdn.affine.pro/mail/2023-8-9/${platform}.png`}
|
||||
alt={`affine ${platform.toLowerCase()} link`}
|
||||
height="16px"
|
||||
/>
|
||||
</Link>
|
||||
</td>
|
||||
))}
|
||||
</Row>
|
||||
</Section>
|
||||
<Section align="center" width="auto">
|
||||
<Row style={TextStyles}>
|
||||
<td>One hyper-fused platform for wildly creative minds</td>
|
||||
</Row>
|
||||
</Section>
|
||||
<Section align="center" width="auto">
|
||||
<Row style={TextStyles}>
|
||||
<td>Copyright</td>
|
||||
<td>
|
||||
<Img
|
||||
src="https://cdn.affine.pro/mail/2023-8-9/copyright.png"
|
||||
alt="copyright"
|
||||
height="14px"
|
||||
style={{ verticalAlign: 'middle', margin: '0 4px' }}
|
||||
/>
|
||||
</td>
|
||||
<td>2023-{new Date().getUTCFullYear()} ToEverything</td>
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './template';
|
||||
export * from './user';
|
||||
export * from './workspace';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Name } from './template';
|
||||
|
||||
export interface UserProps {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export const User = (props: UserProps) => {
|
||||
return <Name>{props.email}</Name>;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { AvatarWithName } from './template';
|
||||
|
||||
export interface WorkspaceProps {
|
||||
name: string;
|
||||
avatar?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const Workspace = (props: WorkspaceProps) => {
|
||||
return (
|
||||
<AvatarWithName
|
||||
name={props.name}
|
||||
img={props.avatar}
|
||||
width={`${props.size}px`}
|
||||
height={`${props.size}px`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user