Skip to content

Commit

Permalink
chore(core): Added integration of resend and @react-email
Browse files Browse the repository at this point in the history
  • Loading branch information
wootsbot committed Jul 27, 2023
1 parent 3e61142 commit e754407
Show file tree
Hide file tree
Showing 22 changed files with 1,920 additions and 271 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-panthers-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'x-boilerplate': minor
---

Added integration of resend and `@react-email`
14 changes: 10 additions & 4 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
NEXT_PUBLIC_MSW_MOCKING = 'disabled'

GITHUB_ID=
GITHUB_SECRET=
GITHUB_ID=""
GITHUB_SECRET=""
NEXTAUTH_URL="http://localhost:3000/"
SECRET=""

NEXTAUTH_URL=http://localhost:3000/
RESEND_API_KEY=""
RESEND_DOMAIN=""

SECRET=
TWITTER_CREATOR="@wootsbot"
SITE_NAME="X Boilerplate"
SITE_URL="https://example.com"
NEXT_PUBLIC_SITE_URL="https://example.com"
2 changes: 0 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ public/
node_modules/
yarn.lock
package.json
tsconfig.json
lerna.json
package-lock.json

mocks/
next.config.js
.changeset

.vscode
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<br>

<p align='center'>
<a href="https://beta-x-boilerplate.vercel.app/">Live Demo</a>
<a href="https://x.openkit.run/">Live Demo</a>
</p>

<br>
Expand Down Expand Up @@ -63,6 +63,7 @@ Many of this boilerplate features are based on the philosophy of being optional.
- [x] ~Authentication (next-auth)~
- [x] ~Integrate linters, hooks to DX~
- [x] Integrate resend
- [x] Integrate @react-email
- [x] Integrate ORM prisma
- [x] Integrate supabase
- [x] Integrate planetscale
Expand Down
90 changes: 90 additions & 0 deletions app/(marketing)/resend/InviteEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable react/no-unescaped-entities */
import * as React from 'react';

import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from '@react-email/components';

import env from '~/env';

interface InviteEmailProps {
invitedByUsername?: string;
inviteLink?: string;
}

const baseUrl = env.SITE_URL ?? '';

export const InviteEmail = ({ invitedByUsername = 'X Boilerplate', inviteLink }: InviteEmailProps) => {
return (
<Html>
<Head />
<Preview>{invitedByUsername}</Preview>
<Tailwind>
<Body className="bg-white my-auto mx-auto font-sans">
<Container className="border border-solid border-[#eaeaea] rounded my-[40px] mx-auto p-[20px] w-[465px]">
<Section className="mt-[32px]">
<Img src={`${baseUrl}/email-img.png`} width="40" height="40" alt="Openkit img" className="my-0 mx-auto" />
</Section>

<Heading className="text-black text-[24px] font-normal text-center p-0 mt-[30px] mb-[16px] mx-0">
Event
</Heading>

<Heading className="text-black text-[24px] font-normal text-center p-0 mb-[30px] mx-0">
<strong>
Open Kit - Community <span className="text-blue-700">2023</span>
</strong>
</Heading>

<Text className="text-black text-[14px] leading-[24px]">
On behalf of the entire <strong>openkit</strong> team, we want to express our sincerest gratitude for
being part of this event and sharing your valuable knowledge with our community.
</Text>

<Section>
<Text className="text-black text-[14px] leading-[24px]">
Don't miss out on the action! Make sure to add your talk to your calendar. We eagerly await you!
</Text>
</Section>

<Section className="text-center mt-[32px] mb-[32px]">
<Button
pX={20}
pY={12}
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center"
href={inviteLink}
>
Join the Talk!
</Button>
</Section>
<Text className="text-black text-[14px] leading-[24px]">
Or copy and paste this URL into your browser:{' '}
<Link href={inviteLink} className="text-blue-600 no-underline">
{inviteLink}
</Link>
</Text>
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
<Text className="text-[#666666] text-[12px] leading-[24px]">
🪂 X-boilerplate a starting boilerplate with configuration and best practices for your Nextjs projects, so
you can only focus on building your product.
</Text>
</Container>
</Body>
</Tailwind>
</Html>
);
};

export default InviteEmail;
104 changes: 104 additions & 0 deletions app/(marketing)/resend/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useForm } from 'react-hook-form';
import toast from 'react-hot-toast';

import Button from '@design-system/Button';
import InputField from '@design-system/InputField';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

import { sendEmailSchema } from '@/utils/validations/send-email';

import Header from '@/components/Header';
import { useResendEmail } from '@/hooks/services/resend/email/use-resend-email.hook';

type FormValues = z.infer<typeof sendEmailSchema>;

