react-email 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,13 @@ exports.components = [
6
6
  title: 'button.tsx',
7
7
  content: "import * as React from 'react';\nimport classnames from 'classnames';\nimport { unreachable } from '../utils';\nimport * as SlotPrimitive from '@radix-ui/react-slot';\n\ntype ButtonElement = React.ElementRef<'button'>;\ntype RootProps = React.ComponentPropsWithoutRef<'button'>;\n\ntype Appearance = 'white' | 'gradient';\ntype Size = '1' | '2' | '3' | '4';\n\ninterface ButtonProps extends RootProps {\n asChild?: boolean;\n appearance?: Appearance;\n size?: Size;\n}\n\nexport const Button = React.forwardRef<ButtonElement, Readonly<ButtonProps>>(\n (\n {\n asChild,\n appearance = 'white',\n className,\n children,\n size = '2',\n ...props\n },\n forwardedRef,\n ) => {\n const classNames = classnames(\n getSize(size),\n getAppearance(appearance),\n 'inline-flex items-center justify-center border font-medium',\n className,\n );\n\n return asChild ? (\n <SlotPrimitive.Slot ref={forwardedRef} {...props} className={classNames}>\n <SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>\n </SlotPrimitive.Slot>\n ) : (\n <button ref={forwardedRef} className={classNames} {...props}>\n {children}\n </button>\n );\n },\n);\n\nButton.displayName = 'Button';\n\nconst getAppearance = (appearance: Appearance | undefined) => {\n switch (appearance) {\n case undefined:\n case 'white':\n return [\n 'bg-white text-black',\n 'hover:bg-white/90',\n 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-white/90',\n ];\n case 'gradient':\n return [\n 'bg-gradient backdrop-blur-[20px] border-[#34343A]',\n 'hover:bg-gradientHover',\n 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-gradientHover',\n ];\n default:\n unreachable(appearance);\n }\n};\n\nconst getSize = (size: Size | undefined) => {\n switch (size) {\n case '1':\n return '';\n case undefined:\n case '2':\n return 'text-[14px] h-8 px-3 rounded-md gap-2';\n case '3':\n return 'text-[14px] h-10 px-4 rounded-md gap-2';\n case '4':\n return 'text-base h-11 px-4 rounded-md gap-2';\n default:\n unreachable(size);\n }\n};\n",
8
8
  },
9
+ {
10
+ title: 'code-container.tsx',
11
+ content: "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a href={url} download={file.name}>\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"py-[10px] px-4 text-xs flex gap-8\">\n {markups.map(({ language }) => {\n return (\n <div key={language}>\n <button\n className={`${activeTab !== language && 'opacity-25'}`}\n onClick={() => setActiveTab(language)}\n >\n {languageMap[language]}\n </button>\n </div>\n );\n })}\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
12
+ },
9
13
  {
10
14
  title: 'code.tsx',
11
- content: "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport { Tooltip } from './tooltip';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n className,\n language = 'html',\n ...props\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <pre\n className={classnames(\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md',\n className,\n )}\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"py-[10px] px-4 text-xs\">\n {language === 'jsx' ? 'React' : 'HTML'}\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n <IconButton\n onClick={async () => {\n setIsCopied(true);\n await copyTextToClipboard(value);\n setTimeout(() => setIsCopied(false), 3000);\n }}\n >\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n <a href={url} download={file.name}>\n <IconDownload />\n </a>\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </pre>\n )}\n </Highlight>\n );\n};\n",
15
+ content: "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
12
16
  },
