mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +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 { UserProps } from './components';
|
||||
import { WorkspaceProps } from './components/workspace';
|
||||
|
||||
export const TEST_USER: UserProps = {
|
||||
email: 'test@test.com',
|
||||
};
|
||||
|
||||
export const TEST_WORKSPACE: WorkspaceProps = {
|
||||
name: 'Test Workspace',
|
||||
};
|
||||
@@ -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`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { render as rawRender } from '@react-email/render';
|
||||
|
||||
import {
|
||||
TeamBecomeAdmin,
|
||||
TeamBecomeCollaborator,
|
||||
TeamDeleteIn24Hours,
|
||||
TeamDeleteInOneMonth,
|
||||
TeamExpired,
|
||||
TeamExpireSoon,
|
||||
TeamWorkspaceDeleted,
|
||||
TeamWorkspaceUpgraded,
|
||||
} from './teams';
|
||||
import {
|
||||
ChangeEmail,
|
||||
ChangeEmailNotification,
|
||||
ChangePassword,
|
||||
SetPassword,
|
||||
SignIn,
|
||||
SignUp,
|
||||
VerifyChangeEmail,
|
||||
VerifyEmail,
|
||||
} from './users';
|
||||
import {
|
||||
Invitation,
|
||||
InvitationAccepted,
|
||||
LinkInvitationApproved,
|
||||
LinkInvitationReviewDeclined,
|
||||
LinkInvitationReviewRequest,
|
||||
MemberLeave,
|
||||
MemberRemoved,
|
||||
OwnershipReceived,
|
||||
OwnershipTransferred,
|
||||
} from './workspaces';
|
||||
|
||||
type EmailContent = {
|
||||
subject: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
function render(component: React.ReactElement) {
|
||||
return rawRender(component, {
|
||||
pretty: AFFiNE.node.test,
|
||||
});
|
||||
}
|
||||
|
||||
type Props<T> = T extends React.FC<infer P> ? P : never;
|
||||
export type EmailRenderer<Props> = (props: Props) => Promise<EmailContent>;
|
||||
|
||||
function make<T extends React.ComponentType<any>>(
|
||||
Component: T,
|
||||
subject: string | ((props: Props<T>) => string)
|
||||
): EmailRenderer<Props<T>> {
|
||||
return async props => {
|
||||
if (!props && AFFiNE.node.test) {
|
||||
// @ts-expect-error test only
|
||||
props = Component.PreviewProps;
|
||||
}
|
||||
return {
|
||||
subject: typeof subject === 'function' ? subject(props) : subject,
|
||||
html: await render(<Component {...props} />),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ================ User ================
|
||||
export const renderSignInMail = make(SignIn, 'Sign in to AFFiNE');
|
||||
export const renderSignUpMail = make(
|
||||
SignUp,
|
||||
'Your AFFiNE account is waiting for you!'
|
||||
);
|
||||
export const renderSetPasswordMail = make(
|
||||
SetPassword,
|
||||
'Set your AFFiNE password'
|
||||
);
|
||||
export const renderChangePasswordMail = make(
|
||||
ChangePassword,
|
||||
'Modify your AFFiNE password'
|
||||
);
|
||||
export const renderVerifyEmailMail = make(
|
||||
VerifyEmail,
|
||||
'Verify your email address'
|
||||
);
|
||||
export const renderChangeEmailMail = make(
|
||||
ChangeEmail,
|
||||
'Change your email address'
|
||||
);
|
||||
export const renderVerifyChangeEmailMail = make(
|
||||
VerifyChangeEmail,
|
||||
'Verify your new email address'
|
||||
);
|
||||
export const renderChangeEmailNotificationMail = make(
|
||||
ChangeEmailNotification,
|
||||
'Account email address changed'
|
||||
);
|
||||
|
||||
// ================ Workspace ================
|
||||
export const renderMemberInvitationMail = make(
|
||||
Invitation,
|
||||
props => `${props.user.email} invited you to join ${props.workspace.name}`
|
||||
);
|
||||
export const renderMemberAcceptedMail = make(
|
||||
InvitationAccepted,
|
||||
props => `${props.user.email} accepted your invitation`
|
||||
);
|
||||
export const renderMemberLeaveMail = make(
|
||||
MemberLeave,
|
||||
props => `${props.user.email} left ${props.workspace.name}`
|
||||
);
|
||||
export const renderLinkInvitationReviewRequestMail = make(
|
||||
LinkInvitationReviewRequest,
|
||||
props => `New request to join ${props.workspace.name}`
|
||||
);
|
||||
export const renderLinkInvitationApproveMail = make(
|
||||
LinkInvitationApproved,
|
||||
props => `Your request to join ${props.workspace.name} has been approved`
|
||||
);
|
||||
export const renderLinkInvitationDeclineMail = make(
|
||||
LinkInvitationReviewDeclined,
|
||||
props => `Your request to join ${props.workspace.name} was declined`
|
||||
);
|
||||
export const renderMemberRemovedMail = make(
|
||||
MemberRemoved,
|
||||
props => `You have been removed from ${props.workspace.name}`
|
||||
);
|
||||
export const renderOwnershipTransferredMail = make(
|
||||
OwnershipTransferred,
|
||||
props => `Your ownership of ${props.workspace.name} has been transferred`
|
||||
);
|
||||
export const renderOwnershipReceivedMail = make(
|
||||
OwnershipReceived,
|
||||
props => `You are now the owner of ${props.workspace.name}`
|
||||
);
|
||||
|
||||
// ================ Team ================
|
||||
export const renderTeamWorkspaceUpgradedMail = make(
|
||||
TeamWorkspaceUpgraded,
|
||||
props =>
|
||||
props.isOwner
|
||||
? 'Your workspace has been upgraded to team workspace! 🎉'
|
||||
: `${props.workspace.name} has been upgraded to team workspace! 🎉`
|
||||
);
|
||||
|
||||
export const renderTeamBecomeAdminMail = make(
|
||||
TeamBecomeAdmin,
|
||||
props => `You are now an admin of ${props.workspace.name}`
|
||||
);
|
||||
|
||||
export const renderTeamBecomeCollaboratorMail = make(
|
||||
TeamBecomeCollaborator,
|
||||
props => `Your role has been changed in ${props.workspace.name}`
|
||||
);
|
||||
export const renderTeamDeleteIn24HoursMail = make(
|
||||
TeamDeleteIn24Hours,
|
||||
props =>
|
||||
`[Action Required] Final warning: Your workspace ${props.workspace.name} will be deleted in 24 hours`
|
||||
);
|
||||
export const renderTeamDeleteIn1MonthMail = make(
|
||||
TeamDeleteInOneMonth,
|
||||
props =>
|
||||
`[Action Required] Important: Your workspace ${props.workspace.name} will be deleted soon`
|
||||
);
|
||||
|
||||
export const renderTeamWorkspaceDeletedMail = make(
|
||||
TeamWorkspaceDeleted,
|
||||
props => `Your workspace ${props.workspace.name} has been deleted`
|
||||
);
|
||||
|
||||
export const renderTeamWorkspaceExpireSoonMail = make(
|
||||
TeamExpireSoon,
|
||||
props =>
|
||||
`[Action Required] Your ${props.workspace.name} team workspace will expire soon`
|
||||
);
|
||||
|
||||
export const renderTeamWorkspaceExpiredMail = make(
|
||||
TeamExpired,
|
||||
props => `Your ${props.workspace.name} team workspace has expired`
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type TeamBecomeAdminProps = {
|
||||
workspace: WorkspaceProps;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function TeamBecomeAdmin(props: TeamBecomeAdminProps) {
|
||||
const { workspace, url } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>You've been promoted to admin.</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You have been promoted to admin of <Workspace {...workspace} />. As an
|
||||
admin, you can help the workspace owner manage members in this
|
||||
workspace.
|
||||
</P>
|
||||
<Button href={url}>Go to Workspace</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamBecomeAdmin.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
role: 'admin',
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type TeamBecomeCollaboratorProps = {
|
||||
workspace: WorkspaceProps;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function TeamBecomeCollaborator(
|
||||
props: TeamBecomeCollaboratorProps
|
||||
) {
|
||||
const { workspace, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Role update in workspace</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your role in <Workspace {...workspace} /> has been changed to{' '}
|
||||
collaborator. You can continue to collaborate in this workspace.
|
||||
</P>
|
||||
<Button href={url}>Go to Workspace</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamBecomeCollaborator.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
role: 'admin',
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Bold,
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Text,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export interface TeamDeleteInOneMonthProps {
|
||||
workspace: WorkspaceProps;
|
||||
expirationDate: Date;
|
||||
deletionDate: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function TeamDeleteInOneMonth(props: TeamDeleteInOneMonthProps) {
|
||||
const { workspace, expirationDate, deletionDate, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Take action to prevent data loss</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your <Workspace {...workspace} /> team workspace expired on{' '}
|
||||
<Bold>{expirationDate.toLocaleDateString()}</Bold>. All workspace data
|
||||
will be permanently deleted on{' '}
|
||||
<Bold>{deletionDate.toLocaleDateString()}</Bold> (180 days after
|
||||
expiration). To prevent data loss, please either:
|
||||
<li>
|
||||
<Text>Renew your subscription to restore team features</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>
|
||||
Export your workspace data from Workspace Settings > Export
|
||||
Workspace
|
||||
</Text>
|
||||
</li>
|
||||
</P>
|
||||
<Button href={url}>Go to Billing</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamDeleteInOneMonth.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
expirationDate: new Date('2025-01-01T00:00:00Z'),
|
||||
deletionDate: new Date('2025-01-31T00:00:00Z'),
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Bold,
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Text,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export interface TeamDeleteIn24HoursProps {
|
||||
workspace: WorkspaceProps;
|
||||
deletionDate: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function TeamDeleteIn24Hours(props: TeamDeleteIn24HoursProps) {
|
||||
const { workspace, deletionDate, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Urgent: Last chance to prevent data loss</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your <Workspace {...workspace} /> team workspace data will be
|
||||
permanently deleted in 24 hours on{' '}
|
||||
<Bold>{deletionDate.toLocaleDateString()}</Bold>. To prevent data
|
||||
loss, please take immediate action:
|
||||
<li>
|
||||
<Text>Renew your subscription to restore team features</Text>
|
||||
</li>
|
||||
<li>
|
||||
<Text>
|
||||
Export your workspace data from Workspace Settings > Export
|
||||
Workspace
|
||||
</Text>
|
||||
</li>
|
||||
</P>
|
||||
<Button href={url}>Go to Billing</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamDeleteIn24Hours.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
deletionDate: new Date('2025-01-31T00:00:00Z'),
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export interface TeamWorkspaceDeletedProps {
|
||||
workspace: WorkspaceProps;
|
||||
}
|
||||
|
||||
export default function TeamWorkspaceDeleted(props: TeamWorkspaceDeletedProps) {
|
||||
const { workspace } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Workspace data deleted</Title>
|
||||
<Content>
|
||||
<P>
|
||||
All data in <Workspace {...workspace} /> has been permanently deleted
|
||||
as the workspace remained expired for 180 days. This action cannot be
|
||||
undone.
|
||||
</P>
|
||||
<P>
|
||||
Thank you for your support of AFFiNE. We hope to see you again in the
|
||||
future.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamWorkspaceDeleted.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Bold,
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export interface TeamExpireSoonProps {
|
||||
workspace: WorkspaceProps;
|
||||
expirationDate: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function TeamExpireSoon(props: TeamExpireSoonProps) {
|
||||
const { workspace, expirationDate, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Team workspace will expire soon</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your <Workspace {...workspace} /> team workspace will expire on{' '}
|
||||
<Bold>{expirationDate.toLocaleDateString()}</Bold>. After expiration,
|
||||
you won't be able to sync or collaborate with team members.
|
||||
Please renew your subscription to continue using all team features.
|
||||
</P>
|
||||
<Button href={url}>Go to Billing</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamExpireSoon.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
expirationDate: new Date('2025-01-01T00:00:00Z'),
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Bold,
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export interface TeamExpiredProps {
|
||||
workspace: WorkspaceProps;
|
||||
expirationDate: Date;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export default function TeamExpired(props: TeamExpiredProps) {
|
||||
const { workspace, expirationDate, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Team workspace expired</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your <Workspace {...workspace} /> team workspace expired on{' '}
|
||||
<Bold>{expirationDate.toLocaleDateString()}</Bold>. Your workspace
|
||||
can't sync or collaborate with team members. Please renew your
|
||||
subscription to restore all team features.
|
||||
</P>
|
||||
<Button href={url}>Go to Billing</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamExpired.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
expirationDate: new Date('2025-01-01T00:00:00Z'),
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
export {
|
||||
default as TeamBecomeAdmin,
|
||||
type TeamBecomeAdminProps,
|
||||
} from './become-admin';
|
||||
export {
|
||||
default as TeamBecomeCollaborator,
|
||||
type TeamBecomeCollaboratorProps,
|
||||
} from './become-collaborator';
|
||||
export {
|
||||
default as TeamDeleteInOneMonth,
|
||||
type TeamDeleteInOneMonthProps,
|
||||
} from './delete-in-1m';
|
||||
export {
|
||||
default as TeamDeleteIn24Hours,
|
||||
type TeamDeleteIn24HoursProps,
|
||||
} from './delete-in-24h';
|
||||
export {
|
||||
default as TeamWorkspaceDeleted,
|
||||
type TeamWorkspaceDeletedProps,
|
||||
} from './deleted';
|
||||
export {
|
||||
default as TeamExpireSoon,
|
||||
type TeamExpireSoonProps,
|
||||
} from './expire-soon';
|
||||
export { default as TeamExpired, type TeamExpiredProps } from './expired';
|
||||
export {
|
||||
default as TeamWorkspaceUpgraded,
|
||||
type TeamWorkspaceUpgradedProps,
|
||||
} from './workspace-upgraded';
|
||||
@@ -0,0 +1,57 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type TeamWorkspaceUpgradedProps = {
|
||||
workspace: WorkspaceProps;
|
||||
isOwner: boolean;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function TeamWorkspaceUpgraded(
|
||||
props: TeamWorkspaceUpgradedProps
|
||||
) {
|
||||
const { workspace, isOwner, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Welcome to the team workspace!</Title>
|
||||
<Content>
|
||||
<P>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Workspace {...workspace} /> has been upgraded to team workspace
|
||||
with the following benefits:
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Great news! <Workspace {...workspace} /> has been upgraded to team
|
||||
workspace by the workspace owner.
|
||||
<br />
|
||||
You now have access to the following enhanced features:
|
||||
</>
|
||||
)}
|
||||
<br /> ✓ 100 GB initial storage + 20 GB per seat
|
||||
<br /> ✓ 500 MB of maximum file size
|
||||
<br /> ✓ Unlimited team members (10+ seats)
|
||||
<br /> ✓ Multiple admin roles
|
||||
<br /> ✓ Priority customer support
|
||||
</P>
|
||||
<Button href={url}>Open Workspace</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
TeamWorkspaceUpgraded.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
isOwner: true,
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Content, Name, P, Template, Title } from '../components';
|
||||
|
||||
export type ChangeEmailNotificationProps = {
|
||||
to: string;
|
||||
};
|
||||
|
||||
export default function ChangeEmailNotification(
|
||||
props: ChangeEmailNotificationProps
|
||||
) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Verify your current email for AFFiNE</Title>
|
||||
<Content>
|
||||
<P>
|
||||
As per your request, we have changed your email. Please make sure
|
||||
you're using <Name>{props.to}</Name> to log in the next time.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
ChangeEmailNotification.PreviewProps = {
|
||||
to: 'test@affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type VerifyChangeEmailProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function VerifyChangeEmail(props: VerifyChangeEmailProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Verify your new email address</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You recently requested to change the email address associated with
|
||||
your AFFiNE account. To complete this process, please click on the
|
||||
verification link below. This magic link will expire in 30 minutes.
|
||||
</P>
|
||||
<Button href={props.url}>Verify your new email address</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
VerifyChangeEmail.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Bold, Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type ChangeEmailProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function ChangeEmail(props: ChangeEmailProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Verify your current email for AFFiNE</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You recently requested to change the email address associated with
|
||||
your AFFiNE account.
|
||||
<br />
|
||||
To complete this process, please click on the verification link below.
|
||||
</P>
|
||||
<P>
|
||||
This magic link will expire in <Bold>30 minutes</Bold>.
|
||||
</P>
|
||||
<Button href={props.url}>Verify and set up a new email address</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
ChangeEmail.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Bold, Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type VerifyEmailProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function VerifyEmail(props: VerifyEmailProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Verify your email address</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You recently requested to verify the email address associated with
|
||||
your AFFiNE account.
|
||||
<br />
|
||||
To complete this process, please click on the verification link below.
|
||||
</P>
|
||||
<P>
|
||||
This magic link will expire in <Bold>30 minutes</Bold>.
|
||||
</P>
|
||||
<Button href={props.url}>Verify your email address</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
VerifyEmail.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export { default as ChangeEmail, type ChangeEmailProps } from './email-change';
|
||||
export {
|
||||
default as ChangeEmailNotification,
|
||||
type ChangeEmailNotificationProps,
|
||||
} from './email-change-notification';
|
||||
export {
|
||||
default as VerifyChangeEmail,
|
||||
type VerifyChangeEmailProps,
|
||||
} from './email-change-verify';
|
||||
export { default as VerifyEmail, type VerifyEmailProps } from './email-verify';
|
||||
export {
|
||||
default as ChangePassword,
|
||||
type ChangePasswordProps,
|
||||
} from './password-change';
|
||||
export { default as SetPassword, type SetPasswordProps } from './password-set';
|
||||
export { default as SignIn, type SignInProps } from './sign-in';
|
||||
export { default as SignUp, type SignUpProps } from './sign-up';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Bold, Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type ChangePasswordProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function ChangePassword(props: ChangePasswordProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Modify your AFFiNE password</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Click the button below to reset your password. The magic link will
|
||||
expire in <Bold>30 minutes</Bold>.
|
||||
</P>
|
||||
<Button href={props.url}>Set new password</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
ChangePassword.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Bold, Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type SetPasswordProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function SetPassword(props: SetPasswordProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Set your AFFiNE password</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Click the button below to set your password. The magic link will
|
||||
expire in <Bold>30 minutes</Bold>.
|
||||
</P>
|
||||
<Button href={props.url}>Sign in to AFFiNE</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
SetPassword.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type SignInProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function SignUp(props: SignInProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Sign in to AFFiNE</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Click the button below to securely sign in. The magic link will expire
|
||||
in 30 minutes.
|
||||
</P>
|
||||
<Button href={props.url}>Sign in to AFFiNE</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Bold, Button, Content, P, Template, Title } from '../components';
|
||||
|
||||
export type SignUpProps = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function SignUp(props: SignUpProps) {
|
||||
return (
|
||||
<Template>
|
||||
<Title>Create AFFiNE Account</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Click the button below to complete your account creation and sign in.
|
||||
This magic link will expire in <Bold>30 minutes</Bold>.
|
||||
</P>
|
||||
<Button href={props.url}>Create account and sign in</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
SignUp.PreviewProps = {
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export { default as Invitation, type InvitationProps } from './invitation';
|
||||
export {
|
||||
default as InvitationAccepted,
|
||||
type InvitationAcceptedProps,
|
||||
} from './invitation-accepted';
|
||||
export { default as MemberLeave, type MemberLeaveProps } from './member-leave';
|
||||
export {
|
||||
default as MemberRemoved,
|
||||
type MemberRemovedProps,
|
||||
} from './member-removed';
|
||||
export {
|
||||
default as OwnershipReceived,
|
||||
type OwnershipReceivedProps,
|
||||
} from './ownership-received';
|
||||
export {
|
||||
default as OwnershipTransferred,
|
||||
type OwnershipTransferredProps,
|
||||
} from './ownership-transferred';
|
||||
export {
|
||||
default as LinkInvitationApproved,
|
||||
type LinkInvitationApprovedProps,
|
||||
} from './review-approved';
|
||||
export {
|
||||
default as LinkInvitationReviewDeclined,
|
||||
type LinkInvitationReviewDeclinedProps,
|
||||
} from './review-declined';
|
||||
export {
|
||||
default as LinkInvitationReviewRequest,
|
||||
type LinkInvitationReviewRequestProps,
|
||||
} from './review-request';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { TEST_USER, TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
User,
|
||||
type UserProps,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type InvitationAcceptedProps = {
|
||||
user: UserProps;
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function InvitationAccepted(props: InvitationAcceptedProps) {
|
||||
const { user, workspace } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>{user.email} accepted your invitation</Title>
|
||||
<Content>
|
||||
<P>
|
||||
<User {...user} /> has joined <Workspace {...workspace} />
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
InvitationAccepted.PreviewProps = {
|
||||
user: TEST_USER,
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TEST_USER, TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
User,
|
||||
type UserProps,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type InvitationProps = {
|
||||
user: UserProps;
|
||||
workspace: WorkspaceProps;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function Invitation(props: InvitationProps) {
|
||||
const { user, workspace, url } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>You are invited!</Title>
|
||||
<Content>
|
||||
<P>
|
||||
<User {...user} /> invited you to join <Workspace {...workspace} />
|
||||
</P>
|
||||
<P>Click button to join this workspace</P>
|
||||
<Button href={url}>Accept & Join</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
Invitation.PreviewProps = {
|
||||
user: TEST_USER,
|
||||
workspace: TEST_WORKSPACE,
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TEST_USER, TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
Name,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
type UserProps,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type MemberLeaveProps = {
|
||||
user: UserProps;
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function MemberLeave(props: MemberLeaveProps) {
|
||||
const { user, workspace } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>
|
||||
Member left <Workspace {...workspace} size={24} />
|
||||
</Title>
|
||||
<Content>
|
||||
<P>
|
||||
<Name>{user.email}</Name> has left workspace{' '}
|
||||
<Workspace {...workspace} />
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
MemberLeave.PreviewProps = {
|
||||
user: TEST_USER,
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type MemberRemovedProps = {
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function MemberRemoved(props: MemberRemovedProps) {
|
||||
const { workspace } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>Workspace access removed</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You have been removed from <Workspace {...workspace} />. You no longer
|
||||
have access to this workspace.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
MemberRemoved.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type OwnershipReceivedProps = {
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function OwnershipReceived(props: OwnershipReceivedProps) {
|
||||
const { workspace } = props;
|
||||
|
||||
return (
|
||||
<Template>
|
||||
<Title>Welcome, new workspace owner!</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You have been assigned as the owner of
|
||||
<Workspace {...workspace} />. As a workspace owner, you have full
|
||||
control over this workspace.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
OwnershipReceived.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type OwnershipTransferredProps = {
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function OwnershipTransferred(props: OwnershipTransferredProps) {
|
||||
const { workspace } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>Ownership transferred</Title>
|
||||
<Content>
|
||||
<P>
|
||||
You have transferred ownership of <Workspace {...workspace} />. You
|
||||
are now a collaborator in this workspace.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
OwnershipTransferred.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type LinkInvitationApprovedProps = {
|
||||
workspace: WorkspaceProps;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function LinkInvitationApproved(
|
||||
props: LinkInvitationApprovedProps
|
||||
) {
|
||||
const { workspace, url } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>Welcome to the workspace!</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your request to join <Workspace {...workspace} /> has been accepted.
|
||||
You can now access the team workspace and collaborate with other
|
||||
members.
|
||||
</P>
|
||||
</Content>
|
||||
<Button href={url}>Open Workspace</Button>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
LinkInvitationApproved.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type LinkInvitationReviewDeclinedProps = {
|
||||
workspace: WorkspaceProps;
|
||||
};
|
||||
|
||||
export default function LinkInvitationReviewDeclined(
|
||||
props: LinkInvitationReviewDeclinedProps
|
||||
) {
|
||||
const { workspace } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>Request declined</Title>
|
||||
<Content>
|
||||
<P>
|
||||
Your request to join <Workspace {...workspace} /> has been declined by
|
||||
the workspace admin.
|
||||
</P>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
LinkInvitationReviewDeclined.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TEST_USER, TEST_WORKSPACE } from '../common';
|
||||
import {
|
||||
Button,
|
||||
Content,
|
||||
P,
|
||||
Template,
|
||||
Title,
|
||||
User,
|
||||
type UserProps,
|
||||
Workspace,
|
||||
type WorkspaceProps,
|
||||
} from '../components';
|
||||
|
||||
export type LinkInvitationReviewRequestProps = {
|
||||
workspace: WorkspaceProps;
|
||||
user: UserProps;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default function LinkInvitationReviewRequest(
|
||||
props: LinkInvitationReviewRequestProps
|
||||
) {
|
||||
const { workspace, user, url } = props;
|
||||
return (
|
||||
<Template>
|
||||
<Title>
|
||||
Request to join <Workspace {...workspace} size={24} />
|
||||
</Title>
|
||||
<Content>
|
||||
<P>
|
||||
<User {...user} /> has requested to join <Workspace {...workspace} />.
|
||||
<br />
|
||||
As a workspace owner/admin, you can approve or decline this request.
|
||||
</P>
|
||||
<Button href={url}>Review request</Button>
|
||||
</Content>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
LinkInvitationReviewRequest.PreviewProps = {
|
||||
workspace: TEST_WORKSPACE,
|
||||
user: TEST_USER,
|
||||
url: 'https://app.affine.pro',
|
||||
};
|
||||
Reference in New Issue
Block a user