Skip to content

Commit

Permalink
write intial working version with zod
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmfern committed Feb 3, 2025
1 parent 0fd1250 commit 012508b
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 76 deletions.
1 change: 0 additions & 1 deletion apps/demo/emails/magic-links/aws-verify-email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
Section,
Text,
} from '@react-email/components';
import { setupForPreview } from 'react-email';
import { z } from 'zod';

const AWSVerifyEmailProps = z.object({
Expand Down
10 changes: 7 additions & 3 deletions packages/react-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"scripts": {
"build": "tsup-node && node build-preview-server.mjs",
"dev": "tsup-node --watch",
"dev": "cd ../../apps/demo && tsx ../../packages/react-email/src/cli/index.ts dev",
"test": "vitest run",
"test:watch": "vitest",
"clean": "rm -rf dist"
Expand All @@ -33,7 +33,10 @@
"url": "https://github.com/resend/react-email.git",
"directory": "packages/react-email"
},
"keywords": ["react", "email"],
"keywords": [
"react",
"email"
],
"engines": {
"node": ">=18.0.0"
},
Expand Down Expand Up @@ -91,6 +94,7 @@
"tsx": "4.9.0",
"typescript": "5.1.6",
"use-debounce": "10.0.4",
"vitest": "1.1.3"
"vitest": "1.1.3",
"zod": "3.24.1"
}
}
49 changes: 46 additions & 3 deletions packages/react-email/src/actions/get-email-controls.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
'use server';

import type { Controls } from '../package';
import { ZodFirstPartyTypeKind, type ZodString } from 'zod';
import { cachedGetEmailComponent } from '../utils/cached-get-email-component';
import type { ErrorObject } from '../utils/types/error-object';

export type ControlsResult = { error: ErrorObject } | { controls: Controls };
export type Control =
| {
key: string;
type: 'email' | 'text' | 'checkbox' | 'number';
}
| {
key: string;
type: 'select';
options: { name: string; value: string }[];
};

type ControlsResult =
| { error: ErrorObject }
| { controls: Control[] | undefined };

export const getEmailControls = async (
emailPath: string,
Expand All @@ -16,6 +29,36 @@ export const getEmailControls = async (
}

const { emailComponent: Email } = result;
if (!Email.PreviewSchema) return { controls: undefined };

const controls: Control[] = [];

const propsShape = Email.PreviewSchema._def.shape();
for (const key in propsShape) {
const type = propsShape[key];
if (type && 'typeName' in type._def) {
switch (type._def.typeName) {
case ZodFirstPartyTypeKind.ZodString:
controls.push({
key,
type: (type as ZodString).isEmail ? 'email' : 'text',
});
break;
case ZodFirstPartyTypeKind.ZodNumber:
controls.push({
key,
type: 'number',
});
break;
case ZodFirstPartyTypeKind.ZodBoolean:
controls.push({
key,
type: 'checkbox',
});
break;
}
}
}

return { controls: Email.controls ?? {} };
return { controls };
};
22 changes: 11 additions & 11 deletions packages/react-email/src/actions/get-preview-props.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
'use server';

import { cookies } from 'next/headers';
import type { Controls } from '../package';
import { emailsDirectoryAbsolutePath } from '../utils/emails-directory-absolute-path';
import { cachedGetEmailComponent } from '../utils/cached-get-email-component';

export const getPreviewProps = async (
emailSlug: string,
controls: Controls | undefined,
) => {
export const getPreviewProps = async (emailPath: string) => {
const cookieStore = await cookies();

const emailSlug = emailPath.replace(`${emailsDirectoryAbsolutePath}/`, '');
const previewPropsCoookieName = `preview-props-${emailSlug.replaceAll('/', '-')}`;
const previewPropsCookie = cookieStore.get(previewPropsCoookieName);

let previewProps: Record<string, unknown>;
let previewProps: Record<string, unknown> = {};
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
previewProps = JSON.parse(previewPropsCookie!.value) as Record<
string,
unknown
>;
} catch (exception) {
previewProps = {};
if (controls) {
for (const [key, control] of Object.entries(controls)) {
previewProps[key] = control?.defaultValue;
const componentResult = await cachedGetEmailComponent(emailPath);
if ('emailComponent' in componentResult) {
const { emailComponent: Email } = componentResult;

if (Email.PreviewProps) {
previewProps = Email.PreviewProps;
}
}
}
Expand Down
5 changes: 1 addition & 4 deletions packages/react-email/src/app/preview/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,7 @@ This is most likely not an issue with the preview server. Maybe there was a typo
});
}

