react-email 1.6.1 → 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",
@@ -1,2 +1,2 @@
1
1
  import { Options } from '@react-email/render';
2
- export declare const exportTemplates: (outDir: string, options: Options) => Promise<void>;
2
+ export declare const exportTemplates: (outDir: string, options: Options) => Promise<never>;
@@ -75,5 +75,6 @@ const exportTemplates = async (outDir, options) => {
75
75
  symbol: log_symbols_1.default.success,
76
76
  text: 'Successfully exported emails',
77
77
  });
78
+ process.exit();
78
79
  };
79
80
  exports.exportTemplates = exportTemplates;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "1.6.1",
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"
@@ -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": {
@@ -67,7 +67,6 @@ export const Code: React.FC<Readonly<CodeProps>> = ({
67
67
  background:
68
68
  '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%)',
69
69
  }}
70
-
71
70
  />
72
71
  <div className="p-4">
73
72
  {tokens.map((line, i) => {
@@ -93,7 +93,14 @@ export const Send = ({ markup }: { markup: string }) => {
93
93
  <div className="flex items-center justify-between">
94
94
  <Text className="inline-block" size="1">
95
95
  Powered by{' '}
96
- <a className="hover:text-slate-12 transition ease-in-out duration-300" href="https://resend.com" target="_blank" rel="noreferrer">Resend</a>
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>
97
104
  </Text>
98
105
  <Button
99
106
  type="submit"
@@ -1,6 +1,6 @@
1
1
  const languageMap = {
2
- jsx: 'React',
3
- markup: 'HTML'
4
- }
2
+ jsx: 'React',
3
+ markup: 'HTML',
4
+ };
5
5
 
6
- export default languageMap;
6
+ export default languageMap;
@@ -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:
@@ -62,4 +62,6 @@ export const exportTemplates = async (outDir: string, options: Options) => {
62
62
  symbol: logSymbols.success,
63
63
  text: 'Successfully exported emails',
64
64
  });
65
+
66
+ process.exit();
65
67
  };