Skip to content

Commit

Permalink
feat(client): Add app (#533)
Browse files Browse the repository at this point in the history
  • Loading branch information
bukinoshita authored Mar 12, 2023
1 parent ec4b691 commit 37387e3
Show file tree
Hide file tree
Showing 37 changed files with 4,319 additions and 0 deletions.
5 changes: 5 additions & 0 deletions client/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
10 changes: 10 additions & 0 deletions client/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
appDir: true,
},
};

module.exports = nextConfig;
43 changes: 43 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "react-email-client",
"version": "0.0.11",
"description": "The React Email preview application",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format:check": "prettier --check \"**/*.{ts,tsx,md}\"",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"@radix-ui/colors": "0.1.8",
"@radix-ui/react-collapsible": "1.0.1",
"@radix-ui/react-popover": "1.0.2",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-toggle-group": "1.0.1",
"@radix-ui/react-tooltip": "1.0.2",
"@react-email/render": "0.0.6",
"classnames": "2.3.2",
"framer-motion": "8.4.6",
"next": "13.2.4",
"prism-react-renderer": "1.3.5",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/classnames": "2.3.1",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"autoprefixer": "10.4.13",
"eslint-config-next": "13.2.4",
"postcss": "8.4.19",
"tailwindcss": "3.2.4",
"typescript": "4.9.3"
}
}
6 changes: 6 additions & 0 deletions client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
25 changes: 25 additions & 0 deletions client/src/app/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import { Button, Heading, Text } from '../components';
import { Shell } from '../components/shell';
import Link from 'next/link';

export default function Home({ navItems }) {
return (
<Shell navItems={navItems}>
<div className="max-w-md border border-slate-6 mx-auto mt-56 rounded-md p-8">
<Heading as="h2" weight="medium">
Welcome to the React Email preview!
</Heading>
<Text as="p" className="mt-2 mb-4">
To start developing your next email template, you can create a{' '}
<code>.jsx</code> or <code>.tsx</code> file under the "emails" folder.
</Text>

<Button asChild>
<Link href="https://react.email/docs">Check the docs</Link>
</Button>
</div>
</Shell>
);
}
24 changes: 24 additions & 0 deletions client/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import '../styles/globals.css';
import classnames from 'classnames';
import { Inter } from "next/font/google";

export const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
});

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-black text-slate-12 font-sans">
<div className={classnames(inter.variable, 'font-sans')}>
{children}
</div>
</body>
</html>
);
}
11 changes: 11 additions & 0 deletions client/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Home from './home';
import { getEmails } from '../utils/get-emails';

export default async function Index() {
const { emails } = await getEmails();
return <Home navItems={emails} />;
}

export const metadata = {
title: 'React Email',
};
46 changes: 46 additions & 0 deletions client/src/app/preview/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { getEmails, CONTENT_DIR } from '../../../utils/get-emails';
import { promises as fs } from 'fs';
import { render } from '@react-email/render';
import Preview from './preview';

export const dynamicParams = true;

export async function generateStaticParams() {
const { emails } = await getEmails();

const paths = emails.map((email) => {
return { slug: email };
});

return paths;
}

export default async function Page({ params }) {
const { emails, filenames } = await getEmails();
const template = filenames.filter((email) => {
const [fileName] = email.split('.');
return params.slug === fileName;
});

const Email = (await import(`../../../../emails/${params.slug}`)).default;
const markup = render(<Email />, { pretty: true });
const plainText = render(<Email />, { plainText: true });
const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;
const reactMarkup = await fs.readFile(path, {
encoding: 'utf-8',
});

return (
<Preview
navItems={emails}
slug={params.slug}
markup={markup}
reactMarkup={reactMarkup}
plainText={plainText}
/>
);
}

export async function generateMetadata({ params }) {
return { title: `${params.slug} — React Email` };
}
72 changes: 72 additions & 0 deletions client/src/app/preview/[slug]/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { Shell } from '../../../components/shell';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { CodeContainer } from '../../../components/code-container';
import React from 'react';
import { Tooltip } from '../../../components/tooltip';

export default function Preview({
navItems,
slug,
markup,
reactMarkup,
plainText,
}) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [activeView, setActiveView] = React.useState('desktop');
const [activeLang, setActiveLang] = React.useState('jsx');

React.useEffect(() => {
const view = searchParams.get('view');
const lang = searchParams.get('lang');

if (view === 'source' || view === 'desktop') {
setActiveView(view);
}

if (lang === 'jsx' || lang === 'markup' || lang === 'markdown') {
setActiveLang(lang);
}
}, [searchParams]);

const handleViewChange = (view: string) => {
setActiveView(view);
router.push(`${pathname}?view=${view}`);
};

const handleLangChange = (lang: string) => {
setActiveLang(lang);
router.push(`${pathname}?view=source&lang=${lang}`);
};

return (
<Shell
navItems={navItems}
title={slug}
markup={markup}
activeView={activeView}
setActiveView={handleViewChange}
>
{activeView === 'desktop' ? (
<iframe srcDoc={markup} className="w-full h-[calc(100vh_-_70px)]" />
) : (
<div className="flex gap-6 mx-auto p-6 max-w-3xl">
<Tooltip.Provider>
<CodeContainer
markups={[
{ language: 'jsx', content: reactMarkup },
{ language: 'markup', content: markup },
{ language: 'markdown', content: plainText },
]}
activeLang={activeLang}
setActiveLang={handleLangChange}
/>
</Tooltip.Provider>
</div>
)}
</Shell>
);
}
85 changes: 85 additions & 0 deletions client/src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';
import classnames from 'classnames';
import { unreachable } from '../utils';
import * as SlotPrimitive from '@radix-ui/react-slot';

type ButtonElement = React.ElementRef<'button'>;
type RootProps = React.ComponentPropsWithoutRef<'button'>;

type Appearance = 'white' | 'gradient';
type Size = '1' | '2' | '3' | '4';

interface ButtonProps extends RootProps {
asChild?: boolean;
appearance?: Appearance;
size?: Size;
}

export const Button = React.forwardRef<ButtonElement, Readonly<ButtonProps>>(
(
{
asChild,
appearance = 'white',
className,
children,
size = '2',
...props
},
forwardedRef,
) => {
const classNames = classnames(
getSize(size),
getAppearance(appearance),
'inline-flex items-center justify-center border font-medium',
className,
);

return asChild ? (
<SlotPrimitive.Slot ref={forwardedRef} {...props} className={classNames}>
<SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>
</SlotPrimitive.Slot>
) : (
<button ref={forwardedRef} className={classNames} {...props}>
{children}
</button>
);
},
);

Button.displayName = 'Button';

const getAppearance = (appearance: Appearance | undefined) => {
switch (appearance) {
case undefined:
case 'white':
return [
'bg-white text-black',
'hover:bg-white/90',
'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-white/90',
];
case 'gradient':
return [
'bg-gradient backdrop-blur-[20px] border-[#34343A]',
'hover:bg-gradientHover',
'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-gradientHover',
];
default:
unreachable(appearance);
}
};

const getSize = (size: Size | undefined) => {
switch (size) {
case '1':
return '';
case undefined:
case '2':
return 'text-[14px] h-8 px-3 rounded-md gap-2';
case '3':
return 'text-[14px] h-10 px-4 rounded-md gap-2';
case '4':
return 'text-base h-11 px-4 rounded-md gap-2';
default:
unreachable(size);
}
};
Loading

0 comments on commit 37387e3

Please sign in to comment.