13
17
  {
14
18
  title: 'heading.tsx',
@@ -48,11 +52,11 @@ exports.components = [
48
52
  },
49
53
  {
50
54
  title: 'send.tsx',
51
- content: 'import { inter } from \'../pages/_app\';\nimport { Button } from \'./button\';\nimport * as Popover from \'@radix-ui/react-popover\';\nimport * as React from \'react\';\n\nexport const Send = ({ markup }: { markup: string }) => {\n const [to, setTo] = React.useState(\'\');\n const [subject, setSubject] = React.useState(\'Testing React Email\');\n const [isSending, setIsSending] = React.useState(false);\n\n const onFormSubmit = async (e: React.FormEvent) => {\n try {\n e.preventDefault();\n setIsSending(true);\n\n const response = await fetch(\'https://react.email/api/send/test\', {\n method: \'POST\',\n headers: { \'Content-Type\': \'application/json\' },\n body: JSON.stringify({\n to,\n subject,\n html: markup,\n }),\n });\n\n if (response.status === 429) {\n const { error } = await response.json();\n alert(error);\n }\n } catch (e) {\n alert(\'Something went wrong. Please try again.\');\n } finally {\n setIsSending(false);\n }\n };\n\n return (\n <Popover.Root>\n <Popover.Trigger asChild>\n <button className="box-border outline-none self-center w-20 h-5 flex items-center justify-center rounded-lg text-center transition duration-300 ease-in-out border border-slate-6 text-slate-11 text-sm px-4 py-4 hover:border-slate-12 hover:text-slate-12 font-sans">\n Send\n </button>\n </Popover.Trigger>\n <Popover.Anchor />\n <Popover.Portal>\n <Popover.Content\n align="end"\n className={`w-80 -mt-10 p-3 bg-black border border-slate-6 text-slate-11 rounded-lg font-sans ${inter.variable}`}\n >\n <Popover.Close\n aria-label="Close"\n className="absolute right-2 flex items-center justify-center w-6 h-6 text-xs text-slate-11 hover:text-slate-12 transition duration-300 ease-in-out rounded-full"\n >\n ✕\n </Popover.Close>\n <form onSubmit={onFormSubmit} className="mt-1">\n <label\n htmlFor="to"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Recipient\n </label>\n <input\n autoFocus={true}\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setTo(e.target.value)}\n defaultValue={to}\n placeholder="you@example.com"\n type="email"\n id="to"\n required\n />\n <label\n htmlFor="subject"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Subject\n </label>\n <input\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setSubject(e.target.value)}\n defaultValue={subject}\n placeholder="My Email"\n type="text"\n id="subject"\n required\n />\n <input\n type="checkbox"\n className="appearance-none checked:bg-blue-500"\n />\n <div className="flex items-center justify-end">\n <Button\n type="submit"\n disabled={subject.length === 0 || to.length === 0 || isSending}\n className="disabled:bg-slate-11 disabled:border-transparent"\n >\n Send\n </Button>\n </div>\n </form>\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n );\n};\n',
55
+ content: 'import { inter } from \'../pages/_app\';\nimport { Button } from \'./button\';\nimport { Text } from \'./text\';\nimport * as Popover from \'@radix-ui/react-popover\';\nimport * as React from \'react\';\n\nexport const Send = ({ markup }: { markup: string }) => {\n const [to, setTo] = React.useState(\'\');\n const [subject, setSubject] = React.useState(\'Testing React Email\');\n const [isSending, setIsSending] = React.useState(false);\n\n const onFormSubmit = async (e: React.FormEvent) => {\n try {\n e.preventDefault();\n setIsSending(true);\n\n const response = await fetch(\'https://react.email/api/send/test\', {\n method: \'POST\',\n headers: { \'Content-Type\': \'application/json\' },\n body: JSON.stringify({\n to,\n subject,\n html: markup,\n }),\n });\n\n if (response.status === 429) {\n const { error } = await response.json();\n alert(error);\n }\n } catch (e) {\n alert(\'Something went wrong. Please try again.\');\n } finally {\n setIsSending(false);\n }\n };\n\n return (\n <Popover.Root>\n <Popover.Trigger asChild>\n <button className="box-border outline-none self-center w-20 h-5 flex items-center justify-center rounded-lg text-center transition duration-300 ease-in-out border border-slate-6 text-slate-11 text-sm px-4 py-4 hover:border-slate-12 hover:text-slate-12 font-sans">\n Send\n </button>\n </Popover.Trigger>\n <Popover.Anchor />\n <Popover.Portal>\n <Popover.Content\n align="end"\n className={`w-80 -mt-10 p-3 bg-black border border-slate-6 text-slate-11 rounded-lg font-sans ${inter.variable}`}\n >\n <Popover.Close\n aria-label="Close"\n className="absolute right-2 flex items-center justify-center w-6 h-6 text-xs text-slate-11 hover:text-slate-12 transition duration-300 ease-in-out rounded-full"\n >\n ✕\n </Popover.Close>\n <form onSubmit={onFormSubmit} className="mt-1">\n <label\n htmlFor="to"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Recipient\n </label>\n <input\n autoFocus={true}\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setTo(e.target.value)}\n defaultValue={to}\n placeholder="you@example.com"\n type="email"\n id="to"\n required\n />\n <label\n htmlFor="subject"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Subject\n </label>\n <input\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setSubject(e.target.value)}\n defaultValue={subject}\n placeholder="My Email"\n type="text"\n id="subject"\n required\n />\n <input\n type="checkbox"\n className="appearance-none checked:bg-blue-500"\n />\n <div className="flex items-center justify-between">\n <Text className="inline-block" size="1">\n Powered by{\' \'}\n <a\n className="hover:text-slate-12 transition ease-in-out duration-300"\n href="https://resend.com"\n target="_blank"\n rel="noreferrer"\n >\n Resend\n </a>\n </Text>\n <Button\n type="submit"\n disabled={subject.length === 0 || to.length === 0 || isSending}\n className="disabled:bg-slate-11 disabled:border-transparent"\n >\n Send\n </Button>\n </div>\n </form>\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n );\n};\n',
52
56
  },
53
57
  {
54
58
  title: 'sidebar.tsx',
55
- content: 'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center">\n <Heading as="h3" color="white" size="2" weight="medium">\n All emails\n </Heading>\n {navItems && navItems.length > 0 && (\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M12 15L8.5359 9.75L15.4641 9.75L12 15Z"\n fill="currentColor"\n />\n </svg>\n )}\n </div>\n </Collapsible.Trigger>\n\n {navItems && navItems.length > 0 && (\n <Collapsible.Content className="relative mt-3">\n <div className="absolute left-2.5 w-px h-full bg-slate-6" />\n\n <div className="py-2 flex flex-col gap-1.5 truncate">\n {navItems &&\n navItems.map((item) => (\n <Link key={item} href={`/preview/${item}`}>\n <span\n className={classnames(\n \'text-[14px] flex items-center font-medium gap-2 h-8 w-full pl-4 rounded-md text-slate-11\',\n {\n \'bg-cyan-3 text-cyan-11\': query.slug === item,\n \'hover:text-slate-12\': query.slug !== item,\n },\n )}\n >\n {query.slug === item && (\n <div className="h-5 bg-cyan-11 w-px absolute left-2.5" />\n )}\n <svg\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M7.75 19.25H16.25C17.3546 19.25 18.25 18.3546 18.25 17.25V9L14 4.75H7.75C6.64543 4.75 5.75 5.64543 5.75 6.75V17.25C5.75 18.3546 6.64543 19.25 7.75 19.25Z"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M18 9.25H13.75V5"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n {item}\n </span>\n </Link>\n ))}\n </div>\n </Collapsible.Content>\n )}\n </Collapsible.Root>\n </nav>\n </aside>\n );\n },\n);\n\nSidebar.displayName = \'Sidebar\';\n',
59
+ content: 'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center">\n <Heading as="h3" color="white" size="2" weight="medium">\n All emails\n </Heading>\n {navItems && navItems.length > 0 && (\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M12 15L8.5359 9.75L15.4641 9.75L12 15Z"\n fill="currentColor"\n />\n </svg>\n )}\n </div>\n </Collapsible.Trigger>\n\n {navItems && navItems.length > 0 && (\n <Collapsible.Content className="relative mt-3">\n <div className="absolute left-2.5 w-px h-full bg-slate-6" />\n\n <div className="py-2 flex flex-col gap-1.5 truncate">\n {navItems &&\n navItems.map((item) => (\n <Link key={item} href={`/preview/${item}`}>\n <span\n className={classnames(\n \'text-[14px] flex items-center font-medium gap-2 h-8 w-full pl-4 rounded-md text-slate-11\',\n {\n \'bg-cyan-3 text-cyan-11\': query.slug === item,\n \'hover:text-slate-12\': query.slug !== item,\n },\n )}\n >\n {query.slug === item && (\n <div className="h-5 bg-cyan-11 w-px absolute left-2.5" />\n )}\n <svg\n className="flex-shrink-0"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M7.75 19.25H16.25C17.3546 19.25 18.25 18.3546 18.25 17.25V9L14 4.75H7.75C6.64543 4.75 5.75 5.64543 5.75 6.75V17.25C5.75 18.3546 6.64543 19.25 7.75 19.25Z"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M18 9.25H13.75V5"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n {item}\n </span>\n </Link>\n ))}\n </div>\n </Collapsible.Content>\n )}\n </Collapsible.Root>\n </nav>\n </aside>\n );\n },\n);\n\nSidebar.displayName = \'Sidebar\';\n',
56
60
  },
57
61
  {
58
62
  title: 'text.tsx',
@@ -17,6 +17,6 @@ exports.pages = [
17
17
  {
18
18
  dir: 'preview',
19
19
  title: '[slug].tsx',
20
- content: "import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { Code } from '../../components';\nimport Head from 'next/head';\nimport { useRouter } from 'next/router';\n\ninterface PreviewProps {}\n\nexport const CONTENT_DIR = 'emails';\n\nconst getEmails = async () => {\n const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);\n const filenames = await fs.readdir(emailsDirectory);\n const emails = filenames\n .map((file) => file.replace(/\\.(jsx|tsx)$/g, ''))\n .filter((file) => file !== 'components');\n return { emails, filenames };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n const { emails } = await getEmails();\n const paths = emails.map((email) => {\n return { params: { slug: email } };\n });\n return { paths, fallback: true };\n};\n\nexport async function getStaticProps({ params }) {\n try {\n const { emails, filenames } = await getEmails();\n const template = filenames.filter((email) => {\n const [fileName] = email.split('.');\n return params.slug === fileName;\n });\n\n const Email = (await import(`../../../emails/${params.slug}`)).default;\n const markup = render(<Email />, { pretty: true });\n const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;\n const reactMarkup = await fs.readFile(path, {\n encoding: 'utf-8',\n });\n\n return emails\n ? { props: { navItems: emails, slug: params.slug, markup, reactMarkup } }\n : { notFound: true };\n } catch (error) {\n console.error(error);\n return { notFound: true };\n }\n}\n\nconst Preview: React.FC<Readonly<PreviewProps>> = ({\n navItems,\n markup,\n reactMarkup,\n slug,\n}: any) => {\n const title = `${slug} — React Email`;\n const router = useRouter();\n const [viewMode, setViewMode] = React.useState('desktop');\n\n const handleViewMode = (mode: string) => {\n setViewMode(mode);\n\n router.push({\n pathname: router.pathname,\n query: {\n ...router.query,\n view: mode,\n },\n });\n };\n\n React.useEffect(() => {\n if (router.query.view === 'source' || router.query.view === 'desktop') {\n setViewMode(router.query.view);\n }\n }, [router.query.view]);\n\n return (\n <Layout\n navItems={navItems}\n title={slug}\n viewMode={viewMode}\n setViewMode={handleViewMode}\n markup={markup}\n >\n <Head>\n <title>{title}</title>\n </Head>\n {viewMode === 'desktop' ? (\n <iframe\n srcDoc={markup}\n frameBorder=\"0\"\n className=\"w-full h-[calc(100vh_-_70px)]\"\n />\n ) : (\n <div className=\"flex gap-6 mx-auto p-6\">\n <Code language=\"jsx\">{reactMarkup}</Code>\n <Code>{markup}</Code>\n </div>\n )}\n </Layout>\n );\n};\n\nexport default Preview;\n",
20
+ content: "import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { CodeContainer } from '../../components/code-container';\nimport { Code } from '../../components';\nimport Head from 'next/head';\nimport { useRouter } from 'next/router';\n\ninterface PreviewProps {\n navItems: string;\n markup: string;\n reactMarkup: string;\n slug: string;\n}\n\nexport const CONTENT_DIR = 'emails';\n\nconst getEmails = async () => {\n const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);\n const filenames = await fs.readdir(emailsDirectory);\n const emails = filenames\n .map((file) => file.replace(/\\.(jsx|tsx)$/g, ''))\n .filter((file) => file !== 'components');\n return { emails, filenames };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n const { emails } = await getEmails();\n const paths = emails.map((email) => {\n return { params: { slug: email } };\n });\n return { paths, fallback: true };\n};\n\nexport async function getStaticProps({ params }) {\n try {\n const { emails, filenames } = await getEmails();\n const template = filenames.filter((email) => {\n const [fileName] = email.split('.');\n return params.slug === fileName;\n });\n\n const Email = (await import(`../../../emails/${params.slug}`)).default;\n const markup = render(<Email />, { pretty: true });\n const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;\n const reactMarkup = await fs.readFile(path, {\n encoding: 'utf-8',\n });\n\n return emails\n ? { props: { navItems: emails, slug: params.slug, markup, reactMarkup } }\n : { notFound: true };\n } catch (error) {\n console.error(error);\n return { notFound: true };\n }\n}\n\nconst Preview: React.FC<Readonly<PreviewProps>> = ({\n navItems,\n markup,\n reactMarkup,\n slug,\n}: any) => {\n const title = `${slug} — React Email`;\n const router = useRouter();\n const [viewMode, setViewMode] = React.useState('desktop');\n\n const handleViewMode = (mode: string) => {\n setViewMode(mode);\n\n router.push({\n pathname: router.pathname,\n query: {\n ...router.query,\n view: mode,\n },\n });\n };\n\n React.useEffect(() => {\n if (router.query.view === 'source' || router.query.view === 'desktop') {\n setViewMode(router.query.view);\n }\n }, [router.query.view]);\n\n return (\n <Layout\n navItems={navItems}\n title={slug}\n viewMode={viewMode}\n setViewMode={handleViewMode}\n markup={markup}\n >\n <Head>\n <title>{title}</title>\n </Head>\n {viewMode === 'desktop' ? (\n <iframe\n srcDoc={markup}\n frameBorder=\"0\"\n className=\"w-full h-[calc(100vh_-_70px)]\"\n />\n ) : (\n <div className=\"flex gap-6 mx-auto p-6\">\n <CodeContainer\n markups={[\n { language: 'jsx', content: reactMarkup },\n { language: 'markup', content: markup },\n ]}\n />\n </div>\n )}\n </Layout>\n );\n};\n\nexport default Preview;\n",
21
21
  },
22
22
  ];
@@ -6,9 +6,13 @@ exports.root = [
6
6
  title: 'next-env.d.ts',
7
7
  content: '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n',
8
8
  },
9
+ {
10
+ title: 'next.config.js',
11
+ content: "/**\n * @type {import('next').NextConfig}\n */\nconst nextConfig = {\n reactStrictMode: true,\n swcMinify: true,\n};\n\nmodule.exports = nextConfig;\n",
12
+ },
9
13
  {
10
14
  title: 'package.json',
11
- content: '{\n "name": "react-email-preview",\n "version": "0.0.5",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "^1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "^1.0.2",\n "@react-email/render": "0.0.2",\n "classnames": "2.3.2",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
15
+ content: '{\n "name": "react-email-preview",\n "version": "0.0.7",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.2",\n "classnames": "2.3.2",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
12
16
  },
13
17
  {
14
18
  title: 'postcss.config.js',
@@ -14,6 +14,10 @@ exports.utils = [
14
14
  title: 'index.ts',
15
15
  content: "export * from './as';\nexport * from './unreachable';\nexport * from './copy-text-to-clipboard';\n",
16
16
  },
17
+ {
18
+ title: 'language-map.ts',
19
+ content: "const languageMap = {\n jsx: 'React',\n markup: 'HTML',\n};\n\nexport default languageMap;\n",
20
+ },
17
21
  {
18
22
  title: 'unreachable.ts',
19
23
  content: "export const unreachable = (\n condition: never,\n message = `Entered unreachable code. Received '${condition}'.`,\n): never => {\n throw new TypeError(message);\n};\n",
@@ -90,12 +90,12 @@ const createAppFiles = async () => {
90
90
  return fs_1.default.promises.writeFile(location, file.content);
91
91
  });
92
92
  };
93
- const pageCreation = pages_1.pages.map((page) => {
93
+ const pageCreation = pages_1.pages.map(async (page) => {
94
94
  const location = page.dir
95
95
  ? `${utils_1.SRC_PATH}/pages/${page.dir}/${page.title}`
96
96
  : `${utils_1.SRC_PATH}/pages/${page.title}`;
97
97
  if (page.dir) {
98
- (0, utils_1.createDirectory)(`${utils_1.SRC_PATH}/pages/${page.dir}`);
98
+ await (0, utils_1.createDirectory)(`${utils_1.SRC_PATH}/pages/${page.dir}`);
99
99
  }
100
100
  return fs_1.default.promises.writeFile(location, page.content);
101
101
  });
@@ -0,0 +1,2 @@
1
+ import { Options } from '@react-email/render';
2
+ export declare const exportTemplates: (outDir: string, options: Options) => Promise<never>;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.exportTemplates = void 0;
30
+ const glob_1 = require("glob");
31
+ const esbuild_1 = __importDefault(require("esbuild"));
32
+ const tree_node_cli_1 = __importDefault(require("tree-node-cli"));
33
+ const ora_1 = __importDefault(require("ora"));
34
+ const log_symbols_1 = __importDefault(require("log-symbols"));
35
+ const render_1 = require("@react-email/render");
36
+ const fs_1 = require("fs");
37
+ const cpy_1 = __importDefault(require("cpy"));
38
+ const normalize_path_1 = __importDefault(require("normalize-path"));
39
+ const utils_1 = require("../utils");
40
+ /*
41
+ This first builds all the templates using esbuild and then puts the output in the `.js`
42
+ files. Then these `.js` files are imported dynamically and rendered to `.html` files
43
+ using the `render` function.
44
+ */
45
+ const exportTemplates = async (outDir, options) => {
46
+ const spinner = (0, ora_1.default)('Preparing files...\n').start();
47
+ const allTemplates = glob_1.glob.sync((0, normalize_path_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/*.{tsx,jsx}`));
48
+ esbuild_1.default.buildSync({
49
+ bundle: true,
50
+ entryPoints: allTemplates,
51
+ platform: 'node',
52
+ write: true,
53
+ outdir: outDir,
54
+ });
55
+ const allBuiltTemplates = glob_1.glob.sync((0, normalize_path_1.default)(`${outDir}/*.js`), {
56
+ absolute: true,
57
+ });
58
+ for (const template of allBuiltTemplates) {
59
+ const component = await Promise.resolve().then(() => __importStar(require(template)));
60
+ const rendered = (0, render_1.render)(component.default(), options);
61
+ const htmlPath = template.replace('.js', options.plainText ? '.txt' : '.html');
62
+ (0, fs_1.writeFileSync)(htmlPath, rendered);
63
+ (0, fs_1.unlinkSync)(template);
64
+ }
65
+ const hasStaticDirectory = (0, utils_1.checkDirectoryExist)(`${utils_1.CLIENT_EMAILS_PATH}/static`);
66
+ if (hasStaticDirectory) {
67
+ await (0, cpy_1.default)(`${utils_1.CLIENT_EMAILS_PATH}/static`, `${outDir}/static`);
68
+ }
69
+ const fileTree = (0, tree_node_cli_1.default)(outDir, {
70
+ allFiles: true,
71
+ maxDepth: 4,
72
+ });
73
+ console.log(fileTree);
74
+ spinner.stopAndPersist({
75
+ symbol: log_symbols_1.default.success,
76
+ text: 'Successfully exported emails',
77
+ });
78
+ process.exit();
79
+ };
80
+ exports.exportTemplates = exportTemplates;
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const extra_typings_1 = require("@commander-js/extra-typings");
5
5
  const constants_1 = require("./utils/constants");
6
6
  const dev_1 = require("./commands/dev");
7
- const exportTemplates_1 = require("./commands/exportTemplates");
7
+ const export_1 = require("./commands/export");
8
8
  extra_typings_1.program
9
9
  .name(constants_1.PACKAGE_NAME)
10
10
  .description('A live preview of your emails right in your browser')
@@ -18,5 +18,6 @@ extra_typings_1.program
18
18
  .description('Build the templates to the `out` directory')
19
19
  .option('--outDir <path>', 'Output directory', 'out')
20
20
  .option('-p, --pretty', 'Pretty print the output', false)
21
- .action(({ outDir, pretty }) => (0, exportTemplates_1.exportTemplates)(outDir, pretty));
21
+ .option('-t, --plainText', 'Set output format as plain Text', false)
22
+ .action(({ outDir, pretty, plainText }) => (0, export_1.exportTemplates)(outDir, { pretty, plainText }));
22
23
  extra_typings_1.program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/index.js"
@@ -18,7 +18,7 @@
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/zenorocha/react-email.git",
21
+ "url": "https://github.com/resendlabs/react-email.git",
22
22
  "directory": "packages/react-email"
23
23
  },
24
24
  "keywords": [
@@ -26,11 +26,11 @@
26
26
  "email"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=18.0.0"
29
+ "node": ">=16.0.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "@commander-js/extra-typings": "9.4.1",
33
- "@react-email/render": "0.0.3",
33
+ "@react-email/render": "0.0.4",
34
34
  "chokidar": "3.5.3",
35
35
  "commander": "9.4.1",
36
36
  "cpy": "8.1.2",
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @type {import('next').NextConfig}
3
+ */
4
+ const nextConfig = {
5
+ reactStrictMode: true,
6
+ swcMinify: true,
7
+ };
8
+
9
+ module.exports = nextConfig;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-preview",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "The React Email preview application",
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -0,0 +1,114 @@
1
+ import { Language } from 'prism-react-renderer';
2
+ import { IconButton } from './icon-button';
3
+ import { IconClipboard } from './icon-clipboard';
4
+ import { IconDownload } from './icon-download';
5
+ import { IconCheck } from './icon-check';
6
+ import { copyTextToClipboard } from '../utils';
7
+ import languageMap from '../utils/language-map';
8
+ import { Tooltip } from './tooltip';
9
+ import { Code } from './code';
10
+ import * as React from 'react';
11
+
12
+ interface CodeContainerProps {
13
+ markups: MarkupProps[];
14
+ }
15
+
16
+ interface MarkupProps {
17
+ language: Language;
18
+ content: string;
19
+ }
20
+
21
+ export const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({
22
+ markups,
23
+ }) => {
24
+ const [isCopied, setIsCopied] = React.useState(false);
25
+ const [activeTab, setActiveTab] = React.useState(markups[0].language);
26
+ let file = null;
27
+ let url = null;
28
+
29
+ const renderDownloadIcon = () => {
30
+ let value = markups.filter((markup) => markup.language === activeTab);
31
+ file = new File([value[0].content], `email.${value[0].language}`);
32
+ url = URL.createObjectURL(file);
33
+
34
+ return (
35
+ <a href={url} download={file.name}>
36
+ <IconDownload />
37
+ </a>
38
+ );
39
+ };
40
+
41
+ const renderClipboardIcon = () => {
42
+ const handleClipboard = async () => {
43
+ const activeContent = markups.filter(({ language }) => {
44
+ return activeTab === language;
45
+ });
46
+ setIsCopied(true);
47
+ await copyTextToClipboard(activeContent[0].content);
48
+ setTimeout(() => setIsCopied(false), 3000);
49
+ };
50
+
51
+ return (
52
+ <IconButton onClick={handleClipboard}>
53
+ {isCopied ? <IconCheck /> : <IconClipboard />}
54
+ </IconButton>
55
+ );
56
+ };
57
+
58
+ React.useEffect(() => {
59
+ setIsCopied(false);
60
+ }, [activeTab]);
61
+
62
+ return (
63
+ <pre
64
+ className={
65
+ 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'
66
+ }
67
+ style={{
68
+ lineHeight: '130%',
69
+ background:
70
+ 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',
71
+ boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',
72
+ }}
73
+ >
74
+ <div className="h-9 border-b border-slate-6">
75
+ <div className="py-[10px] px-4 text-xs flex gap-8">
76
+ {markups.map(({ language }) => {
77
+ return (
78
+ <div key={language}>
79
+ <button
80
+ className={`${activeTab !== language && 'opacity-25'}`}
81
+ onClick={() => setActiveTab(language)}
82
+ >
83
+ {languageMap[language]}
84
+ </button>
85
+ </div>
86
+ );
87
+ })}
88
+ </div>
89
+ <Tooltip>
90
+ <Tooltip.Trigger className="absolute top-2 right-2 hidden md:block">
91
+ {renderClipboardIcon()}
92
+ </Tooltip.Trigger>
93
+ <Tooltip.Content>Copy to Clipboard</Tooltip.Content>
94
+ </Tooltip>
95
+ <Tooltip>
96
+ <Tooltip.Trigger className="text-gray-11 absolute top-2 right-8 hidden md:block">
97
+ {renderDownloadIcon()}
98
+ </Tooltip.Trigger>
99
+ <Tooltip.Content>Download</Tooltip.Content>
100
+ </Tooltip>
101
+ </div>
102
+ {markups.map(({ language, content }) => {
103
+ return (
104
+ <div
105
+ className={`${activeTab !== language && 'hidden'}`}
106
+ key={language}
107
+ >
108
+ <Code language={language}>{content}</Code>
109
+ </div>
110
+ );
111
+ })}
112
+ </pre>
113
+ );
114
+ };
@@ -1,11 +1,5 @@
1
1
  import classnames from 'classnames';
2
2
  import Highlight, { defaultProps, Language } from 'prism-react-renderer';
3
- import { IconButton } from './icon-button';
4
- import { IconClipboard } from './icon-clipboard';
5
- import { IconDownload } from './icon-download';
6
- import { IconCheck } from './icon-check';
7
- import { copyTextToClipboard } from '../utils';
8
- import { Tooltip } from './tooltip';
9
3
  import * as React from 'react';
10
4
 
11
5
  interface CodeProps {
@@ -50,9 +44,7 @@ const theme = {
50
44
 
51
45
  export const Code: React.FC<Readonly<CodeProps>> = ({
52
46
  children,
53
- className,
54
47
  language = 'html',
55
- ...props
56
48
  }) => {
57
49
  const [isCopied, setIsCopied] = React.useState(false);
58
50
  const value = children.trim();
@@ -68,47 +60,7 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
68
60
  language={language as Language}
69
61
  >
70
62
  {({ tokens, getLineProps, getTokenProps }) => (
71
- <pre
72
- className={classnames(
73
- 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md',
74
- className,
75
- )}
76
- style={{
77
- lineHeight: '130%',
78
- background:
79
- 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',
80
- boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',
81
- }}
82
- >
83
- <div className="h-9 border-b border-slate-6">
84
- <div className="py-[10px] px-4 text-xs">
85
- {language === 'jsx' ? 'React' : 'HTML'}
86
- </div>
87
- <Tooltip>
88
- <Tooltip.Trigger className="absolute top-2 right-2 hidden md:block">
89
- <IconButton
90
- onClick={async () => {
91
- setIsCopied(true);
92
- await copyTextToClipboard(value);
93
- setTimeout(() => setIsCopied(false), 3000);
94
- }}
95
- >
96
- {isCopied ? <IconCheck /> : <IconClipboard />}
97
- </IconButton>
98
- </Tooltip.Trigger>
99
- <Tooltip.Content>Copy to Clipboard</Tooltip.Content>
100
- </Tooltip>
101
-
102
- <Tooltip>
103
- <Tooltip.Trigger className="text-gray-11 absolute top-2 right-8 hidden md:block">
104
- <a href={url} download={file.name}>
105
- <IconDownload />
106
- </a>
107
- </Tooltip.Trigger>
108
- <Tooltip.Content>Download</Tooltip.Content>
109
- </Tooltip>
110
- </div>
111
-
63
+ <>
112
64
  <div
113
65
  className="absolute right-0 top-0 h-px w-[200px]"
114
66
  style={{
@@ -153,7 +105,7 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
153
105
  'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',
154
106
  }}
155
107
  />
156
- </pre>
108
+ </>
157
109
  )}
158
110
  </Highlight>
159
111
  );
@@ -1,5 +1,6 @@
1
1
  import { inter } from '../pages/_app';
2
2
  import { Button } from './button';
3
+ import { Text } from './text';
3
4
  import * as Popover from '@radix-ui/react-popover';
4
5
  import * as React from 'react';
5
6
 
@@ -89,7 +90,18 @@ export const Send = ({ markup }: { markup: string }) => {
89
90
  type="checkbox"
90
91
  className="appearance-none checked:bg-blue-500"
91
92
  />
92
- <div className="flex items-center justify-end">
93
+ <div className="flex items-center justify-between">
94
+ <Text className="inline-block" size="1">
95
+ Powered by{' '}
96
+ <a
97
+ className="hover:text-slate-12 transition ease-in-out duration-300"
98
+ href="https://resend.com"
99
+ target="_blank"
100
+ rel="noreferrer"
101
+ >
102
+ Resend
103
+ </a>
104
+ </Text>
93
105
  <Button
94
106
  type="submit"
95
107
  disabled={subject.length === 0 || to.length === 0 || isSending}
@@ -4,11 +4,17 @@ import path from 'path';
4
4
  import { render } from '@react-email/render';
5
5
  import { GetStaticPaths } from 'next';
6
6
  import { Layout } from '../../components/layout';
7
+ import { CodeContainer } from '../../components/code-container';
7
8
  import { Code } from '../../components';
8
9
  import Head from 'next/head';
9
10
  import { useRouter } from 'next/router';
10
11
 
11
- interface PreviewProps {}
12
+ interface PreviewProps {
13
+ navItems: string;
14
+ markup: string;
15
+ reactMarkup: string;
16
+ slug: string;
17
+ }
12
18
 
13
19
  export const CONTENT_DIR = 'emails';
14
20
 
@@ -100,8 +106,12 @@ const Preview: React.FC<Readonly<PreviewProps>> = ({
100
106
  />
101
107
  ) : (
102
108
  <div className="flex gap-6 mx-auto p-6">
103
- <Code language="jsx">{reactMarkup}</Code>
104
- <Code>{markup}</Code>
109
+ <CodeContainer
110
+ markups={[
111
+ { language: 'jsx', content: reactMarkup },
112
+ { language: 'markup', content: markup },
113
+ ]}
114
+ />
105
115
  </div>
106
116
  )}
107
117
  </Layout>
@@ -0,0 +1,6 @@
1
+ const languageMap = {
2
+ jsx: 'React',
3
+ markup: 'HTML',
4
+ };
5
+
6
+ export default languageMap;
package/readme.md CHANGED
@@ -6,7 +6,7 @@
6
6
  <div align="center">
7
7
  <a href="https://react.email">Website</a>
8
8
  <span> · </span>
9
- <a href="https://github.com/zenorocha/react-email">GitHub</a>
9
+ <a href="https://github.com/resendlabs/react-email">GitHub</a>
10
10
  <span> · </span>
11
11
  <a href="https://react.email/discord">Discord</a>
12
12
  </div>
@@ -4,10 +4,15 @@ export const components = [
4
4
  content:
5
5
  "import * as React from 'react';\nimport classnames from 'classnames';\nimport { unreachable } from '../utils';\nimport * as SlotPrimitive from '@radix-ui/react-slot';\n\ntype ButtonElement = React.ElementRef<'button'>;\ntype RootProps = React.ComponentPropsWithoutRef<'button'>;\n\ntype Appearance = 'white' | 'gradient';\ntype Size = '1' | '2' | '3' | '4';\n\ninterface ButtonProps extends RootProps {\n asChild?: boolean;\n appearance?: Appearance;\n size?: Size;\n}\n\nexport const Button = React.forwardRef<ButtonElement, Readonly<ButtonProps>>(\n (\n {\n asChild,\n appearance = 'white',\n className,\n children,\n size = '2',\n ...props\n },\n forwardedRef,\n ) => {\n const classNames = classnames(\n getSize(size),\n getAppearance(appearance),\n 'inline-flex items-center justify-center border font-medium',\n className,\n );\n\n return asChild ? (\n <SlotPrimitive.Slot ref={forwardedRef} {...props} className={classNames}>\n <SlotPrimitive.Slottable>{children}</SlotPrimitive.Slottable>\n </SlotPrimitive.Slot>\n ) : (\n <button ref={forwardedRef} className={classNames} {...props}>\n {children}\n </button>\n );\n },\n);\n\nButton.displayName = 'Button';\n\nconst getAppearance = (appearance: Appearance | undefined) => {\n switch (appearance) {\n case undefined:\n case 'white':\n return [\n 'bg-white text-black',\n 'hover:bg-white/90',\n 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-white/90',\n ];\n case 'gradient':\n return [\n 'bg-gradient backdrop-blur-[20px] border-[#34343A]',\n 'hover:bg-gradientHover',\n 'focus:ring-2 focus:ring-white/20 focus:outline-none focus:bg-gradientHover',\n ];\n default:\n unreachable(appearance);\n }\n};\n\nconst getSize = (size: Size | undefined) => {\n switch (size) {\n case '1':\n return '';\n case undefined:\n case '2':\n return 'text-[14px] h-8 px-3 rounded-md gap-2';\n case '3':\n return 'text-[14px] h-10 px-4 rounded-md gap-2';\n case '4':\n return 'text-base h-11 px-4 rounded-md gap-2';\n default:\n unreachable(size);\n }\n};\n",
6
6
  },
7
+ {
8
+ title: 'code-container.tsx',
9
+ content:
10
+ "import { Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport languageMap from '../utils/language-map';\nimport { Tooltip } from './tooltip';\nimport { Code } from './code';\nimport * as React from 'react';\n\ninterface CodeContainerProps {\n markups: MarkupProps[];\n}\n\ninterface MarkupProps {\n language: Language;\n content: string;\n}\n\nexport const CodeContainer: React.FC<Readonly<CodeContainerProps>> = ({\n markups,\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const [activeTab, setActiveTab] = React.useState(markups[0].language);\n let file = null;\n let url = null;\n\n const renderDownloadIcon = () => {\n let value = markups.filter((markup) => markup.language === activeTab);\n file = new File([value[0].content], `email.${value[0].language}`);\n url = URL.createObjectURL(file);\n\n return (\n <a href={url} download={file.name}>\n <IconDownload />\n </a>\n );\n };\n\n const renderClipboardIcon = () => {\n const handleClipboard = async () => {\n const activeContent = markups.filter(({ language }) => {\n return activeTab === language;\n });\n setIsCopied(true);\n await copyTextToClipboard(activeContent[0].content);\n setTimeout(() => setIsCopied(false), 3000);\n };\n\n return (\n <IconButton onClick={handleClipboard}>\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n );\n };\n\n React.useEffect(() => {\n setIsCopied(false);\n }, [activeTab]);\n\n return (\n <pre\n className={\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md'\n }\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"py-[10px] px-4 text-xs flex gap-8\">\n {markups.map(({ language }) => {\n return (\n <div key={language}>\n <button\n className={`${activeTab !== language && 'opacity-25'}`}\n onClick={() => setActiveTab(language)}\n >\n {languageMap[language]}\n </button>\n </div>\n );\n })}\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n {renderClipboardIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n {renderDownloadIcon()}\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n {markups.map(({ language, content }) => {\n return (\n <div\n className={`${activeTab !== language && 'hidden'}`}\n key={language}\n >\n <Code language={language}>{content}</Code>\n </div>\n );\n })}\n </pre>\n );\n};\n",
11
+ },
7
12
  {
8
13
  title: 'code.tsx',
9
14
  content:
10
- "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport { IconButton } from './icon-button';\nimport { IconClipboard } from './icon-clipboard';\nimport { IconDownload } from './icon-download';\nimport { IconCheck } from './icon-check';\nimport { copyTextToClipboard } from '../utils';\nimport { Tooltip } from './tooltip';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n className,\n language = 'html',\n ...props\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <pre\n className={classnames(\n 'border-slate-6 relative w-full items-center overflow-auto whitespace-pre rounded-md border text-sm backdrop-blur-md',\n className,\n )}\n style={{\n lineHeight: '130%',\n background:\n 'linear-gradient(145.37deg, rgba(255, 255, 255, 0.09) -8.75%, rgba(255, 255, 255, 0.027) 83.95%)',\n boxShadow: 'rgb(0 0 0 / 10%) 0px 5px 30px -5px',\n }}\n >\n <div className=\"h-9 border-b border-slate-6\">\n <div className=\"py-[10px] px-4 text-xs\">\n {language === 'jsx' ? 'React' : 'HTML'}\n </div>\n <Tooltip>\n <Tooltip.Trigger className=\"absolute top-2 right-2 hidden md:block\">\n <IconButton\n onClick={async () => {\n setIsCopied(true);\n await copyTextToClipboard(value);\n setTimeout(() => setIsCopied(false), 3000);\n }}\n >\n {isCopied ? <IconCheck /> : <IconClipboard />}\n </IconButton>\n </Tooltip.Trigger>\n <Tooltip.Content>Copy to Clipboard</Tooltip.Content>\n </Tooltip>\n\n <Tooltip>\n <Tooltip.Trigger className=\"text-gray-11 absolute top-2 right-8 hidden md:block\">\n <a href={url} download={file.name}>\n <IconDownload />\n </a>\n </Tooltip.Trigger>\n <Tooltip.Content>Download</Tooltip.Content>\n </Tooltip>\n </div>\n\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </pre>\n )}\n </Highlight>\n );\n};\n",
15
+ "import classnames from 'classnames';\nimport Highlight, { defaultProps, Language } from 'prism-react-renderer';\nimport * as React from 'react';\n\ninterface CodeProps {\n children: any;\n className?: string;\n language?: Language;\n}\n\nconst theme = {\n plain: {\n color: '#EDEDEF',\n fontSize: 13,\n fontFamily: 'MonoLisa, Menlo, monospace',\n },\n styles: [\n {\n types: ['comment'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['atrule', 'keyword', 'attr-name', 'selector'],\n style: {\n color: '#7E7D86',\n },\n },\n {\n types: ['punctuation', 'operator'],\n style: {\n color: '#706F78',\n },\n },\n {\n types: ['class-name', 'function', 'tag', 'key-white'],\n style: {\n color: '#EDEDEF',\n },\n },\n ],\n};\n\nexport const Code: React.FC<Readonly<CodeProps>> = ({\n children,\n language = 'html',\n}) => {\n const [isCopied, setIsCopied] = React.useState(false);\n const value = children.trim();\n\n const file = new File([value], `email.${language}`);\n const url = URL.createObjectURL(file);\n\n return (\n <Highlight\n {...defaultProps}\n theme={theme}\n code={value}\n language={language as Language}\n >\n {({ tokens, getLineProps, getTokenProps }) => (\n <>\n <div\n className=\"absolute right-0 top-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n <div className=\"p-4\">\n {tokens.map((line, i) => {\n return (\n <div\n key={i}\n {...getLineProps({ line, key: i })}\n className={classnames('whitespace-pre', {\n \"before:text-slate-11 before:mr-2 before:content-['$']\":\n language === 'bash' && tokens && tokens.length === 1,\n })}\n >\n {line.map((token, key) => {\n const isException =\n token.content === 'from' &&\n line[key + 1]?.content === ':';\n const newTypes = isException\n ? [...token.types, 'key-white']\n : token.types;\n token.types = newTypes;\n\n return (\n <React.Fragment key={key}>\n <span {...getTokenProps({ token, key })} />\n </React.Fragment>\n );\n })}\n </div>\n );\n })}\n </div>\n <div\n className=\"absolute left-0 bottom-0 h-px w-[200px]\"\n style={{\n background:\n 'linear-gradient(90deg, rgba(56, 189, 248, 0) 0%, rgba(56, 189, 248, 0) 0%, rgba(232, 232, 232, 0.2) 33.02%, rgba(143, 143, 143, 0.6719) 64.41%, rgba(236, 72, 153, 0) 98.93%)',\n }}\n />\n </>\n )}\n </Highlight>\n );\n};\n",
11
16
  },
12
17
  {
13
18
  title: 'heading.tsx',
@@ -57,12 +62,12 @@ export const components = [
57
62
  {
58
63
  title: 'send.tsx',
59
64
  content:
60
- 'import { inter } from \'../pages/_app\';\nimport { Button } from \'./button\';\nimport * as Popover from \'@radix-ui/react-popover\';\nimport * as React from \'react\';\n\nexport const Send = ({ markup }: { markup: string }) => {\n const [to, setTo] = React.useState(\'\');\n const [subject, setSubject] = React.useState(\'Testing React Email\');\n const [isSending, setIsSending] = React.useState(false);\n\n const onFormSubmit = async (e: React.FormEvent) => {\n try {\n e.preventDefault();\n setIsSending(true);\n\n const response = await fetch(\'https://react.email/api/send/test\', {\n method: \'POST\',\n headers: { \'Content-Type\': \'application/json\' },\n body: JSON.stringify({\n to,\n subject,\n html: markup,\n }),\n });\n\n if (response.status === 429) {\n const { error } = await response.json();\n alert(error);\n }\n } catch (e) {\n alert(\'Something went wrong. Please try again.\');\n } finally {\n setIsSending(false);\n }\n };\n\n return (\n <Popover.Root>\n <Popover.Trigger asChild>\n <button className="box-border outline-none self-center w-20 h-5 flex items-center justify-center rounded-lg text-center transition duration-300 ease-in-out border border-slate-6 text-slate-11 text-sm px-4 py-4 hover:border-slate-12 hover:text-slate-12 font-sans">\n Send\n </button>\n </Popover.Trigger>\n <Popover.Anchor />\n <Popover.Portal>\n <Popover.Content\n align="end"\n className={`w-80 -mt-10 p-3 bg-black border border-slate-6 text-slate-11 rounded-lg font-sans ${inter.variable}`}\n >\n <Popover.Close\n aria-label="Close"\n className="absolute right-2 flex items-center justify-center w-6 h-6 text-xs text-slate-11 hover:text-slate-12 transition duration-300 ease-in-out rounded-full"\n >\n ✕\n </Popover.Close>\n <form onSubmit={onFormSubmit} className="mt-1">\n <label\n htmlFor="to"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Recipient\n </label>\n <input\n autoFocus={true}\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setTo(e.target.value)}\n defaultValue={to}\n placeholder="you@example.com"\n type="email"\n id="to"\n required\n />\n <label\n htmlFor="subject"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Subject\n </label>\n <input\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setSubject(e.target.value)}\n defaultValue={subject}\n placeholder="My Email"\n type="text"\n id="subject"\n required\n />\n <input\n type="checkbox"\n className="appearance-none checked:bg-blue-500"\n />\n <div className="flex items-center justify-end">\n <Button\n type="submit"\n disabled={subject.length === 0 || to.length === 0 || isSending}\n className="disabled:bg-slate-11 disabled:border-transparent"\n >\n Send\n </Button>\n </div>\n </form>\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n );\n};\n',
65
+ 'import { inter } from \'../pages/_app\';\nimport { Button } from \'./button\';\nimport { Text } from \'./text\';\nimport * as Popover from \'@radix-ui/react-popover\';\nimport * as React from \'react\';\n\nexport const Send = ({ markup }: { markup: string }) => {\n const [to, setTo] = React.useState(\'\');\n const [subject, setSubject] = React.useState(\'Testing React Email\');\n const [isSending, setIsSending] = React.useState(false);\n\n const onFormSubmit = async (e: React.FormEvent) => {\n try {\n e.preventDefault();\n setIsSending(true);\n\n const response = await fetch(\'https://react.email/api/send/test\', {\n method: \'POST\',\n headers: { \'Content-Type\': \'application/json\' },\n body: JSON.stringify({\n to,\n subject,\n html: markup,\n }),\n });\n\n if (response.status === 429) {\n const { error } = await response.json();\n alert(error);\n }\n } catch (e) {\n alert(\'Something went wrong. Please try again.\');\n } finally {\n setIsSending(false);\n }\n };\n\n return (\n <Popover.Root>\n <Popover.Trigger asChild>\n <button className="box-border outline-none self-center w-20 h-5 flex items-center justify-center rounded-lg text-center transition duration-300 ease-in-out border border-slate-6 text-slate-11 text-sm px-4 py-4 hover:border-slate-12 hover:text-slate-12 font-sans">\n Send\n </button>\n </Popover.Trigger>\n <Popover.Anchor />\n <Popover.Portal>\n <Popover.Content\n align="end"\n className={`w-80 -mt-10 p-3 bg-black border border-slate-6 text-slate-11 rounded-lg font-sans ${inter.variable}`}\n >\n <Popover.Close\n aria-label="Close"\n className="absolute right-2 flex items-center justify-center w-6 h-6 text-xs text-slate-11 hover:text-slate-12 transition duration-300 ease-in-out rounded-full"\n >\n ✕\n </Popover.Close>\n <form onSubmit={onFormSubmit} className="mt-1">\n <label\n htmlFor="to"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Recipient\n </label>\n <input\n autoFocus={true}\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setTo(e.target.value)}\n defaultValue={to}\n placeholder="you@example.com"\n type="email"\n id="to"\n required\n />\n <label\n htmlFor="subject"\n className="text-slate-10 text-xs uppercase mb-2 block"\n >\n Subject\n </label>\n <input\n className="appearance-none rounded-lg px-2 py-1 mb-3 outline-none w-full bg-slate-3 border placeholder-slate-8 border-slate-6 text-slate-12 text-sm focus:ring-1 focus:ring-slate-12 transition duration-300 ease-in-out"\n onChange={(e) => setSubject(e.target.value)}\n defaultValue={subject}\n placeholder="My Email"\n type="text"\n id="subject"\n required\n />\n <input\n type="checkbox"\n className="appearance-none checked:bg-blue-500"\n />\n <div className="flex items-center justify-between">\n <Text className="inline-block" size="1">\n Powered by{\' \'}\n <a\n className="hover:text-slate-12 transition ease-in-out duration-300"\n href="https://resend.com"\n target="_blank"\n rel="noreferrer"\n >\n Resend\n </a>\n </Text>\n <Button\n type="submit"\n disabled={subject.length === 0 || to.length === 0 || isSending}\n className="disabled:bg-slate-11 disabled:border-transparent"\n >\n Send\n </Button>\n </div>\n </form>\n </Popover.Content>\n </Popover.Portal>\n </Popover.Root>\n );\n};\n',
61
66
  },
62
67
  {
63
68
  title: 'sidebar.tsx',
64
69
  content:
65
- 'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center">\n <Heading as="h3" color="white" size="2" weight="medium">\n All emails\n </Heading>\n {navItems && navItems.length > 0 && (\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M12 15L8.5359 9.75L15.4641 9.75L12 15Z"\n fill="currentColor"\n />\n </svg>\n )}\n </div>\n </Collapsible.Trigger>\n\n {navItems && navItems.length > 0 && (\n <Collapsible.Content className="relative mt-3">\n <div className="absolute left-2.5 w-px h-full bg-slate-6" />\n\n <div className="py-2 flex flex-col gap-1.5 truncate">\n {navItems &&\n navItems.map((item) => (\n <Link key={item} href={`/preview/${item}`}>\n <span\n className={classnames(\n \'text-[14px] flex items-center font-medium gap-2 h-8 w-full pl-4 rounded-md text-slate-11\',\n {\n \'bg-cyan-3 text-cyan-11\': query.slug === item,\n \'hover:text-slate-12\': query.slug !== item,\n },\n )}\n >\n {query.slug === item && (\n <div className="h-5 bg-cyan-11 w-px absolute left-2.5" />\n )}\n <svg\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M7.75 19.25H16.25C17.3546 19.25 18.25 18.3546 18.25 17.25V9L14 4.75H7.75C6.64543 4.75 5.75 5.64543 5.75 6.75V17.25C5.75 18.3546 6.64543 19.25 7.75 19.25Z"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M18 9.25H13.75V5"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n {item}\n </span>\n </Link>\n ))}\n </div>\n </Collapsible.Content>\n )}\n </Collapsible.Root>\n </nav>\n </aside>\n );\n },\n);\n\nSidebar.displayName = \'Sidebar\';\n',
70
+ 'import { Logo } from \'./logo\';\nimport * as React from \'react\';\nimport classnames from \'classnames\';\nimport Link from \'next/link\';\nimport { Heading } from \'./heading\';\nimport { useRouter } from \'next/router\';\nimport * as Collapsible from \'@radix-ui/react-collapsible\';\n\ntype SidebarElement = React.ElementRef<\'aside\'>;\ntype RootProps = React.ComponentPropsWithoutRef<\'aside\'>;\n\ninterface SidebarProps extends RootProps {\n navItems: string[];\n}\n\nexport const Sidebar = React.forwardRef<SidebarElement, Readonly<SidebarProps>>(\n ({ className, navItems, ...props }, forwardedRef) => {\n const { query } = useRouter();\n\n return (\n <aside\n ref={forwardedRef}\n className="px-6 min-w-[275px] max-w-[275px] flex flex-col gap-4 border-r border-slate-6"\n {...props}\n >\n <div className="h-[70px] flex items-center">\n <Logo />\n </div>\n\n <nav className="flex flex-col gap-4">\n <Collapsible.Root defaultOpen>\n <Collapsible.Trigger\n className={classnames(\'flex items-center gap-1\', {\n \'cursor-default\': navItems && navItems.length === 0,\n })}\n >\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M19.25 17.25V9.75C19.25 8.64543 18.3546 7.75 17.25 7.75H4.75V17.25C4.75 18.3546 5.64543 19.25 6.75 19.25H17.25C18.3546 19.25 19.25 18.3546 19.25 17.25Z"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M13.5 7.5L12.5685 5.7923C12.2181 5.14977 11.5446 4.75 10.8127 4.75H6.75C5.64543 4.75 4.75 5.64543 4.75 6.75V11"\n stroke="currentColor"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n\n <div className="flex items-center">\n <Heading as="h3" color="white" size="2" weight="medium">\n All emails\n </Heading>\n {navItems && navItems.length > 0 && (\n <svg\n className="text-slate-11"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M12 15L8.5359 9.75L15.4641 9.75L12 15Z"\n fill="currentColor"\n />\n </svg>\n )}\n </div>\n </Collapsible.Trigger>\n\n {navItems && navItems.length > 0 && (\n <Collapsible.Content className="relative mt-3">\n <div className="absolute left-2.5 w-px h-full bg-slate-6" />\n\n <div className="py-2 flex flex-col gap-1.5 truncate">\n {navItems &&\n navItems.map((item) => (\n <Link key={item} href={`/preview/${item}`}>\n <span\n className={classnames(\n \'text-[14px] flex items-center font-medium gap-2 h-8 w-full pl-4 rounded-md text-slate-11\',\n {\n \'bg-cyan-3 text-cyan-11\': query.slug === item,\n \'hover:text-slate-12\': query.slug !== item,\n },\n )}\n >\n {query.slug === item && (\n <div className="h-5 bg-cyan-11 w-px absolute left-2.5" />\n )}\n <svg\n className="flex-shrink-0"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M7.75 19.25H16.25C17.3546 19.25 18.25 18.3546 18.25 17.25V9L14 4.75H7.75C6.64543 4.75 5.75 5.64543 5.75 6.75V17.25C5.75 18.3546 6.64543 19.25 7.75 19.25Z"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n <path\n d="M18 9.25H13.75V5"\n stroke="currentColor"\n strokeOpacity="0.927"\n strokeWidth="1.5"\n strokeLinecap="round"\n strokeLinejoin="round"\n />\n </svg>\n {item}\n </span>\n </Link>\n ))}\n </div>\n </Collapsible.Content>\n )}\n </Collapsible.Root>\n </nav>\n </aside>\n );\n },\n);\n\nSidebar.displayName = \'Sidebar\';\n',
66
71
  },
67
72
  {
68
73
  title: 'text.tsx',
@@ -18,6 +18,6 @@ export const pages = [
18
18
  dir: 'preview',
19
19
  title: '[slug].tsx',
20
20
  content:
21
- "import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { Code } from '../../components';\nimport Head from 'next/head';\nimport { useRouter } from 'next/router';\n\ninterface PreviewProps {}\n\nexport const CONTENT_DIR = 'emails';\n\nconst getEmails = async () => {\n const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);\n const filenames = await fs.readdir(emailsDirectory);\n const emails = filenames\n .map((file) => file.replace(/\\.(jsx|tsx)$/g, ''))\n .filter((file) => file !== 'components');\n return { emails, filenames };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n const { emails } = await getEmails();\n const paths = emails.map((email) => {\n return { params: { slug: email } };\n });\n return { paths, fallback: true };\n};\n\nexport async function getStaticProps({ params }) {\n try {\n const { emails, filenames } = await getEmails();\n const template = filenames.filter((email) => {\n const [fileName] = email.split('.');\n return params.slug === fileName;\n });\n\n const Email = (await import(`../../../emails/${params.slug}`)).default;\n const markup = render(<Email />, { pretty: true });\n const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;\n const reactMarkup = await fs.readFile(path, {\n encoding: 'utf-8',\n });\n\n return emails\n ? { props: { navItems: emails, slug: params.slug, markup, reactMarkup } }\n : { notFound: true };\n } catch (error) {\n console.error(error);\n return { notFound: true };\n }\n}\n\nconst Preview: React.FC<Readonly<PreviewProps>> = ({\n navItems,\n markup,\n reactMarkup,\n slug,\n}: any) => {\n const title = `${slug} — React Email`;\n const router = useRouter();\n const [viewMode, setViewMode] = React.useState('desktop');\n\n const handleViewMode = (mode: string) => {\n setViewMode(mode);\n\n router.push({\n pathname: router.pathname,\n query: {\n ...router.query,\n view: mode,\n },\n });\n };\n\n React.useEffect(() => {\n if (router.query.view === 'source' || router.query.view === 'desktop') {\n setViewMode(router.query.view);\n }\n }, [router.query.view]);\n\n return (\n <Layout\n navItems={navItems}\n title={slug}\n viewMode={viewMode}\n setViewMode={handleViewMode}\n markup={markup}\n >\n <Head>\n <title>{title}</title>\n </Head>\n {viewMode === 'desktop' ? (\n <iframe\n srcDoc={markup}\n frameBorder=\"0\"\n className=\"w-full h-[calc(100vh_-_70px)]\"\n />\n ) : (\n <div className=\"flex gap-6 mx-auto p-6\">\n <Code language=\"jsx\">{reactMarkup}</Code>\n <Code>{markup}</Code>\n </div>\n )}\n </Layout>\n );\n};\n\nexport default Preview;\n",
21
+ "import * as React from 'react';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimport { render } from '@react-email/render';\nimport { GetStaticPaths } from 'next';\nimport { Layout } from '../../components/layout';\nimport { CodeContainer } from '../../components/code-container';\nimport { Code } from '../../components';\nimport Head from 'next/head';\nimport { useRouter } from 'next/router';\n\ninterface PreviewProps {\n navItems: string;\n markup: string;\n reactMarkup: string;\n slug: string;\n}\n\nexport const CONTENT_DIR = 'emails';\n\nconst getEmails = async () => {\n const emailsDirectory = path.join(process.cwd(), CONTENT_DIR);\n const filenames = await fs.readdir(emailsDirectory);\n const emails = filenames\n .map((file) => file.replace(/\\.(jsx|tsx)$/g, ''))\n .filter((file) => file !== 'components');\n return { emails, filenames };\n};\n\nexport const getStaticPaths: GetStaticPaths = async () => {\n const { emails } = await getEmails();\n const paths = emails.map((email) => {\n return { params: { slug: email } };\n });\n return { paths, fallback: true };\n};\n\nexport async function getStaticProps({ params }) {\n try {\n const { emails, filenames } = await getEmails();\n const template = filenames.filter((email) => {\n const [fileName] = email.split('.');\n return params.slug === fileName;\n });\n\n const Email = (await import(`../../../emails/${params.slug}`)).default;\n const markup = render(<Email />, { pretty: true });\n const path = `${process.cwd()}/${CONTENT_DIR}/${template[0]}`;\n const reactMarkup = await fs.readFile(path, {\n encoding: 'utf-8',\n });\n\n return emails\n ? { props: { navItems: emails, slug: params.slug, markup, reactMarkup } }\n : { notFound: true };\n } catch (error) {\n console.error(error);\n return { notFound: true };\n }\n}\n\nconst Preview: React.FC<Readonly<PreviewProps>> = ({\n navItems,\n markup,\n reactMarkup,\n slug,\n}: any) => {\n const title = `${slug} — React Email`;\n const router = useRouter();\n const [viewMode, setViewMode] = React.useState('desktop');\n\n const handleViewMode = (mode: string) => {\n setViewMode(mode);\n\n router.push({\n pathname: router.pathname,\n query: {\n ...router.query,\n view: mode,\n },\n });\n };\n\n React.useEffect(() => {\n if (router.query.view === 'source' || router.query.view === 'desktop') {\n setViewMode(router.query.view);\n }\n }, [router.query.view]);\n\n return (\n <Layout\n navItems={navItems}\n title={slug}\n viewMode={viewMode}\n setViewMode={handleViewMode}\n markup={markup}\n >\n <Head>\n <title>{title}</title>\n </Head>\n {viewMode === 'desktop' ? (\n <iframe\n srcDoc={markup}\n frameBorder=\"0\"\n className=\"w-full h-[calc(100vh_-_70px)]\"\n />\n ) : (\n <div className=\"flex gap-6 mx-auto p-6\">\n <CodeContainer\n markups={[\n { language: 'jsx', content: reactMarkup },\n { language: 'markup', content: markup },\n ]}\n />\n </div>\n )}\n </Layout>\n );\n};\n\nexport default Preview;\n",
22
22
  },
23
23
  ];
@@ -4,10 +4,15 @@ export const root = [
4
4
  content:
5
5
  '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n',
6
6
  },
7
+ {
8
+ title: 'next.config.js',
9
+ content:
10
+ "/**\n * @type {import('next').NextConfig}\n */\nconst nextConfig = {\n reactStrictMode: true,\n swcMinify: true,\n};\n\nmodule.exports = nextConfig;\n",
11
+ },
7
12
  {
8
13
  title: 'package.json',
9
14
  content:
10
- '{\n "name": "react-email-preview",\n "version": "0.0.5",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "^1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "^1.0.2",\n "@react-email/render": "0.0.2",\n "classnames": "2.3.2",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
15
+ '{\n "name": "react-email-preview",\n "version": "0.0.7",\n "description": "The React Email preview application",\n "license": "MIT",\n "scripts": {\n "dev": "next dev",\n "build": "next build",\n "start": "next start",\n "lint": "next lint",\n "format:check": "prettier --check \\"**/*.{ts,tsx,md}\\"",\n "format": "prettier --write \\"**/*.{ts,tsx,md}\\""\n },\n "engines": {\n "node": ">=18.0.0"\n },\n "dependencies": {\n "@next/font": "13.0.4",\n "@radix-ui/colors": "0.1.8",\n "@radix-ui/react-collapsible": "1.0.1",\n "@radix-ui/react-popover": "1.0.2",\n "@radix-ui/react-slot": "1.0.1",\n "@radix-ui/react-toggle-group": "1.0.1",\n "@radix-ui/react-tooltip": "1.0.2",\n "@react-email/render": "0.0.2",\n "classnames": "2.3.2",\n "next": "13.0.4",\n "prism-react-renderer": "1.3.5",\n "react": "18.2.0",\n "react-dom": "18.2.0"\n },\n "devDependencies": {\n "@types/classnames": "2.3.1",\n "@types/node": "18.11.9",\n "@types/react": "18.0.25",\n "@types/react-dom": "18.0.9",\n "autoprefixer": "10.4.13",\n "postcss": "8.4.19",\n "tailwindcss": "3.2.4",\n "typescript": "4.9.3"\n }\n}\n',
11
16
  },
12
17
  {
13
18
  title: 'postcss.config.js',
@@ -14,6 +14,11 @@ export const utils = [
14
14
  content:
15
15
  "export * from './as';\nexport * from './unreachable';\nexport * from './copy-text-to-clipboard';\n",
16
16
  },
17
+ {
18
+ title: 'language-map.ts',
19
+ content:
20
+ "const languageMap = {\n jsx: 'React',\n markup: 'HTML',\n};\n\nexport default languageMap;\n",
21
+ },
17
22
  {
18
23
  title: 'unreachable.ts',
19
24
  content:
@@ -105,13 +105,13 @@ const createAppFiles = async () => {
105
105
  });
106
106
  };
107
107
 
108
- const pageCreation = pages.map((page) => {
108
+ const pageCreation = pages.map(async (page) => {
109
109
  const location = page.dir
110
110
  ? `${SRC_PATH}/pages/${page.dir}/${page.title}`
111
111
  : `${SRC_PATH}/pages/${page.title}`;
112
112
 
113
113
  if (page.dir) {
114
- createDirectory(`${SRC_PATH}/pages/${page.dir}`);
114
+ await createDirectory(`${SRC_PATH}/pages/${page.dir}`);
115
115
  }
116
116
 
117
117
  return fs.promises.writeFile(location, page.content);
@@ -3,7 +3,7 @@ import esbuild from 'esbuild';
3
3
  import tree from 'tree-node-cli';
4
4
  import ora from 'ora';
5
5
  import logSymbols from 'log-symbols';
6
- import { render } from '@react-email/render';
6
+ import { render, Options } from '@react-email/render';
7
7
  import { unlinkSync, writeFileSync } from 'fs';
8
8
  import copy from 'cpy';
9
9
  import normalize from 'normalize-path';
@@ -14,9 +14,11 @@ import { checkDirectoryExist, CLIENT_EMAILS_PATH } from '../utils';
14
14
  files. Then these `.js` files are imported dynamically and rendered to `.html` files
15
15
  using the `render` function.
16
16
  */
17
- export const exportTemplates = async (outDir: string, pretty: boolean) => {
17
+ export const exportTemplates = async (outDir: string, options: Options) => {
18
18
  const spinner = ora('Preparing files...\n').start();
19
- const allTemplates = glob.sync(normalize(`${CLIENT_EMAILS_PATH}/*.{tsx,jsx}`));
19
+ const allTemplates = glob.sync(
20
+ normalize(`${CLIENT_EMAILS_PATH}/*.{tsx,jsx}`),
21
+ );
20
22
 
21
23
  esbuild.buildSync({
22
24
  bundle: true,
@@ -32,8 +34,11 @@ export const exportTemplates = async (outDir: string, pretty: boolean) => {
32
34
 
33
35
  for (const template of allBuiltTemplates) {
34
36
  const component = await import(template);
35
- const rendered = render(component.default(), { pretty });
36
- const htmlPath = template.replace('.js', '.html');
37
+ const rendered = render(component.default(), options);
38
+ const htmlPath = template.replace(
39
+ '.js',
40
+ options.plainText ? '.txt' : '.html',
41
+ );
37
42
  writeFileSync(htmlPath, rendered);
38
43
  unlinkSync(template);
39
44
  }
@@ -57,4 +62,6 @@ export const exportTemplates = async (outDir: string, pretty: boolean) => {
57
62
  symbol: logSymbols.success,
58
63
  text: 'Successfully exported emails',
59
64
  });
65
+
66
+ process.exit();
60
67
  };
package/source/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { program } from '@commander-js/extra-typings';
3
3
  import { PACKAGE_NAME } from './utils/constants';
4
4
  import { dev } from './commands/dev';
5
- import { exportTemplates } from './commands/exportTemplates';
5
+ import { exportTemplates } from './commands/export';
6
6
 
7
7
  program
8
8
  .name(PACKAGE_NAME)
@@ -19,6 +19,9 @@ program
19
19
  .description('Build the templates to the `out` directory')
20
20
  .option('--outDir <path>', 'Output directory', 'out')
21
21
  .option('-p, --pretty', 'Pretty print the output', false)
22
- .action(({ outDir, pretty }) => exportTemplates(outDir, pretty));
22
+ .option('-t, --plainText', 'Set output format as plain Text', false)
23
+ .action(({ outDir, pretty, plainText }) =>
24
+ exportTemplates(outDir, { pretty, plainText }),
25
+ );
23
26
 
24
27
  program.parse();