function ResendPage() {
const router = useRouter();

const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<FormValues>({
resolver: zodResolver(sendEmailSchema),
});

const { mutate: handleResendEmail, isLoading } = useResendEmail({
onSuccess: (data) => {
toast.success('Congratulations, you have successfully sent an email.');
reset();
},
onError: (_error) => {
toast.error(`It seems we couldn't send the email, please try again.`);
},
});

function handleGoBack() {
router.push('/');
}

function handleSubmitValues(data: FormValues) {
console.log('data', data);
handleResendEmail({ emailTo: data.emailToField, subject: data.subjectField, inviteLink: data.inviteLinkField });
}

return (
<>
<Header
title="✉️ Resend"
subTitle="Hi "
name="Example with Resend, @react-email and TanStack Query"
message="Example of sending an email with an invitation design to participate in an event, including a link."
onGoBack={handleGoBack}
/>

<div
style={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
}}
>
<form style={{ marginTop: 16 }} onSubmit={handleSubmit(handleSubmitValues)}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 24 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<InputField
placeholder="Email to"
isError={Boolean(errors?.emailToField)}
errMsg={errors?.emailToField?.message}
{...register('emailToField')}
/>

<InputField
placeholder="Email subject"
isError={Boolean(errors?.subjectField)}
errMsg={errors?.subjectField?.message}
{...register('subjectField')}
/>

<InputField
placeholder="Invite link"
isError={Boolean(errors?.inviteLinkField)}
errMsg={errors?.inviteLinkField?.message}
{...register('inviteLinkField')}
/>
</div>

<Button style={{ marginTop: '8px' }} type="submit" disabled={isLoading}>
{isLoading ? 'Sending Email...' : ' Send Email'}
</Button>
</div>
</form>
</div>
</>
);
}

export default ResendPage;
66 changes: 66 additions & 0 deletions app/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client';

import * as React from 'react';
import { Toaster } from 'react-hot-toast';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

import StoreProvider from '@/store/StoreProvider';

const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } },
});

const Providers: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<StoreProvider>
<>{children}</>
</StoreProvider>
<ReactQueryDevtools initialIsOpen={false} />

<Toaster
position="top-center"
reverseOrder={false}
gutter={8}
containerClassName="toast-container"
containerStyle={{}}
toastOptions={{
className: 'toast',
duration: 5000,
style: {
borderRadius: '8px',
},
icon: null,
success: {
style: {
background: '#042f2e',
borderColor: '#0f766e',
borderWidth: 1,
color: '#99f6e4',
},
},
error: {
style: {
background: '#4c0519',
borderColor: '#be123c',
borderWidth: 1,
color: '#fecdd3',
},
},
loading: {
style: {
background: '#172554',
borderColor: '#1d4ed8',
borderWidth: 1,
color: '#bfdbfe',
},
},
}}
/>
</QueryClientProvider>
);
};

export default Providers;
28 changes: 28 additions & 0 deletions app/api/resend/email/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';

import { Resend } from 'resend';

import { emailSchema } from '@/hooks/services/resend/email/email.schema';
import InviteEmail from '~/app/(marketing)/resend/InviteEmail';
import env from '~/env';

const resend = new Resend(env.RESEND_API_KEY);

export async function POST(req: Request) {
try {
const json = await req.json();
const emailContent = emailSchema.parse(json);

const data = await resend.emails.send({
from: env.RESEND_DOMAIN,
to: emailContent.emailTo,
subject: emailContent.subject,
react: InviteEmail({ ...emailContent }),
});

return NextResponse.json(data);
} catch (error) {
console.log('error', error);
return NextResponse.json({ error });
}
}
12 changes: 5 additions & 7 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Metadata } from 'next';
import * as React from 'react';

import Providers from './Providers';

import Container from '@/components/Container';
import SocialContainer from '@/components/SocialContainer';
import ProviderRoot from '@/providers/ProviderRoot';
import StoreProvider from '@/store/StoreProvider';

type MainLayoutProps = {
children?: React.ReactNode;
Expand Down Expand Up @@ -61,14 +61,12 @@ function MainLayout({ children }: MainLayoutProps) {
return (
<html lang="en">
<body>
<StoreProvider>
<ProviderRoot>
<Providers>
<main>
<Container>{children}</Container>
</main>
</ProviderRoot>
</StoreProvider>
<SocialContainer />
<SocialContainer />
</Providers>
</body>
</html>
);
Expand Down
6 changes: 6 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ async function HomePage() {
validation with static type inference"
/>

<FeatureCard
to="/resend"
title="✉️ Send emails with Resend, @react-email and TanStack Query"
description="Resend is the email API for developers."
/>

{/* <FeatureCard>
<Typography as="h2">🔥 Mocking via msw</Typography>
<Typography size="s">
Expand Down
2 changes: 1 addition & 1 deletion components/HelloForm/HelloForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import InputField from '@design-system/InputField';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

import { helloFormSchema } from '@/libs/validations/hello-form';
import { helloFormSchema } from '@/utils/validations/hello-form';

import { HelloFormProps } from './HelloForm.type';

Expand Down
Loading

1 comment on commit e754407

@vercel
Copy link

@vercel vercel bot commented on e754407 Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.