Skip to content

Commit 3e54a0d

Browse files
authored
feat(web): send button for copy-paste components (#2629)
1 parent 4f858a3 commit 3e54a0d

File tree

5 files changed

+180
-27
lines changed

5 files changed

+180
-27
lines changed

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@babel/parser": "7.27.0",
1616
"@babel/preset-typescript": "7.27.0",
1717
"@babel/traverse": "7.27.0",
18+
"@radix-ui/react-popover": "1.1.15",
1819
"@react-email/components": "workspace:*",
1920
"@react-email/render": "workspace:*",
2021
"@react-three/drei": "^9.120.3",
@@ -30,6 +31,7 @@
3031
"react": "^19",
3132
"react-dom": "^19",
3233
"resend": "4.3.0",
34+
"sonner": "2.0.3",
3335
"three": "^0.170.0",
3436
"vaul": "1.1.2"
3537
},

apps/web/src/components/component-view.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ComponentPreview } from './component-preview';
1010
import { IconMonitor } from './icons/icon-monitor';
1111
import { IconPhone } from './icons/icon-phone';
1212
import { IconSource } from './icons/icon-source';
13+
import { Send } from './send';
1314
import { TabTrigger } from './tab-trigger';
1415
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
1516

@@ -111,6 +112,11 @@ export function ComponentView({ component, className }: ComponentViewProps) {
111112
>
112113
<IconSource />
113114
</TabTriggetWithTooltip>
115+
<Send
116+
className="ml-2"
117+
markup={component.code.html}
118+
defaultSubject={component.title}
119+
/>
114120
</Tabs.List>
115121
<div className="absolute right-0 bottom-0 h-px w-[100dvw] bg-slate-4" />
116122
</div>

apps/web/src/components/send.tsx

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as Popover from '@radix-ui/react-popover';
2+
import classNames from 'classnames';
3+
import * as React from 'react';
4+
import { toast } from 'sonner';
5+
import { Button } from './button';
6+
import { Text } from './text';
7+
8+
interface SendProps extends React.ComponentProps<'button'> {
9+
markup: string;
10+
defaultSubject?: string;
11+
}
12+
13+
export const Send = ({
14+
markup,
15+
defaultSubject,
16+
className,
17+
...rest
18+
}: SendProps) => {
19+
const [to, setTo] = React.useState('');
20+
const [subject, setSubject] = React.useState(defaultSubject ?? '');
21+
const [isSending, setIsSending] = React.useState(false);
22+
const [isPopOverOpen, setIsPopOverOpen] = React.useState(false);
23+
24+
const onFormSubmit = async (e: React.FormEvent) => {
25+
try {
26+
e.preventDefault();
27+
setIsSending(true);
28+
29+
const response = await fetch('https://react.email/api/send/test', {
30+
method: 'POST',
31+
headers: { 'Content-Type': 'application/json' },
32+
body: JSON.stringify({
33+
to,
34+
subject,
35+
html: markup,
36+
}),
37+
});
38+
39+
if (response.ok) {
40+
toast.success('Email sent! Check your inbox.');
41+
} else {
42+
if (response.status === 429) {
43+
const { error } = (await response.json()) as { error: string };
44+
toast.error(error);
45+
} else {
46+
toast.error('Something went wrong. Please try again.');
47+
}
48+
}
49+
} catch (_exception) {
50+
toast.error('Something went wrong. Please try again.');
51+
} finally {
52+
setIsSending(false);
53+
}
54+
};
55+
56+
const toId = React.useId();
57+
const subjectId = React.useId();
58+
59+
return (
60+
<Popover.Root
61+
onOpenChange={() => {
62+
if (!isPopOverOpen) {
63+
document.body.classList.add('popup-open');
64+
setIsPopOverOpen(true);
65+
} else {
66+
document.body.classList.remove('popup-open');
67+
setIsPopOverOpen(false);
68+
}
69+
}}
70+
open={isPopOverOpen}
71+
>
72+
<Popover.Trigger asChild>
73+
<button
74+
type="submit"
75+
{...rest}
76+
className={classNames(
77+
'flex items-center justify-center self-center rounded-lg bg-slate-6 border border-solid border-transparent px-3 py-1 h-full text-center font-sans text-sm text-slate-11 outline-none transition duration-300 ease-in-out hover:text-slate-12',
78+
className,
79+
)}
80+
>
81+
Send
82+
</button>
83+
</Popover.Trigger>
84+
<Popover.Anchor />
85+
<Popover.Portal>
86+
<Popover.Content
87+
align="end"
88+
className="-mt-10 w-80 rounded-lg border border-slate-6 bg-black/70 p-3 text-slate-11 shadow-md backdrop-blur-lg font-sans z-[3]"
89+
sideOffset={48}
90+
>
91+
<form className="mt-1" onSubmit={(e) => void onFormSubmit(e)}>
92+
<label
93+
className="mb-2 block text-xs uppercase text-slate-10"
94+
htmlFor={toId}
95+
>
96+
Recipient
97+
</label>
98+
<input
99+
autoFocus
100+
className="mb-3 w-full appearance-none rounded-lg border border-slate-6 bg-slate-3 px-2 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
101+
defaultValue={to}
102+
id={toId}
103+
onChange={(e) => {
104+
setTo(e.target.value);
105+
}}
106+
placeholder="you@example.com"
107+
required
108+
type="email"
109+
/>
110+
<label
111+
className="mb-2 mt-1 block text-xs uppercase text-slate-10"
112+
htmlFor={subjectId}
113+
>
114+
Subject
115+
</label>
116+
<input
117+
className="mb-3 w-full appearance-none rounded-lg border border-slate-6 bg-slate-3 px-2 py-1 text-sm text-slate-12 placeholder-slate-10 outline-none transition duration-300 ease-in-out focus:ring-1 focus:ring-slate-10"
118+
defaultValue={subject}
119+
id={subjectId}
120+
onChange={(e) => {
121+
setSubject(e.target.value);
122+
}}
123+
placeholder="My Email"
124+
required
125+
type="text"
126+
/>
127+
<input
128+
className="appearance-none checked:bg-blue-500"
129+
type="checkbox"
130+
/>
131+
<div className="mt-3 flex items-center justify-between">
132+
<Text className="inline-block" size="1">
133+
Powered by{' '}
134+
<a
135+
className="text-white/85 transition duration-300 ease-in-out hover:text-slate-12"
136+
href="https://resend.com"
137+
rel="noreferrer"
138+
target="_blank"
139+
>
140+
Resend
141+
</a>
142+
</Text>
143+
<Button
144+
className="disabled:border-transparent disabled:bg-slate-11"
145+
disabled={subject.length === 0 || to.length === 0 || isSending}
146+
type="submit"
147+
>
148+
Send
149+
</Button>
150+
</div>
151+
</form>
152+
</Popover.Content>
153+
</Popover.Portal>
154+
</Popover.Root>
155+
);
156+
};

packages/preview-server/src/components/send.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ export const Send = ({ markup }: { markup: string }) => {
2525
}),
2626
});
2727

28-
if (response.status === 429) {
29-
const { error } = (await response.json()) as { error: string };
30-
toast.error(error);
28+
if (response.ok) {
29+
toast.success('Email sent! Check your inbox.');
30+
} else {
31+
if (response.status === 429) {
32+
const { error } = (await response.json()) as { error: string };
33+
toast.error(error);
34+
} else {
35+
toast.error('Something went wrong. Please try again.');
36+
}
3137
}
32-
33-
toast.success('Email sent! Check your inbox.');
3438
} catch (_exception) {
3539
toast.error('Something went wrong. Please try again.');
3640
} finally {

pnpm-lock.yaml

Lines changed: 7 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)