const previewProps = await getPreviewProps(
slug,
'controls' in controlsResult ? controlsResult.controls : undefined,
);
const previewProps = await getPreviewProps(emailPath);

const serverEmailRenderingResult = await renderEmailByPath(
emailPath,
Expand Down
22 changes: 11 additions & 11 deletions packages/react-email/src/components/preview-prop-controls.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as Checkbox from '@radix-ui/react-checkbox';
import * as Select from '@radix-ui/react-select';
import type { Controls } from '../package';
import { IconArrowDown } from './icons/icon-arrow-down';
import { IconCheck } from './icons/icon-check';
import type { Control } from '../actions/get-email-controls';

interface PreviewPropControls {
previewProps: Record<string, unknown>;
onValueChange: (key: string, newValue: unknown) => void;
controls: Controls;
controls: Control[];
}

export const PreviewPropControls = ({
Expand All @@ -17,10 +17,10 @@ export const PreviewPropControls = ({
}: PreviewPropControls) => {
return (
<div className="fixed px-3 py-2 border-t border-solid border-t-slate-9 left-0 bottom-0 bg-black w-full grid gap-3 grid-cols-1 md:grid-cols-3 lg:grid-cols-4 h-40">
{Object.entries(controls).map(([key, control]) => {
{controls.map((control) => {
if (control) {
const fieldId = `${key}-${control.type}`;
const value = previewProps[key];
const fieldId = `${control.key}-${control.type}`;
const value = previewProps[control.key];

switch (control.type) {
case 'text':
Expand All @@ -32,14 +32,14 @@ export const PreviewPropControls = ({
className="text-slate-10 text-sm mb-2 block"
htmlFor={fieldId}
>
{key}
{control.key}
</label>
<input
className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-10 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-10 transition duration-300 ease-in-out"
data-1p-ignore
id={fieldId}
onChange={(event) => {
onValueChange(key, event.currentTarget.value);
onValueChange(control.key, event.currentTarget.value);
}}
type={control.type}
value={value as string}
Expand All @@ -53,11 +53,11 @@ export const PreviewPropControls = ({
className="text-slate-10 text-sm mb-2 block"
htmlFor={fieldId}
>
{key}
{control.key}
</label>
<Select.Root
onValueChange={(newValue) => {
onValueChange(key, newValue);
onValueChange(control.key, newValue);
}}
value={value as string}
>
Expand Down Expand Up @@ -101,7 +101,7 @@ export const PreviewPropControls = ({
checked={value as boolean}
id={fieldId}
onCheckedChange={(newValue) => {
onValueChange(key, newValue);
onValueChange(control.key, newValue);
}}
>
<Checkbox.Indicator>
Expand All @@ -112,7 +112,7 @@ export const PreviewPropControls = ({
className="text-slate-10 text-sm mb-2 block"
htmlFor={fieldId}
>
{key}
{control.key}
</label>
</div>
);
Expand Down
1 change: 0 additions & 1 deletion packages/react-email/src/package/index.ts

This file was deleted.

40 changes: 0 additions & 40 deletions packages/react-email/src/package/setup-for-preview.ts

This file was deleted.

5 changes: 3 additions & 2 deletions packages/react-email/src/utils/types/email-template.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Controls } from '../../package';
import type { ZodObject, ZodType, ZodTypeDef } from 'zod';

export interface EmailTemplate {
(props: Record<string, unknown> | Record<string, never>): React.ReactNode;
controls?: Controls;
PreviewProps?: Record<string, unknown>;
PreviewSchema?: ZodObject<Record<string, ZodType<any, ZodTypeDef, any>>>;
}

export const isEmailTemplate = (val: unknown): val is EmailTemplate => {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 012508b

Please sign in to comment.