feat: move rendering to runtime

This commit is contained in:
DarkPhoenix2704
2025-03-07 05:00:34 +00:00
parent 4824c2a19c
commit b6ee13468f
52 changed files with 460 additions and 832 deletions

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,73 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Img,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import { baseUrl } from '../utils/constant';
// Corresponding ejs template
interface Props {
workspaceTitle: string;
baseTitle: string;
role: string;
name: string;
email: string;
link: string;
}
export const BaseRoleUpdateEE = () => (
<Html>
<RootWrapper>
<Head />
<Preview>Your base role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your base role has been updated
</Heading>
<Section className="py-6 text-center">
<span className="font-bold text-gray-900 text-base">
{'<%= workspaceTitle %>'}
</span>
<span className="px-2 text-gray-700">
/
</span>
<span className="font-bold text-gray-900 text-base">
{'<%= baseTitle %>'}
</span>
</Section>
<Section className="pb-6 text-center">
<Img
src={`${baseUrl}/badges/<%= role %>.png`}
alt="<%= role %>" className="h-7 mx-auto" />
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in
<span className="font-bold text-gray-800">
{' <%= baseTitle %> '}
</span>
has been updated to <span className="font-bold text-gray-800">{'<%= role %> '}</span>
by <span className="font-bold text-gray-800">{' <%= name %>'}</span> ( {'<%= email %>'})
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to Base
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default BaseRoleUpdateEE;

View File

@@ -1,64 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Img,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import { baseUrl } from '../utils/constant';
// Corresponding ejs template
interface Props {
baseTitle: string;
role: string;
name: string;
email: string;
link: string;
}
export const BaseRoleUpdate = () => (
<Html>
<RootWrapper>
<Head />
<Preview>Your base role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your base role has been updated
</Heading>
<Section className="py-6 text-center font-bold text-gray-900 text-base">
{'<%= baseTitle %>'}
</Section>
<Section className="pb-6 text-center">
<Img
src={`${baseUrl}/badges/<%= role %>.png`}
alt="<%= role %>" className="h-7 mx-auto" />
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in
<span className="font-bold text-gray-800">
{' <%= baseTitle %> '}
</span>
has been updated to <span className="font-bold text-gray-800">{'<%= role %> '}</span>
by <span className="font-bold text-gray-800">{' <%= name %>'}</span> ( {'<%= email %>'})
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to Base
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default BaseRoleUpdate;

View File

@@ -1,63 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
// Corresponding ejs template
interface Props {
name: string;
email: string;
link: string;
workspaceTitle: string;
baseTitle: string;
}
export const MentionCommentEE = () => (
<Html>
<RootWrapper>
<Head />
<Preview>You have been mentioned</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
You have been mentioned
</Heading>
<Section className="py-6 text-center">
<span className="font-bold text-gray-900 text-base">
{'<%= workspaceTitle %>'}
</span>
<span className="px-2 text-gray-700">
/
</span>
<span className="font-bold text-gray-900 text-base">
{'<%= baseTitle %>'}
</span>
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
{'<%= name %>'} ({'<%= email %>'}) has mentioned you in
<span className="font-semibold text-gray-800">
{' <%= baseTitle %>'}
</span>
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
View Comment
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default MentionCommentEE;

View File

@@ -1,61 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
// Corresponding ejs template
interface Props {
name: string;
email: string;
tableTitle: string;
link: string;
workspaceTitle: string;
baseTitle: string;
}
export const MentionRowEE = () => (
<Html>
<RootWrapper>
<Head />
<Preview>You have been mentioned</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
You have been mentioned
</Heading>
<Section className="py-6 text-center">
<span className="font-bold text-gray-900 text-base">
{'<%= workspaceTitle %>'}
</span>
<span className="px-2 text-gray-700">
/
</span>
<span className="font-bold text-gray-900 text-base">
{'<%= baseTitle %>'}
</span>
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
{'<%= name %>'} ({'<%= email %>'}) has mentioned you in a record in the <span className="font-semibold text-gray-800"> {'<%= tableTitle %>'}
</span> table.
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
View Record
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default MentionRowEE;

View File

@@ -1,51 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
// Corresponding ejs template
interface Props {
name: string;
email: string;
link: string;
}
export const OrganizationInvite = () => (
<Html>
<RootWrapper>
<Head />
<Preview>Youve been invited to NocoDB</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Youve been invited to NocoDB
</Heading>
<Section className="py-6 mx-auto font-bold mx-auto text-center text-gray-900 text-base">
{'<%= baseTitle %>'}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
<span className="font-bold text-gray-800">{'<%= name %>'}</span> ( {'<%= email %>'}) has invited you to
collaborate on NocoDB.
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to NocoDB
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default OrganizationInvite;

View File

@@ -1,56 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Img,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import { baseUrl } from '../utils/constant';
// Corresponding ejs template
interface Props {
role: string;
name: string;
email: string;
link: string;
}
export const OrganizationRoleUpdate = () => (
<Html>
<RootWrapper>
<Head />
<Preview>Your organization role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your organization role has been updated
</Heading>
<Section className="py-6 text-center">
<Img
src={`${baseUrl}/badges/<%= role %>.png`}
alt="<%= role %>" className="h-7 mx-auto" />
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in NocoDB has been updated to <span className="font-bold text-gray-800">{'<%= role %> '}</span>
by <span className="font-bold text-gray-800">{' <%= name %>'}</span> ( {'<%= email %>'})
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to NocoDB
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default OrganizationRoleUpdate;

View File

@@ -1,51 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
// Corresponding ejs template
interface Props {
workspaceTitle: string;
name: string;
email: string;
link: string;
}
export const WorkspaceInviteEe = () => (
<Html>
<RootWrapper>
<Head />
<Preview>You have been invited</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Youve been invited
</Heading>
<Section className="py-6 mx-auto font-bold text-center text-gray-900 text-base">
{'<%= workspaceTitle %>'}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
<span className="font-bold text-gray-800">{'<%= name %>'}</span> ({'<%= email %>'}) has invited you to
join <span className="font-bold text-gray-800">{'<%= workspaceTitle %>'}</span>. Click on Accept invite to join.
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Accept invite
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default WorkspaceInviteEe;

View File

@@ -1,66 +0,0 @@
import {
Body,
Head,
Heading,
Html,
Preview,
Img,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import { baseUrl } from '../utils/constant';
// Corresponding ejs template
interface Props {
workspaceTitle: string;
role: string;
name: string;
email: string;
link: string;
}
export const WorkspaceRoleUpdateEe = () => (
<Html>
<RootWrapper>
<Head />
<Preview>Your workspace role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your workspace role has been updated
</Heading>
<Section className="py-6 text-center">
<span className="font-bold text-gray-900 mx-auto text-base">
{'<%= workspaceTitle %>'}
</span>
</Section>
<Section className="pb-6 text-center">
<Img
src={`${baseUrl}/badges/<%= role %>.png`}
alt="<%= role %>" className="h-7 mx-auto" />
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in
<span className="font-bold text-gray-800">
{' <%= workspaceTitle %> '}
</span>
has been updated to <span className="font-bold text-gray-800">{'<%= role %> '}</span>
by <span className="font-bold text-gray-800">{'<%= name %>'}</span> ({'<%= email %>'})
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to Workspace
</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
export default WorkspaceRoleUpdateEe;

View File

@@ -1,44 +0,0 @@
import { existsSync, rmSync, mkdirSync, readdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { render } from '@react-email/render';
import decode from 'html-entities-decoder';
const outputDir = './dist';
if (existsSync(outputDir)) {
rmSync(outputDir, { recursive: true });
}
mkdirSync(outputDir, { recursive: true });
const exportString = 'export default `';
async function renderEmailTemplates() {
const emailsDir = './emails';
const files = readdirSync(emailsDir);
for (const file of files) {
if (file.endsWith('.tsx')) {
try {
const templatePath = join(process.cwd(), emailsDir, file);
const EmailTemplate = (await import(templatePath)).default;
const html = await render(EmailTemplate());
const decodeHtml= decode(html);
const outputFilename = file.replace('.tsx', '.ts');
const outputPath = join(outputDir, outputFilename);
writeFileSync(outputPath, exportString + decodeHtml + "`");
console.log(`Rendered ${file} to ${outputPath}`);
} catch (error) {
console.error(`Error rendering ${file}:`, error);
}
}
}
}
renderEmailTemplates()
.then(() => console.log('Email rendering complete!'))
.catch(err => console.error('Error in email rendering process:', err));

View File

@@ -1,27 +0,0 @@
# React Email Starter
A live preview right in your browser so you don't need to keep sending real emails during development.
## Getting Started
First, install the dependencies:
```sh
npm install
# or
yarn
```
Then, run the development server:
```sh
npm run dev
# or
yarn dev
```
Open [localhost:3000](http://localhost:3000) with your browser to see the result.
## License
MIT License

View File

@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react"
}
}

View File

@@ -1 +0,0 @@
export const baseUrl = 'https://cdn.nocodb.com/emails/v2'

View File

@@ -16,6 +16,8 @@ if (!NC_REFRESH_TOKEN_EXP_IN_DAYS || NC_REFRESH_TOKEN_EXP_IN_DAYS <= 0) {
throw new Error('NC_REFRESH_TOKEN_EXP_IN_DAYS must be a positive number');
}
export const NC_EMAIL_ASSETS_BASE_URL = 'https://cdn.nocodb.com/emails/v2';
export const S3_PATCH_KEYS = [
'uploads',
'thumbnails',

View File

@@ -1,22 +1,21 @@
import { Injectable } from '@nestjs/common';
import * as ejs from 'ejs';
import { RoleLabels } from 'nocodb-sdk';
import { render } from '@react-email/render';
import type { NcRequest } from 'nocodb-sdk';
import type { MailParams } from '~/interface/Mail';
import type { ComponentProps } from 'react';
import * as MailTemplates from '~/services/mail/templates';
import { MailEvent } from '~/interface/Mail';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import {
BaseInvite,
BaseRoleUpdate,
OrganizationInvite,
OrganizationRoleUpdate,
PasswordReset,
VerifyEmail,
Welcome,
} from '~/services/mail/templates';
import Noco from '~/Noco';
import config from '~/app.config';
type TemplateComponent<K extends keyof typeof MailTemplates> =
(typeof MailTemplates)[K];
type TemplateProps<K extends keyof typeof MailTemplates> = ComponentProps<
TemplateComponent<K>
>;
@Injectable()
export class MailService {
async getAdapter() {
@@ -28,6 +27,15 @@ export class MailService {
}
}
async renderMail<K extends keyof typeof MailTemplates>(
template: K,
props: TemplateProps<K>,
) {
const Component = MailTemplates[template];
// TODO: Fix Type here
return await render(Component(props as TemplateProps<any>));
}
buildUrl(
req: NcRequest,
params?: {
@@ -101,7 +109,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `You have been invited to ${base.title}`,
html: ejs.render(BaseInvite, {
html: await this.renderMail('BaseInvite', {
baseTitle: base.title,
name:
invitee.display_name ??
@@ -122,7 +130,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `Role updated in ${base.title}`,
html: ejs.render(BaseRoleUpdate, {
html: await this.renderMail('BaseRoleUpdate', {
baseTitle: base.title,
name:
user.display_name ?? user.email.split('@')[0].toLocaleUpperCase(),
@@ -142,7 +150,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `Reset your password`,
html: ejs.render(PasswordReset, {
html: await this.renderMail('PasswordReset', {
email: user.email,
link: this.buildUrl(req, {
token: (user as any).reset_password_token,
@@ -157,7 +165,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `Verify your email`,
html: ejs.render(VerifyEmail, {
html: await this.renderMail('VerifyEmail', {
email: user.email,
link: this.buildUrl(req, {
verificationToken: (user as any).email_verification_token,
@@ -171,7 +179,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `Welcome to NocoDB!`,
html: ejs.render(Welcome, {
html: await this.renderMail('Welcome', {
email: user.email,
link: this.buildUrl(req, {}),
}),
@@ -183,7 +191,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `You have been invited to join NocoDB`,
html: ejs.render(OrganizationInvite, {
html: await this.renderMail('OrganizationInvite', {
name:
user.display_name ?? user.email.split('@')[0].toLocaleUpperCase(),
email: user.email,
@@ -199,7 +207,7 @@ export class MailService {
await mailerAdapter.mailSend({
to: user.email,
subject: `Role updated in NocoDB`,
html: ejs.render(OrganizationRoleUpdate, {
html: await this.renderMail('OrganizationRoleUpdate', {
name:
user.display_name ?? user.email.split('@')[0].toLocaleUpperCase(),
email: user.email,

File diff suppressed because one or more lines are too long

View File

@@ -1,26 +1,33 @@
import {
Body,
Button,
Head,
Heading,
Html,
Preview,
Section,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
// Corresponding ejs template
interface Props {
interface BaseInviteTemplateProps {
baseTitle: string;
name: string;
email: string;
link: string;
}
export const BaseInvite = () => (
export const BaseInvite = ({
baseTitle,
name,
email,
link,
}: BaseInviteTemplateProps) => (
<Html>
<RootWrapper>
<Head />
@@ -31,22 +38,31 @@ export const BaseInvite = () => (
Youve been invited to a Base
</Heading>
<Section className="py-6 mx-auto font-bold mx-auto text-center text-gray-900 text-base">
{'<%= baseTitle %>'}
{baseTitle}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
<span className="font-bold text-gray-800">{'<%= name %>'}</span> ( {'<%= email %>'}) has invited you to
collaborate on <span className="font-bold text-gray-800">{'<%= baseTitle %>'}</span>
<span className="font-bold text-gray-800">{name}</span> ({email})
has invited you to collaborate on{' '}
<span className="font-bold text-gray-800">{baseTitle}</span>
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to Base
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Go to Base</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
BaseInvite.PreviewProps = {
baseTitle: 'Base Title',
name: 'John Doe',
email: 'johndoe@nocodb.com',
link: 'https://app.nocodb.com',
};
export default BaseInvite;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,83 @@
import {
Body,
Button,
Head,
Heading,
Html,
Img,
Preview,
Section,
Text,
} from '@react-email/components';
import * as React from 'react';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
interface BaseRoleUpdateTemplateProps {
baseTitle: string;
role: string;
name: string;
email: string;
link: string;
}
export const BaseRoleUpdate = ({
baseTitle,
email,
link,
role,
name,
}: BaseRoleUpdateTemplateProps) => (
<Html>
<RootWrapper>
<Head />
<Preview>Your base role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your base role has been updated
</Heading>
<Section className="py-6 text-center font-bold text-gray-900 text-base">
{baseTitle}
</Section>
<Section className="pb-6 text-center">
<Img
src={`${NC_EMAIL_ASSETS_BASE_URL}/badges/${role}.png`}
alt={role}
className="h-7 mx-auto"
/>
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in
<span className="font-bold text-gray-800">{baseTitle}</span>
has been updated to{' '}
<span className="font-bold text-gray-800 capitalize ">{role}</span>
by <span className="font-bold text-gray-800">{` ${name}`}</span> ({' '}
{email})
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Go to Base</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
BaseRoleUpdate.PreviewProps = {
baseTitle: 'Base Title',
role: 'editor',
name: 'John Doe',
email: 'johndoe@nocodb.com',
link: 'www.nocodb.com',
};
export default BaseRoleUpdate;

View File

@@ -1,16 +1,14 @@
import { Img, Section, Container } from '@react-email/components';
import { baseUrl } from '../utils/constant';
import { Container, Img, Section } from '@react-email/components';
import * as React from 'react';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
const ContentWrapper = ({ children }: {
children: React.ReactNode;
}) => {
export const ContentWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<Container className="px-3 my-16 max-w-[480px]">
<Section className="py-6 m-auto bg-gray-50 border border-gray-200 border-solid rounded-t-xl">
<Img
alt="NocoDB"
src={`${baseUrl}/nocodb-logo.png`}
src={`${NC_EMAIL_ASSETS_BASE_URL}/nocodb-logo.png`}
width={40}
style={{ display: 'block', margin: 'auto auto' }}
height={40}
@@ -19,9 +17,6 @@ const ContentWrapper = ({ children }: {
<Section className="p-6 border border-gray-200 border-solid border-t-0 rounded-b-xl bg-white">
{children}
</Section>
</Container>
)
}
export default ContentWrapper;
);
};

View File

@@ -1,60 +1,73 @@
import { Column, Container, Img, Link, Row, Section, Text } from '@react-email/components';
import {
Column,
Container,
Img,
Link,
Row,
Section,
Text,
} from '@react-email/components';
import * as React from 'react';
import { baseUrl } from '../utils/constant';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
export const Footer = () => {
return (
<Container className="px-3">
<Text className="text-gray-500 m-auto text-sm max-w-[400px] text-center">
NocoDB is your solution for all your no-code needs.
Now on cloud, we help organisations maintain critical data with our solutions.
NocoDB is your solution for all your no-code needs. Now on cloud, we
help organisations maintain critical data with our solutions.
</Text>
<Section className="mt-12">
<Row className="max-w-[100px] m-auto">
<Column>
<Link href="https://github.com/nocodb" target="_blank">
<Img alt="Github"
src={`${baseUrl}/social/github.png`}
height={32}
width={32}
<Img
alt="Github"
src={`${NC_EMAIL_ASSETS_BASE_URL}/social/github.png`}
height={32}
width={32}
/>
</Link>
</Column>
<Column>
<Link href="https://twitter.com/nocodb" target="_blank">
<Img alt="X"
src={`${baseUrl}/social/x.png`}
height={32}
width={32} />
<Img
alt="X"
src={`${NC_EMAIL_ASSETS_BASE_URL}/social/x.png`}
height={32}
width={32}
/>
</Link>
</Column>
<Column>
<Link href="https://www.youtube.com/@nocodb" target="_blank">
<Img alt="Youtube"
height={32}
width={32}
src={`${baseUrl}/social/youtube.png`}
<Img
alt="Youtube"
height={32}
width={32}
src={`${NC_EMAIL_ASSETS_BASE_URL}/social/youtube.png`}
/>
</Link>
</Column>
<Column>
<Link href="http://discord.nocodb.com/" target="_blank">
<Img alt="Discord"
src={`${baseUrl}/social/discord.png`}
height={32}
width={32}
<Img
alt="Discord"
src={`${NC_EMAIL_ASSETS_BASE_URL}/social/discord.png`}
height={32}
width={32}
/>
</Link>
</Column>
<Column>
<Link href="https://www.linkedin.com/company/nocodb" target="_blank">
<Img alt="Linkedin"
src={`${baseUrl}/social/linkedin.png`}
height={32}
width={32}
<Link
href="https://www.linkedin.com/company/nocodb"
target="_blank"
>
<Img
alt="Linkedin"
src={`${NC_EMAIL_ASSETS_BASE_URL}/social/linkedin.png`}
height={32}
width={32}
/>
</Link>
</Column>
@@ -108,5 +121,5 @@ export const Footer = () => {
</Row>
</Section>
</Container>
)
}
);
};

View File

@@ -1,9 +1,7 @@
import { Font, Tailwind } from '@react-email/components';
import * as React from 'react';
const RootWrapper = ({children}: {
children: React.ReactNode;
}) => {
export const RootWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<Tailwind
config={{
@@ -12,56 +10,56 @@ const RootWrapper = ({children}: {
sans: ['Manrope', 'sans-serif'],
},
fontSize: {
xs: ["12px", { lineHeight: "16px" }],
sm: ["14px", { lineHeight: "20px" }],
base: ["16px", { lineHeight: "24px" }],
lg: ["18px", { lineHeight: "28px" }],
xl: ["20px", { lineHeight: "28px" }],
"2xl": ["24px", { lineHeight: "32px" }],
"3xl": ["30px", { lineHeight: "36px" }],
"4xl": ["36px", { lineHeight: "36px" }],
"5xl": ["48px", { lineHeight: "1" }],
"6xl": ["60px", { lineHeight: "1" }],
"7xl": ["72px", { lineHeight: "1" }],
"8xl": ["96px", { lineHeight: "1" }],
"9xl": ["144px", { lineHeight: "1" }],
xs: ['12px', { lineHeight: '16px' }],
sm: ['14px', { lineHeight: '20px' }],
base: ['16px', { lineHeight: '24px' }],
lg: ['18px', { lineHeight: '28px' }],
xl: ['20px', { lineHeight: '28px' }],
'2xl': ['24px', { lineHeight: '32px' }],
'3xl': ['30px', { lineHeight: '36px' }],
'4xl': ['36px', { lineHeight: '36px' }],
'5xl': ['48px', { lineHeight: '1' }],
'6xl': ['60px', { lineHeight: '1' }],
'7xl': ['72px', { lineHeight: '1' }],
'8xl': ['96px', { lineHeight: '1' }],
'9xl': ['144px', { lineHeight: '1' }],
},
spacing: {
px: "1px",
0: "0",
0.5: "2px",
1: "4px",
1.5: "6px",
2: "8px",
2.5: "10px",
3: "12px",
3.5: "14px",
4: "16px",
5: "20px",
6: "24px",
7: "28px",
8: "32px",
9: "36px",
10: "40px",
11: "44px",
12: "48px",
14: "56px",
16: "64px",
20: "80px",
24: "96px",
28: "112px",
32: "128px",
36: "144px",
40: "160px",
44: "176px",
48: "192px",
52: "208px",
56: "224px",
60: "240px",
64: "256px",
72: "288px",
80: "320px",
96: "384px",
px: '1px',
0: '0',
0.5: '2px',
1: '4px',
1.5: '6px',
2: '8px',
2.5: '10px',
3: '12px',
3.5: '14px',
4: '16px',
5: '20px',
6: '24px',
7: '28px',
8: '32px',
9: '36px',
10: '40px',
11: '44px',
12: '48px',
14: '56px',
16: '64px',
20: '80px',
24: '96px',
28: '112px',
32: '128px',
36: '144px',
40: '160px',
44: '176px',
48: '192px',
52: '208px',
56: '224px',
60: '240px',
64: '256px',
72: '288px',
80: '320px',
96: '384px',
},
extend: {
colors: {
@@ -207,5 +205,3 @@ const RootWrapper = ({children}: {
</Tailwind>
);
};
export default RootWrapper;

View File

@@ -0,0 +1,5 @@
import { ContentWrapper } from '~/services/mail/templates/components/ContentWrapper';
import { Footer } from '~/services/mail/templates/components/Footer';
import { RootWrapper } from '~/services/mail/templates/components/RootWrapper';
export { ContentWrapper, Footer, RootWrapper };

View File

@@ -1,10 +1,10 @@
import Welcome from './welcome';
import BaseInvite from './base-invite';
import BaseRoleUpdate from './base-role-update';
import PasswordReset from './password-reset';
import VerifyEmail from './verify-your-email';
import OrganizationInvite from './org-invite';
import OrganizationRoleUpdate from './org-role-update';
import Welcome from '~/services/mail/templates/welcome';
import BaseInvite from '~/services/mail/templates/base-invite';
import PasswordReset from '~/services/mail/templates/password-reset';
import VerifyEmail from '~/services/mail/templates/verify-your-email';
import OrganizationInvite from '~/services/mail/templates/org-invite';
import OrganizationRoleUpdate from '~/services/mail/templates/org-role-update';
import BaseRoleUpdate from '~/services/mail/templates/base-role-update';
export {
Welcome,

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import {
Body,
Button,
Head,
Heading,
Html,
Preview,
Text,
} from '@react-email/components';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
interface OrganizationInviteTemplateProps {
name: string;
email: string;
link: string;
}
export const OrganizationInvite = ({
name,
email,
link,
}: OrganizationInviteTemplateProps) => (
<Html>
<RootWrapper>
<Head />
<Preview>Youve been invited to NocoDB</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Youve been invited to NocoDB
</Heading>
<Text className="text-gray-600 text-center text-sm">
<span className="font-bold text-gray-800">{name}</span> ( {email})
has invited you to collaborate on NocoDB.
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Go to NocoDB</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
OrganizationInvite.PreviewProps = {
name: 'John Doe',
email: 'johndoe@gmail.com',
link: 'https://nocodb.com',
};
export default OrganizationInvite;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,74 @@
import {
Body,
Button,
Head,
Heading,
Html,
Img,
Preview,
Section,
Text,
} from '@react-email/components';
import * as React from 'react';
import { NC_EMAIL_ASSETS_BASE_URL } from '~/constants';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
interface OrganizationRoleUpdateTemplateProps {
role: string;
name: string;
email: string;
link: string;
}
export const OrganizationRoleUpdate = ({
role,
email,
name,
link,
}: OrganizationRoleUpdateTemplateProps) => (
<Html>
<RootWrapper>
<Head />
<Preview>Your organization role has been updated</Preview>
<Body className="bg-white">
<ContentWrapper>
<Heading className="text-gray-900 text-center font-bold m-auto text-xl md:text-2xl">
Your organization role has been updated
</Heading>
<Section className="py-6 text-center">
<Img
src={`${NC_EMAIL_ASSETS_BASE_URL}/badges/${role}.png`}
alt="<%= role %>"
className="h-7 mx-auto"
/>
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Your access in NocoDB has been updated to
<span className="font-bold text-gray-800 capitalize"> {role} </span>
by <span className="font-bold text-gray-800">{name}</span> ({email})
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Go to NocoDB</Text>
</Button>
</ContentWrapper>
<Footer />
</Body>
</RootWrapper>
</Html>
);
OrganizationRoleUpdate.PreviewProps = {
role: 'creator',
email: 'janedoe@nocodb.com',
name: 'Jane Doe',
link: 'https://nocodb.com',
};
export default OrganizationRoleUpdate;

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,26 @@
import {
Body,
Button,
Head,
Heading,
Html,
Preview,
Section,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
// Corresponding ejs template
interface Props {
interface PasswordResetTemplateProps {
email: string;
link: string;
}
export const PasswordReset = () => (
export const PasswordReset = ({ email, link }: PasswordResetTemplateProps) => (
<Html>
<RootWrapper>
<Head />
@@ -29,15 +31,17 @@ export const PasswordReset = () => (
Password reset requested
</Heading>
<Section className="py-6 mx-auto font-bold text-center text-gray-900 text-base">
{'<%= email %>'}
{email}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Youve requested for a password reset, click on the Reset Password button to reset your password.
Youve requested for a password reset, click on the Reset Password
button to reset your password.
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Reset Password
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Reset Password</Text>
</Button>
</ContentWrapper>
<Footer />
@@ -45,4 +49,10 @@ export const PasswordReset = () => (
</RootWrapper>
</Html>
);
PasswordReset.PreviewProps = {
email: 'janedoe@nocodb.com',
link: 'https://nocodb.com',
};
export default PasswordReset;

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,29 @@
import {
Body,
Button,
Head,
Heading,
Html,
Preview,
Section,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
// Corresponding ejs template
interface Props {
interface VerifyYourEmailTemplateProps {
email: string;
link: string;
}
export const VerifyYourEmail = () => (
export const VerifyYourEmail = ({
email,
link,
}: VerifyYourEmailTemplateProps) => (
<Html>
<RootWrapper>
<Head />
@@ -29,15 +34,16 @@ export const VerifyYourEmail = () => (
Verify your Email
</Heading>
<Section className="py-6 mx-auto font-bold text-center text-gray-900 text-base">
{'<%= email %>'}
{email}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
Please verify your account to complete the sign-up process.
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Verify Email
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Verify Email</Text>
</Button>
</ContentWrapper>
<Footer />
@@ -45,4 +51,10 @@ export const VerifyYourEmail = () => (
</RootWrapper>
</Html>
);
VerifyYourEmail.PreviewProps = {
email: 'janedoe@gmail.com',
link: 'https://nocodb.com',
};
export default VerifyYourEmail;

File diff suppressed because one or more lines are too long

View File

@@ -1,24 +1,26 @@
import {
Body,
Button,
Head,
Heading,
Html,
Preview,
Section,
Text,
Button, Section,
} from '@react-email/components';
import * as React from 'react';
import RootWrapper from '../components/RootWrapper';
import { Footer } from '../components/Footer';
import ContentWrapper from '../components/ContentWrapper';
import {
ContentWrapper,
Footer,
RootWrapper,
} from '~/services/mail/templates/components';
// Corresponding ejs template
interface Props {
interface WelcomeTemplateProps {
email: string;
link: string;
}
export const Welcome = () => (
export const Welcome = ({ email, link }: WelcomeTemplateProps) => (
<Html>
<RootWrapper>
<Head />
@@ -29,13 +31,16 @@ export const Welcome = () => (
Welcome to NocoDB!
</Heading>
<Section className="py-6 mx-auto font-bold text-center text-gray-900 text-base">
{'<%= email %>'}
{email}
</Section>
<Text className="text-gray-600 text-center text-sm !mt-0">
We're thrilled to have you on board! 🚀 Turn your databases into powerful smart tables and manage your data the way you want no code required.
We're thrilled to have you on board! 🚀 Turn your databases into
powerful smart tables and manage your data the way you want no
code required.
</Text>
<Text className="text-gray-600 text-center text-sm !mt-0">
Get started by creating your first project or exploring templates to see whats possible.
Get started by creating your first project or exploring templates to
see whats possible.
</Text>
<Text className="text-gray-600 text-center text-sm !mt-0">
Need help? Our docs and community are just a click away.
@@ -43,10 +48,11 @@ export const Welcome = () => (
<Text className="text-gray-600 text-center text-sm !mt-0">
Lets build something amazing together! 💡
</Text>
<Button className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10" href="<%= link %>">
<Text className="!my-[8px]">
Go to your Workspace
</Text>
<Button
className="text-center w-full text-base font-bold bg-brand-500 text-white rounded-lg h-10"
href={link}
>
<Text className="!my-[8px]">Go to your Workspace</Text>
</Button>
</ContentWrapper>
<Footer />
@@ -54,4 +60,10 @@ export const Welcome = () => (
</RootWrapper>
</Html>
);
Welcome.PreviewProps = {
email: 'janedoe@nocodb.com',
link: 'https://nocodb.com',
};
export default Welcome;

View File

@@ -20,6 +20,7 @@
"noFallthroughCasesInSwitch": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"jsx": "react",
"paths": {
"src/*": [
"src/*"