Skip to content

Commit

Permalink
feat: resend & react email integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ixahmedxi committed May 26, 2024
1 parent 7580f4a commit 7aae523
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 8 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- [x] github actions.
- [x] SEO (dub.co is a great reference) for example robots.ts file.
- [x] tRPC api with tanstack query.
- [ ] Shadcn UI.
- [x] Shadcn UI.
- [ ] Slate/Plate.js editor.
- [ ] New & Refined README.md.
- [ ] Add a `CONTRIBUTING.md` file.
Expand All @@ -18,7 +18,7 @@
- [x] Neon Database / Drizzle orm.
- [x] Upstash redis
- [x] Clerk authentication.
- [ ] Resend & React email.
- [x] Resend & React email.
- [ ] Stripe payment.
- [ ] Sentry.
- [ ] Posthog.
Expand Down
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export default tseslint.config(

'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',

// we're not building a library here
'jsdoc/require-jsdoc': 'off',
},
},
);
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"db:studio": "drizzle-kit studio",
"db:up": "drizzle-kit up",
"dev": "next dev --turbo",
"email:dev": "SKIP_ENV_VALIDATION=true email dev --port 3001 --dir src/emails/templates",
"format": "pnpm format:write",
"format:check": "prettier \"**/*\" --ignore-unknown --list-different",
"format:write": "prettier \"**/*\" --ignore-unknown --list-different --write",
Expand Down Expand Up @@ -59,6 +60,7 @@
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-slot": "^1.0.2",
"@react-email/components": "^0.0.19",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.39.0",
"@trpc/client": "^11.0.0-rc.374",
Expand All @@ -76,6 +78,8 @@
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-email": "^2.1.4",
"resend": "^3.2.0",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"tailwind-merge": "^2.3.0",
Expand Down
Binary file added public/_static/fonts/Geist-Variable.ttf
Binary file not shown.
Binary file added public/_static/fonts/GeistMono-Variable.ttf
Binary file not shown.
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export const constants = {
'Noodle is a productivity platform including many tools students need to be productive and stay on top of their work such as note taking, task management, and more.',
twitter_handle: '@noodle_run',
github_repo: 'https://github.com/noodle-run/noodle',
domain: 'noodle.run',
discord: 'https://discord.gg/ewKmQd8kYm',
twitter: 'https://x.com/noodle_run',
};
56 changes: 56 additions & 0 deletions src/emails/layouts/Base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
Body,
Container,
Font,
Head,
Html,
Preview,
Tailwind,
} from '@react-email/components';
import { emailTailwindConfig } from '../tailwind';
import type { PropsWithChildren } from 'react';
import { emailBaseUrl } from '../utils';
import { cn } from '@/lib/utils';

type Props = PropsWithChildren<{
title: string;
previewText: string;
className?: string;
}>;

export const BaseEmailLayout = ({
children,
title,
previewText,
className,
}: Props) => {
return (
<Tailwind config={emailTailwindConfig}>
<Html lang="en" dir="ltr">
<Head>
<title>{title}</title>
<Font
fontFamily="Geist Mono"
fallbackFontFamily="monospace"
webFont={{
url: `${emailBaseUrl()}/_static/fonts/GeistMono-Variable.ttf`,
format: 'truetype',
}}
/>
<Font
fontFamily="Geist"
fallbackFontFamily="sans-serif"
webFont={{
url: `${emailBaseUrl()}/_static/fonts/Geist-Variable.ttf`,
format: 'truetype',
}}
/>
</Head>
<Preview>{previewText}</Preview>
<Body>
<Container className={cn('px-3', className)}>{children}</Container>
</Body>
</Html>
</Tailwind>
);
};
177 changes: 177 additions & 0 deletions src/emails/tailwind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { TailwindConfig } from '@react-email/components';
import { gray, indigo, tomato } from '@radix-ui/colors';

