Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ Thumbs.db

## Environment variables
### Prevent `.env` files from being tracked by Git to avoid exposing sensitive information.
.env*
.env*

next-env.d.ts
2 changes: 1 addition & 1 deletion apps/web/license.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2024 Plus Five Five, Inc
Copyright 2025 Plus Five Five, Inc

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
6 changes: 0 additions & 6 deletions apps/web/next-env.d.ts

This file was deleted.

10 changes: 4 additions & 6 deletions apps/web/next.config.js → apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
/** @type {import('next').NextConfig} */
import type { NextConfig } from 'next';

module.exports = {
devIndicators: {
appIsrStatus: false,
},
reactStrictMode: true,
const nextConfig: NextConfig = {
serverExternalPackages: ['@react-email/components', '@react-email/render'],
async redirects() {
return [
Expand Down Expand Up @@ -48,3 +44,5 @@ module.exports = {
];
},
};

export default nextConfig;
2 changes: 0 additions & 2 deletions apps/web/src/app/api/check-spam/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { type NextRequest, NextResponse } from 'next/server';
import { ZodError, z } from 'zod';
import { checkSpam } from './check-spam';

export const dynamic = 'force-dynamic';

export function OPTIONS() {
return Promise.resolve(NextResponse.json({}));
}
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/app/api/send/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server';
import { Resend } from 'resend';
import { z } from 'zod';

export const dynamic = 'force-dynamic';

export function OPTIONS() {
return Promise.resolve(NextResponse.json({}));
}
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/app/components/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { notFound } from 'next/navigation';
import { componentsStructure } from '../../../../components/structure';
import { ComponentsView } from '../../../components/components-view';
import { IconArrowLeft } from '../../../components/icons/icon-arrow-left';
import PageTransition from '../../../components/page-transition';
import { PageTransition } from '../../../components/page-transition';
import { slugify } from '../../../utils/slugify';
import { getImportedComponentsFor } from '../get-imported-components-for';

Expand Down Expand Up @@ -54,14 +54,16 @@ export const generateMetadata = async ({
};
};

const ComponentPage: React.FC<ComponentPageParams> = async ({ params }) => {
export default async function ComponentPage({ params }: ComponentPageParams) {
const { slug: rawSlug } = await params;
const slug = decodeURIComponent(rawSlug);
const foundCategory = componentsStructure.find(
(category) => slugify(category.name) === slug,
);

if (!foundCategory) return <p>Component category not found.</p>;
if (!foundCategory) {
return <p>Component category not found.</p>;
}

const importedComponents = await getImportedComponentsFor(foundCategory);
return (
Expand All @@ -76,7 +78,7 @@ const ComponentPage: React.FC<ComponentPageParams> = async ({ params }) => {
<div className="flex w-full flex-col gap-4 px-6 pt-16 pb-10 md:px-8">
<div className="flex flex-inline">
<Link
className="mr-2 flex scroll-m-2 items-center justify-center gap-2 self-start rounded-md px-2 py-1 text-slate-11 transition transition-colors duration-200 ease-in-out hover:text-slate-12 focus:bg-slate-6 focus:outline-none focus:ring focus:ring-slate-3"
className="mr-2 flex scroll-m-2 items-center justify-center gap-2 self-start rounded-md px-2 py-1 text-slate-11 transition-colors duration-200 ease-in-out hover:text-slate-12 focus:bg-slate-6 focus:outline-none focus:ring focus:ring-slate-3"
href="/components"
>
<IconArrowLeft className="mt-[.0625rem]" size={14} />
Expand All @@ -93,6 +95,4 @@ const ComponentPage: React.FC<ComponentPageParams> = async ({ params }) => {
</PageTransition>
</>
);
};

export default ComponentPage;
}
163 changes: 77 additions & 86 deletions apps/web/src/app/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,107 +3,98 @@ import type { Metadata } from 'next';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { componentsStructure } from '../../../components/structure';
import PageTransition from '../../components/page-transition';
import { PageTransition } from '../../components/page-transition';
import { Spotlight } from '../../components/spotlight';
import { slugify } from '../../utils/slugify';

const title = 'Components - React Email';
const description =
'Build beautiful emails with pre-built components that you can copy-and-paste into your app.';

export const metadata: Metadata = {
title,
description,
title: 'Components - React Email',
description:
'Build beautiful emails with pre-built components that you can copy-and-paste into your app.',
openGraph: {
title,
description,
images: [
{
url: 'https://react.email/static/covers/patterns.png',
},
],
images: [{ url: 'https://react.email/static/covers/patterns.png' }],
},
alternates: {
canonical: '/components',
},
};

const ComponentsPage = async () => (
<>
<div className="pointer-events-none absolute inset-0 flex justify-center">
<div className="hidden h-full w-full max-w-7xl grid-cols-2 gap-4 px-4 lg:grid">
<div className="border-r-slate-3 border-l border-l-slate-4" />
<div className="border-r border-r-slate-4" />
</div>
</div>
<PageTransition className="pb-10" key="about" tag="main">
<div className="flex w-full flex-col gap-2 px-6 pt-16 pb-10 md:px-8">
<h1 className="font-bold text-2xl text-slate-12">Components</h1>
<p>
Build beautiful emails with pre-built components that you can
copy-and-paste into your app.
</p>
export default async function ComponentsPage() {
return (
<>
<div className="pointer-events-none absolute inset-0 flex justify-center">
<div className="hidden h-full w-full max-w-7xl grid-cols-2 gap-4 px-4 lg:grid">
<div className="border-r-slate-3 border-l border-l-slate-4" />
<div className="border-r border-r-slate-4" />
</div>
</div>
<div className="relative grid grid-cols-1 gap-x-4 px-1 pb-10 md:grid-cols-2 md:px-0 lg:grid-cols-3">
<div className="-translate-x-1/2 absolute top-0 left-1/2 h-px w-[100dvw] border-slate-4 border-t" />
<div className="-translate-x-1/2 absolute bottom-0 left-1/2 h-px w-[100dvw] border-slate-4 border-b" />
{componentsStructure.map((category, index) => {
const slug = slugify(category.name);
const Illustration = dynamic(
() =>
import(
`@/illustrations/${category.name
.toLowerCase()
.replace(/ /g, '-')}`
),
);
<PageTransition className="pb-10" key="about" tag="main">
<div className="flex w-full flex-col gap-2 px-6 pt-16 pb-10 md:px-8">
<h1 className="font-bold text-2xl text-slate-12">Components</h1>
<p>
Build beautiful emails with pre-built components that you can
copy-and-paste into your app.
</p>
</div>
<div className="relative grid grid-cols-1 gap-x-4 px-1 pb-10 md:grid-cols-2 md:px-0 lg:grid-cols-3">
<div className="-translate-x-1/2 absolute top-0 left-1/2 h-px w-[100dvw] border-slate-4 border-t" />
<div className="-translate-x-1/2 absolute bottom-0 left-1/2 h-px w-[100dvw] border-slate-4 border-b" />
{componentsStructure.map((category, index) => {
const slug = slugify(category.name);
const Illustration = dynamic(
() =>
import(
`@/illustrations/${category.name
.toLowerCase()
.replace(/ /g, '-')}`
),
);

return (
<Link
className={classNames(
'group relative isolate mt-7 cursor-pointer scroll-m-6 rounded-md focus:outline-none focus:ring focus:ring-slate-2 md:before:absolute md:before:inset-0 md:before:rounded-md md:before:border md:before:border-slate-4 md:before:border-dashed md:before:transition-colors md:before:duration-[720ms] md:before:ease-[cubic-bezier(.24,.9,.32,1.4)] md:focus:before:border-slate-6 md:hover:before:border-slate-6',
{
'md:ml-6': index % 3 === 0,
'md:mx-3': index % 3 === 1,
'md:mr-6': index % 3 === 2,
},
)}
href={`/components/${slug}`}
key={category.name}
tabIndex={0}
>
<Spotlight
return (
<Link
className={classNames(
'relative isolate flex cursor-pointer flex-col justify-end rounded-md bg-black p-4 group-focus:ring group-focus:ring-slate-2 md:transition-transform md:duration-[240ms] md:ease-[cubic-bezier(.36,.66,.6,1)]',
'group relative isolate mt-7 cursor-pointer scroll-m-6 rounded-md focus:outline-none focus:ring focus:ring-slate-2 md:before:absolute md:before:inset-0 md:before:rounded-md md:before:border md:before:border-slate-4 md:before:border-dashed md:before:transition-colors md:before:duration-[720ms] md:before:ease-[cubic-bezier(.24,.9,.32,1.4)] md:focus:before:border-slate-6 md:hover:before:border-slate-6',
{
'md:group-hover:-translate-x-2 md:group-hover:-translate-y-2 md:group-focus:-translate-x-2 md:group-focus:-translate-y-2':
index % 3 === 0,
'md:group-hover:-translate-y-2 md:group-focus:-translate-y-2':
index % 3 === 1,
'md:group-hover:-translate-y-2 md:group-focus:-translate-y-2 md:group-focus:translate-x-2 md:group-hover:translate-x-2':
index % 3 === 2,
'md:ml-6': index % 3 === 0,
'md:mx-3': index % 3 === 1,
'md:mr-6': index % 3 === 2,
},
)}
href={`/components/${slug}`}
key={category.name}
tabIndex={0}
>
<div className="pointer-events-none absolute inset-0 rounded-md border border-slate-4 transition-colors duration-300 ease-[cubic-bezier(.36,.66,.6,1)] group-hover:border-slate-6 group-focus:border-slate-6" />
<div className="relative flex aspect-[2/1] items-center justify-center overflow-hidden rounded-sm text-slate-300">
<div className="absolute inset-0 bg-[radial-gradient(#27272A_.0313rem,transparent_.0313rem),_radial-gradient(#27272A_.0313rem,transparent_.0313rem)] bg-transparent opacity-80 [background-position:0_0,.625rem_.625rem] [background-size:1.25rem_1.25rem]" />
<Illustration />
</div>
<h3 className="relative z-[2] mt-4 font-semibold text-slate-12 capitalize leading-7 tracking-wide">
{category.name}
</h3>
<span className="relative z-[2] text-slate-11 text-xs">
{category.components.length} component
{category.components.length > 1 && 's'}
</span>
</Spotlight>
</Link>
);
})}
</div>
</PageTransition>
</>
);

export default ComponentsPage;
<Spotlight
className={classNames(
'relative isolate flex cursor-pointer flex-col justify-end rounded-md bg-black p-4 group-focus:ring group-focus:ring-slate-2 md:transition-transform md:duration-[240ms] md:ease-[cubic-bezier(.36,.66,.6,1)]',
{
'md:group-hover:-translate-x-2 md:group-hover:-translate-y-2 md:group-focus:-translate-x-2 md:group-focus:-translate-y-2':
index % 3 === 0,
'md:group-hover:-translate-y-2 md:group-focus:-translate-y-2':
index % 3 === 1,
'md:group-hover:-translate-y-2 md:group-focus:-translate-y-2 md:group-focus:translate-x-2 md:group-hover:translate-x-2':
index % 3 === 2,
},
)}
>
<div className="pointer-events-none absolute inset-0 rounded-md border border-slate-4 transition-colors duration-300 ease-[cubic-bezier(.36,.66,.6,1)] group-hover:border-slate-6 group-focus:border-slate-6" />
<div className="relative flex aspect-[2/1] items-center justify-center overflow-hidden rounded-sm text-slate-300">
<div className="absolute inset-0 bg-[radial-gradient(#27272A_.0313rem,transparent_.0313rem),_radial-gradient(#27272A_.0313rem,transparent_.0313rem)] bg-transparent opacity-80 [background-position:0_0,.625rem_.625rem] [background-size:1.25rem_1.25rem]" />
<Illustration />
</div>
<h3 className="relative z-[2] mt-4 font-semibold text-slate-12 capitalize leading-7 tracking-wide">
{category.name}
</h3>
<span className="relative z-[2] text-slate-11 text-xs">
{category.components.length} component
{category.components.length > 1 && 's'}
</span>
</Spotlight>
</Link>
);
})}
</div>
</PageTransition>
</>
);
}
10 changes: 6 additions & 4 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ export const viewport = {
themeColor: '#25AEBA',
};

const RootLayout = ({ children }: { children: React.ReactNode }) => {
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
className={`${inter.variable} ${commitMono.variable} antialiased`}
Expand All @@ -102,6 +106,4 @@ const RootLayout = ({ children }: { children: React.ReactNode }) => {
</body>
</html>
);
};

export default RootLayout;
}
52 changes: 26 additions & 26 deletions apps/web/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import PageTransition from '@/components/page-transition';
import { PageTransition } from '@/components/page-transition';

export const metadata = {
title: '404 Not found',
Expand All @@ -7,29 +7,29 @@ export const metadata = {
},
};

const NotFound = () => (
<>
<div className="pointer-events-none absolute inset-0 flex justify-center">
<div className="hidden h-full w-full max-w-7xl grid-cols-2 gap-4 px-4 lg:grid">
<div className="border-r-slate-3 border-l border-l-slate-4" />
<div className="border-r border-r-slate-4" />
export default function NotFound() {
return (
<>
<div className="pointer-events-none absolute inset-0 flex justify-center">
<div className="hidden h-full w-full max-w-7xl grid-cols-2 gap-4 px-4 lg:grid">
<div className="border-r-slate-3 border-l border-l-slate-4" />
<div className="border-r border-r-slate-4" />
</div>
</div>
</div>
<PageTransition
className="flex w-full flex-col items-center justify-center gap-2 px-8 pt-16 pb-10 text-center"
key="not-found"
tag="main"
>
<h1 className="font-bold text-2xl text-slate-12 uppercase italic">
<span className="font-mono">404</span> <br />
Not Found
</h1>
<div className="mt-1 leading-loose">
<p>This page does not exist.</p>
<p>Please check the URL and try again.</p>
</div>
</PageTransition>
</>
);

export default NotFound;
<PageTransition
className="flex w-full flex-col items-center justify-center gap-2 px-8 pt-16 pb-10 text-center"
key="not-found"
tag="main"
>
<h1 className="font-bold text-2xl text-slate-12 uppercase italic">
<span className="font-mono">404</span> <br />
Not Found
</h1>
<div className="mt-1 leading-loose">
<p>This page does not exist.</p>
<p>Please check the URL and try again.</p>
</div>
</PageTransition>
</>
);
}
Loading
Loading