/**
* This config is used for the emails, hence why it's almost the same as the normal tailwind config, just more verbose.
*/
export const emailTailwindConfig = {
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
colors: {
black: '#000',
white: '#fff',
transparent: 'transparent',
current: 'currentColor',

background: gray.gray1,
foreground: gray.gray12,
'foreground-muted': gray.gray11,
border: gray.gray4,

gray: {
DEFAULT: gray.gray9,
'foreground-muted': gray.gray11,
foreground: gray.gray12,

app: gray.gray1,
subtle: gray.gray2,
'subtle-border': gray.gray6,

element: gray.gray3,
'element-hover': gray.gray4,
'element-active': gray.gray5,
'element-border': gray.gray7,
'element-border-hover': gray.gray8,

solid: gray.gray9,
'solid-hover': gray.gray10,
},
pink: {
DEFAULT: 'hsl(339 99% 66%)',
'foreground-muted': 'hsl(335 71% 46%)',
foreground: 'hsl(338 62% 24%)',

app: 'hsl(340 100% 99%)',

subtle: 'hsl(351 78% 98%)',
'subtle-border': 'hsl(345 71% 85%)',

element: 'hsl(346 100% 96%)',
'element-hover': 'hsl(346 100% 93%)',
'element-active': 'hsl(347 85% 90%)',
'element-border': 'hsl(345 61% 80%)',
'element-border-hover': 'hsl(345 57% 73%)',

solid: 'hsl(339 99% 66%)',
'solid-hover': 'hsl(338 85% 61%)',
},
salmon: {
DEFAULT: 'hsl(1 91% 69%)',
'foreground-muted': 'hsl(359 57% 53%)',
foreground: 'hsl(3 36% 25%)',

app: 'hsl(0 50% 99%)',

subtle: 'hsl(7 100% 98%)',
'subtle-border': 'hsl(4 100% 86%)',

element: 'hsl(5 100 96%)',
'element-hover': 'hsl(6 100% 92%)',
'element-active': 'hsl(5 100% 88%)',
'element-border': 'hsl(4 79% 80%)',
'element-border-hover': 'hsl(3 70% 73%)',

solid: 'hsl(1 91% 69%)',
'solid-hover': 'hsl(0 79% 65%)',
},
indigo: {
DEFAULT: indigo.indigo9,
'foreground-muted': indigo.indigo11,
foreground: indigo.indigo12,

app: indigo.indigo1,
subtle: indigo.indigo2,
'subtle-border': indigo.indigo6,

element: indigo.indigo3,
'element-hover': indigo.indigo4,
'element-active': indigo.indigo5,
'element-border': indigo.indigo7,
'element-border-hover': indigo.indigo8,

solid: indigo.indigo9,
'solid-hover': indigo.indigo10,
},
red: {
DEFAULT: tomato.tomato9,
'foreground-muted': tomato.tomato11,
foreground: tomato.tomato12,

app: tomato.tomato1,
subtle: tomato.tomato2,
'subtle-border': tomato.tomato6,

element: tomato.tomato3,
'element-hover': tomato.tomato4,
'element-active': tomato.tomato5,
'element-border': tomato.tomato7,
'element-border-hover': tomato.tomato8,

solid: tomato.tomato9,
'solid-hover': tomato.tomato10,
},
},
fontFamily: {
sans: ['Geist'],
mono: ['Geist Mono'],
},
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' }],
},
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',
},
},
} satisfies TailwindConfig;
60 changes: 60 additions & 0 deletions src/emails/templates/early-access-joined.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Heading, Img, Link, Text } from '@react-email/components';
import { BaseEmailLayout } from '../layouts/Base';
import { emailBaseUrl } from '../utils';
import { constants } from '@/constants';

interface Props {
firstName: string;
email: string;
}

export default function EarlyAccessJoinedEmail({ firstName, email }: Props) {
return (
<BaseEmailLayout
title={`${firstName}, you have joined Noodle's early access list!`}
previewText={`Hey ${firstName}, this is to just let you know that you have joined Noodle's early access waiting list.`}
className="py-8"
>
<Img
src={`${emailBaseUrl()}/logo.svg`}
width={50}
height={50}
alt="Noodle logo"
/>
<Heading as="h1" className="my-0 pb-0 pt-6 font-medium">
You are on the list!
</Heading>
<Text className="my-0 pt-4">
Hey {firstName}, Ahmed here, founder and creator of Noodle.
</Text>
<Text>
I wanted to personally thank you for joining Noodle&apos;s early access
list. I am super excited to have you on board and can&apos;t wait for
you to start using Noodle.
</Text>
<Text>
I am currently working hard to get Noodle ready for you and will be in
touch soon to let you know when you can access and start using it. Just
make sure to sign in using the same email you joined the early access
list with, which is{' '}
<Link className="text-pink underline underline-offset-4">{email}</Link>.
</Text>
<Text>
You can join our{' '}
<Link
href={constants.discord}
className="text-pink underline underline-offset-4"
>
Discord server
</Link>{' '}
in the meantime to stay up to date and to share your thoughts and
feedback, helping shape Noodle into the best product it can be.
</Text>
</BaseEmailLayout>
);
}

EarlyAccessJoinedEmail.PreviewProps = {
firstName: 'John',
email: '[email protected]',
};
18 changes: 18 additions & 0 deletions src/emails/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { constants } from '@/constants';
import { env } from '@/env';

/**
* This is used to get the base URL of the assets that the email templates can use.
* @returns The correct URL.
*/
export const emailBaseUrl = () => {
if (env.NODE_ENV === 'development') {
return 'http://localhost:3000';
}

if (env.VERCEL_URL) {
return `https://${env.VERCEL_URL}`;
}

return `https://${constants.domain}`;
};
5 changes: 4 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ export const env = createEnv({
// Neon DB
DATABASE_URL: z.string().url(),

// upstash
// Upstash
UPSTASH_REDIS_REST_URL: z.string(),
UPSTASH_REDIS_REST_TOKEN: z.string(),

// Resend
RESEND_API_KEY: z.string(),

// Clerk
CLERK_SECRET_KEY: z.string(),
},
Expand Down
4 changes: 4 additions & 0 deletions src/lib/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { env } from '@/env';
import { Resend } from 'resend';

export const resend = new Resend(env.RESEND_API_KEY);
Loading

0 comments on commit 7aae523

Please sign in to comment.