EmailSettingsForm.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. "use client";
  2. import { useState, useTransition } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { toast } from "sonner";
  5. import { Input } from "@/components/ui/input";
  6. import { Button } from "@/components/ui/button";
  7. import { Label } from "@/components/ui/label";
  8. import { Switch } from "@/components/ui/switch";
  9. import {
  10. Card,
  11. CardContent,
  12. CardDescription,
  13. CardHeader,
  14. CardTitle,
  15. } from "@/components/ui/card";
  16. import { Loader2, Send, Info } from "lucide-react";
  17. import { ORG_EMAIL_KEYS } from "../Schema/emailSettingsSchema";
  18. import {
  19. setEmailSettings,
  20. testOrgEmailConnection,
  21. } from "../Actions/emailSettingsActions";
  22. type EmailProviderType =
  23. | "smtp"
  24. | "resend"
  25. | "postmark"
  26. | "mailgun"
  27. | "sendgrid"
  28. | "ses";
  29. export function EmailSettingsForm({
  30. initial,
  31. }: {
  32. initial: Record<string, string>;
  33. }) {
  34. const router = useRouter();
  35. const [isPending, startTransition] = useTransition();
  36. const [isTesting, setIsTesting] = useState(false);
  37. const hasCustomProvider = !!initial[ORG_EMAIL_KEYS.EMAIL_PROVIDER];
  38. const [useCustom, setUseCustom] = useState(hasCustomProvider);
  39. // Provider
  40. const [emailProvider, setEmailProvider] = useState<EmailProviderType>(
  41. (initial[ORG_EMAIL_KEYS.EMAIL_PROVIDER] as EmailProviderType) || "smtp",
  42. );
  43. // SMTP
  44. const [smtpHost, setSmtpHost] = useState(
  45. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_HOST] || "",
  46. );
  47. const [smtpPort, setSmtpPort] = useState(
  48. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_PORT] || "587",
  49. );
  50. const [smtpUser, setSmtpUser] = useState(
  51. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_USER] || "",
  52. );
  53. const [smtpPass, setSmtpPass] = useState(
  54. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_PASS] || "",
  55. );
  56. const [smtpSecure, setSmtpSecure] = useState(
  57. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_SECURE] === "true",
  58. );
  59. const [smtpFromEmail, setSmtpFromEmail] = useState(
  60. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_EMAIL] || "",
  61. );
  62. const [smtpFromName, setSmtpFromName] = useState(
  63. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_NAME] || "",
  64. );
  65. const [smtpRejectUnauthorized, setSmtpRejectUnauthorized] = useState(
  66. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_REJECT_UNAUTHORIZED] !== "false",
  67. );
  68. const [smtpRequireTls, setSmtpRequireTls] = useState(
  69. initial[ORG_EMAIL_KEYS.EMAIL_SMTP_REQUIRE_TLS] === "true",
  70. );
  71. // Resend
  72. const [resendApiKey, setResendApiKey] = useState(
  73. initial[ORG_EMAIL_KEYS.EMAIL_RESEND_API_KEY] || "",
  74. );
  75. const [resendFromEmail, setResendFromEmail] = useState(
  76. initial[ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_EMAIL] || "",
  77. );
  78. const [resendFromName, setResendFromName] = useState(
  79. initial[ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_NAME] || "",
  80. );
  81. // Postmark
  82. const [postmarkApiKey, setPostmarkApiKey] = useState(
  83. initial[ORG_EMAIL_KEYS.EMAIL_POSTMARK_API_KEY] || "",
  84. );
  85. const [postmarkFromEmail, setPostmarkFromEmail] = useState(
  86. initial[ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_EMAIL] || "",
  87. );
  88. const [postmarkFromName, setPostmarkFromName] = useState(
  89. initial[ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_NAME] || "",
  90. );
  91. // Mailgun
  92. const [mailgunApiKey, setMailgunApiKey] = useState(
  93. initial[ORG_EMAIL_KEYS.EMAIL_MAILGUN_API_KEY] || "",
  94. );
  95. const [mailgunDomain, setMailgunDomain] = useState(
  96. initial[ORG_EMAIL_KEYS.EMAIL_MAILGUN_DOMAIN] || "",
  97. );
  98. const [mailgunRegion, setMailgunRegion] = useState(
  99. initial[ORG_EMAIL_KEYS.EMAIL_MAILGUN_REGION] || "us",
  100. );
  101. const [mailgunFromEmail, setMailgunFromEmail] = useState(
  102. initial[ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_EMAIL] || "",
  103. );
  104. const [mailgunFromName, setMailgunFromName] = useState(
  105. initial[ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_NAME] || "",
  106. );
  107. // SendGrid
  108. const [sendgridApiKey, setSendgridApiKey] = useState(
  109. initial[ORG_EMAIL_KEYS.EMAIL_SENDGRID_API_KEY] || "",
  110. );
  111. const [sendgridFromEmail, setSendgridFromEmail] = useState(
  112. initial[ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_EMAIL] || "",
  113. );
  114. const [sendgridFromName, setSendgridFromName] = useState(
  115. initial[ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_NAME] || "",
  116. );
  117. // Amazon SES
  118. const [sesAccessKeyId, setSesAccessKeyId] = useState(
  119. initial[ORG_EMAIL_KEYS.EMAIL_SES_ACCESS_KEY_ID] || "",
  120. );
  121. const [sesSecretAccessKey, setSesSecretAccessKey] = useState(
  122. initial[ORG_EMAIL_KEYS.EMAIL_SES_SECRET_ACCESS_KEY] || "",
  123. );
  124. const [sesRegion, setSesRegion] = useState(
  125. initial[ORG_EMAIL_KEYS.EMAIL_SES_REGION] || "us-east-1",
  126. );
  127. const [sesFromEmail, setSesFromEmail] = useState(
  128. initial[ORG_EMAIL_KEYS.EMAIL_SES_FROM_EMAIL] || "",
  129. );
  130. const [sesFromName, setSesFromName] = useState(
  131. initial[ORG_EMAIL_KEYS.EMAIL_SES_FROM_NAME] || "",
  132. );
  133. const handleSave = () => {
  134. startTransition(async () => {
  135. if (!useCustom) {
  136. // Clear the provider key to revert to platform default
  137. const result = await setEmailSettings({
  138. [ORG_EMAIL_KEYS.EMAIL_PROVIDER]: "",
  139. });
  140. if (result.success) {
  141. toast.success("Email settings saved — using platform default");
  142. router.refresh();
  143. } else {
  144. toast.error(result.error ?? "Failed to save settings");
  145. }
  146. return;
  147. }
  148. const data: Record<string, string> = {
  149. [ORG_EMAIL_KEYS.EMAIL_PROVIDER]: emailProvider,
  150. // SMTP
  151. [ORG_EMAIL_KEYS.EMAIL_SMTP_HOST]: smtpHost,
  152. [ORG_EMAIL_KEYS.EMAIL_SMTP_PORT]: smtpPort,
  153. [ORG_EMAIL_KEYS.EMAIL_SMTP_USER]: smtpUser,
  154. [ORG_EMAIL_KEYS.EMAIL_SMTP_PASS]: smtpPass,
  155. [ORG_EMAIL_KEYS.EMAIL_SMTP_SECURE]: String(smtpSecure),
  156. [ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_EMAIL]: smtpFromEmail,
  157. [ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_NAME]: smtpFromName,
  158. [ORG_EMAIL_KEYS.EMAIL_SMTP_REJECT_UNAUTHORIZED]: String(
  159. smtpRejectUnauthorized,
  160. ),
  161. [ORG_EMAIL_KEYS.EMAIL_SMTP_REQUIRE_TLS]: String(smtpRequireTls),
  162. // Resend
  163. [ORG_EMAIL_KEYS.EMAIL_RESEND_API_KEY]: resendApiKey,
  164. [ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_EMAIL]: resendFromEmail,
  165. [ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_NAME]: resendFromName,
  166. // Postmark
  167. [ORG_EMAIL_KEYS.EMAIL_POSTMARK_API_KEY]: postmarkApiKey,
  168. [ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_EMAIL]: postmarkFromEmail,
  169. [ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_NAME]: postmarkFromName,
  170. // Mailgun
  171. [ORG_EMAIL_KEYS.EMAIL_MAILGUN_API_KEY]: mailgunApiKey,
  172. [ORG_EMAIL_KEYS.EMAIL_MAILGUN_DOMAIN]: mailgunDomain,
  173. [ORG_EMAIL_KEYS.EMAIL_MAILGUN_REGION]: mailgunRegion,
  174. [ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_EMAIL]: mailgunFromEmail,
  175. [ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_NAME]: mailgunFromName,
  176. // SendGrid
  177. [ORG_EMAIL_KEYS.EMAIL_SENDGRID_API_KEY]: sendgridApiKey,
  178. [ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_EMAIL]: sendgridFromEmail,
  179. [ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_NAME]: sendgridFromName,
  180. // Amazon SES
  181. [ORG_EMAIL_KEYS.EMAIL_SES_ACCESS_KEY_ID]: sesAccessKeyId,
  182. [ORG_EMAIL_KEYS.EMAIL_SES_SECRET_ACCESS_KEY]: sesSecretAccessKey,
  183. [ORG_EMAIL_KEYS.EMAIL_SES_REGION]: sesRegion,
  184. [ORG_EMAIL_KEYS.EMAIL_SES_FROM_EMAIL]: sesFromEmail,
  185. [ORG_EMAIL_KEYS.EMAIL_SES_FROM_NAME]: sesFromName,
  186. };
  187. const result = await setEmailSettings(data);
  188. if (result.success) {
  189. toast.success("Email settings saved");
  190. router.refresh();
  191. } else {
  192. toast.error(result.error ?? "Failed to save settings");
  193. }
  194. });
  195. };
  196. const handleTestEmail = async () => {
  197. setIsTesting(true);
  198. try {
  199. const result = await testOrgEmailConnection();
  200. if (result.success) {
  201. toast.success(`Test email sent to ${result.data?.sentTo}`);
  202. } else {
  203. toast.error(result.error ?? "Email test failed");
  204. }
  205. } finally {
  206. setIsTesting(false);
  207. }
  208. };
  209. const isTestDisabled =
  210. isTesting ||
  211. !useCustom ||
  212. (emailProvider === "smtp" && !smtpHost) ||
  213. (emailProvider === "resend" && !resendApiKey) ||
  214. (emailProvider === "postmark" && !postmarkApiKey) ||
  215. (emailProvider === "mailgun" && !mailgunApiKey) ||
  216. (emailProvider === "sendgrid" && !sendgridApiKey) ||
  217. (emailProvider === "ses" && !sesAccessKeyId);
  218. return (
  219. <div className="space-y-6">
  220. <Card>
  221. <CardHeader>
  222. <CardTitle>Email Provider</CardTitle>
  223. <CardDescription>
  224. Configure your organization&apos;s email provider for sending
  225. invoices and quotes. If not configured, the platform&apos;s default
  226. email provider will be used.
  227. </CardDescription>
  228. </CardHeader>
  229. <CardContent className="space-y-6">
  230. <div className="flex items-center justify-between">
  231. <div className="space-y-0.5">
  232. <Label htmlFor="use-custom-email">
  233. Use custom email provider
  234. </Label>
  235. <p className="text-xs text-muted-foreground">
  236. Enable to configure your own email provider instead of using the
  237. platform default
  238. </p>
  239. </div>
  240. <Switch
  241. id="use-custom-email"
  242. checked={useCustom}
  243. onCheckedChange={setUseCustom}
  244. />
  245. </div>
  246. {!useCustom && (
  247. <div className="flex items-start gap-3 rounded-lg border bg-muted/50 p-4">
  248. <Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
  249. <p className="text-sm text-muted-foreground">
  250. Your organization is using the platform&apos;s default email
  251. provider. Invoices and quotes will be sent using the
  252. platform-configured email settings.
  253. </p>
  254. </div>
  255. )}
  256. {useCustom && (
  257. <>
  258. <div className="flex flex-wrap gap-2">
  259. <Button
  260. type="button"
  261. variant={emailProvider === "smtp" ? "default" : "outline"}
  262. onClick={() => setEmailProvider("smtp")}
  263. className="flex-1"
  264. >
  265. SMTP
  266. </Button>
  267. <Button
  268. type="button"
  269. variant={emailProvider === "resend" ? "default" : "outline"}
  270. onClick={() => setEmailProvider("resend")}
  271. className="flex-1"
  272. >
  273. Resend
  274. </Button>
  275. <Button
  276. type="button"
  277. variant={emailProvider === "postmark" ? "default" : "outline"}
  278. onClick={() => setEmailProvider("postmark")}
  279. className="flex-1"
  280. >
  281. Postmark
  282. </Button>
  283. <Button
  284. type="button"
  285. variant={emailProvider === "mailgun" ? "default" : "outline"}
  286. onClick={() => setEmailProvider("mailgun")}
  287. className="flex-1"
  288. >
  289. Mailgun
  290. </Button>
  291. <Button
  292. type="button"
  293. variant={
  294. emailProvider === "sendgrid" ? "default" : "outline"
  295. }
  296. onClick={() => setEmailProvider("sendgrid")}
  297. className="flex-1"
  298. >
  299. SendGrid
  300. </Button>
  301. <Button
  302. type="button"
  303. variant={emailProvider === "ses" ? "default" : "outline"}
  304. onClick={() => setEmailProvider("ses")}
  305. className="flex-1"
  306. >
  307. Amazon SES
  308. </Button>
  309. </div>
  310. {emailProvider === "smtp" && (
  311. <>
  312. <div className="grid gap-4 sm:grid-cols-2">
  313. <div className="space-y-2">
  314. <Label htmlFor="org-smtp-host">SMTP Host</Label>
  315. <Input
  316. id="org-smtp-host"
  317. placeholder="smtp.example.com"
  318. value={smtpHost}
  319. onChange={(e) => setSmtpHost(e.target.value)}
  320. />
  321. </div>
  322. <div className="space-y-2">
  323. <Label htmlFor="org-smtp-port">SMTP Port</Label>
  324. <Input
  325. id="org-smtp-port"
  326. placeholder="587"
  327. value={smtpPort}
  328. onChange={(e) => setSmtpPort(e.target.value)}
  329. />
  330. </div>
  331. </div>
  332. <div className="grid gap-4 sm:grid-cols-2">
  333. <div className="space-y-2">
  334. <Label htmlFor="org-smtp-user">Username</Label>
  335. <Input
  336. id="org-smtp-user"
  337. placeholder="user@example.com"
  338. value={smtpUser}
  339. onChange={(e) => setSmtpUser(e.target.value)}
  340. />
  341. </div>
  342. <div className="space-y-2">
  343. <Label htmlFor="org-smtp-pass">Password</Label>
  344. <Input
  345. id="org-smtp-pass"
  346. type="password"
  347. placeholder="••••••••"
  348. value={smtpPass}
  349. onChange={(e) => setSmtpPass(e.target.value)}
  350. />
  351. </div>
  352. </div>
  353. <div className="grid gap-4 sm:grid-cols-2">
  354. <div className="space-y-2">
  355. <Label htmlFor="org-smtp-from-email">From Email</Label>
  356. <Input
  357. id="org-smtp-from-email"
  358. placeholder="noreply@example.com"
  359. value={smtpFromEmail}
  360. onChange={(e) => setSmtpFromEmail(e.target.value)}
  361. />
  362. </div>
  363. <div className="space-y-2">
  364. <Label htmlFor="org-smtp-from-name">From Name</Label>
  365. <Input
  366. id="org-smtp-from-name"
  367. placeholder="Your Workshop"
  368. value={smtpFromName}
  369. onChange={(e) => setSmtpFromName(e.target.value)}
  370. />
  371. </div>
  372. </div>
  373. <div className="space-y-4">
  374. <div className="flex items-center justify-between">
  375. <div className="space-y-0.5">
  376. <Label htmlFor="org-smtp-secure">
  377. TLS Connection (Port 465)
  378. </Label>
  379. <p className="text-xs text-muted-foreground">
  380. Enable for implicit TLS. Disable for STARTTLS (port
  381. 587/25).
  382. </p>
  383. </div>
  384. <Switch
  385. id="org-smtp-secure"
  386. checked={smtpSecure}
  387. onCheckedChange={setSmtpSecure}
  388. />
  389. </div>
  390. <div className="flex items-center justify-between">
  391. <div className="space-y-0.5">
  392. <Label htmlFor="org-smtp-reject-unauthorized">
  393. Verify TLS Certificates
  394. </Label>
  395. <p className="text-xs text-muted-foreground">
  396. Disable to allow self-signed certificates (not
  397. recommended for production)
  398. </p>
  399. </div>
  400. <Switch
  401. id="org-smtp-reject-unauthorized"
  402. checked={smtpRejectUnauthorized}
  403. onCheckedChange={setSmtpRejectUnauthorized}
  404. />
  405. </div>
  406. <div className="flex items-center justify-between">
  407. <div className="space-y-0.5">
  408. <Label htmlFor="org-smtp-require-tls">
  409. Require TLS Upgrade
  410. </Label>
  411. <p className="text-xs text-muted-foreground">
  412. Force TLS upgrade on STARTTLS connections
  413. </p>
  414. </div>
  415. <Switch
  416. id="org-smtp-require-tls"
  417. checked={smtpRequireTls}
  418. onCheckedChange={setSmtpRequireTls}
  419. />
  420. </div>
  421. </div>
  422. </>
  423. )}
  424. {emailProvider === "resend" && (
  425. <>
  426. <div className="space-y-2">
  427. <Label htmlFor="org-resend-api-key">API Key</Label>
  428. <Input
  429. id="org-resend-api-key"
  430. type="password"
  431. placeholder="re_••••••••"
  432. value={resendApiKey}
  433. onChange={(e) => setResendApiKey(e.target.value)}
  434. />
  435. <p className="text-xs text-muted-foreground">
  436. Get your API key from{" "}
  437. <a
  438. href="https://resend.com"
  439. target="_blank"
  440. rel="noopener noreferrer"
  441. className="underline"
  442. >
  443. resend.com
  444. </a>
  445. </p>
  446. </div>
  447. <div className="grid gap-4 sm:grid-cols-2">
  448. <div className="space-y-2">
  449. <Label htmlFor="org-resend-from-email">From Email</Label>
  450. <Input
  451. id="org-resend-from-email"
  452. placeholder="noreply@yourdomain.com"
  453. value={resendFromEmail}
  454. onChange={(e) => setResendFromEmail(e.target.value)}
  455. />
  456. </div>
  457. <div className="space-y-2">
  458. <Label htmlFor="org-resend-from-name">From Name</Label>
  459. <Input
  460. id="org-resend-from-name"
  461. placeholder="Your Workshop"
  462. value={resendFromName}
  463. onChange={(e) => setResendFromName(e.target.value)}
  464. />
  465. </div>
  466. </div>
  467. </>
  468. )}
  469. {emailProvider === "postmark" && (
  470. <>
  471. <div className="space-y-2">
  472. <Label htmlFor="org-postmark-api-key">Server Token</Label>
  473. <Input
  474. id="org-postmark-api-key"
  475. type="password"
  476. placeholder="••••••••-••••-••••-••••-••••••••••••"
  477. value={postmarkApiKey}
  478. onChange={(e) => setPostmarkApiKey(e.target.value)}
  479. />
  480. <p className="text-xs text-muted-foreground">
  481. Get your Server Token from{" "}
  482. <a
  483. href="https://postmarkapp.com"
  484. target="_blank"
  485. rel="noopener noreferrer"
  486. className="underline"
  487. >
  488. postmarkapp.com
  489. </a>
  490. </p>
  491. </div>
  492. <div className="grid gap-4 sm:grid-cols-2">
  493. <div className="space-y-2">
  494. <Label htmlFor="org-postmark-from-email">
  495. From Email
  496. </Label>
  497. <Input
  498. id="org-postmark-from-email"
  499. placeholder="noreply@yourdomain.com"
  500. value={postmarkFromEmail}
  501. onChange={(e) => setPostmarkFromEmail(e.target.value)}
  502. />
  503. </div>
  504. <div className="space-y-2">
  505. <Label htmlFor="org-postmark-from-name">From Name</Label>
  506. <Input
  507. id="org-postmark-from-name"
  508. placeholder="Your Workshop"
  509. value={postmarkFromName}
  510. onChange={(e) => setPostmarkFromName(e.target.value)}
  511. />
  512. </div>
  513. </div>
  514. </>
  515. )}
  516. {emailProvider === "mailgun" && (
  517. <>
  518. <div className="space-y-2">
  519. <Label htmlFor="org-mailgun-api-key">API Key</Label>
  520. <Input
  521. id="org-mailgun-api-key"
  522. type="password"
  523. placeholder="key-••••••••••••••••••••••••••••••••"
  524. value={mailgunApiKey}
  525. onChange={(e) => setMailgunApiKey(e.target.value)}
  526. />
  527. <p className="text-xs text-muted-foreground">
  528. Get your API key from{" "}
  529. <a
  530. href="https://app.mailgun.com"
  531. target="_blank"
  532. rel="noopener noreferrer"
  533. className="underline"
  534. >
  535. app.mailgun.com
  536. </a>
  537. </p>
  538. </div>
  539. <div className="grid gap-4 sm:grid-cols-2">
  540. <div className="space-y-2">
  541. <Label htmlFor="org-mailgun-domain">Domain</Label>
  542. <Input
  543. id="org-mailgun-domain"
  544. placeholder="mg.yourdomain.com"
  545. value={mailgunDomain}
  546. onChange={(e) => setMailgunDomain(e.target.value)}
  547. />
  548. </div>
  549. <div className="space-y-2">
  550. <Label htmlFor="org-mailgun-region">Region</Label>
  551. <div className="flex gap-2">
  552. <Button
  553. type="button"
  554. variant={
  555. mailgunRegion === "us" ? "default" : "outline"
  556. }
  557. onClick={() => setMailgunRegion("us")}
  558. className="flex-1"
  559. size="sm"
  560. >
  561. US
  562. </Button>
  563. <Button
  564. type="button"
  565. variant={
  566. mailgunRegion === "eu" ? "default" : "outline"
  567. }
  568. onClick={() => setMailgunRegion("eu")}
  569. className="flex-1"
  570. size="sm"
  571. >
  572. EU
  573. </Button>
  574. </div>
  575. </div>
  576. </div>
  577. <div className="grid gap-4 sm:grid-cols-2">
  578. <div className="space-y-2">
  579. <Label htmlFor="org-mailgun-from-email">
  580. From Email
  581. </Label>
  582. <Input
  583. id="org-mailgun-from-email"
  584. placeholder="noreply@yourdomain.com"
  585. value={mailgunFromEmail}
  586. onChange={(e) => setMailgunFromEmail(e.target.value)}
  587. />
  588. </div>
  589. <div className="space-y-2">
  590. <Label htmlFor="org-mailgun-from-name">From Name</Label>
  591. <Input
  592. id="org-mailgun-from-name"
  593. placeholder="Your Workshop"
  594. value={mailgunFromName}
  595. onChange={(e) => setMailgunFromName(e.target.value)}
  596. />
  597. </div>
  598. </div>
  599. </>
  600. )}
  601. {emailProvider === "sendgrid" && (
  602. <>
  603. <div className="space-y-2">
  604. <Label htmlFor="org-sendgrid-api-key">API Key</Label>
  605. <Input
  606. id="org-sendgrid-api-key"
  607. type="password"
  608. placeholder="SG.••••••••••••••••••••••••••••••••"
  609. value={sendgridApiKey}
  610. onChange={(e) => setSendgridApiKey(e.target.value)}
  611. />
  612. <p className="text-xs text-muted-foreground">
  613. Get your API key from{" "}
  614. <a
  615. href="https://app.sendgrid.com"
  616. target="_blank"
  617. rel="noopener noreferrer"
  618. className="underline"
  619. >
  620. app.sendgrid.com
  621. </a>
  622. </p>
  623. </div>
  624. <div className="grid gap-4 sm:grid-cols-2">
  625. <div className="space-y-2">
  626. <Label htmlFor="org-sendgrid-from-email">
  627. From Email
  628. </Label>
  629. <Input
  630. id="org-sendgrid-from-email"
  631. placeholder="noreply@yourdomain.com"
  632. value={sendgridFromEmail}
  633. onChange={(e) => setSendgridFromEmail(e.target.value)}
  634. />
  635. </div>
  636. <div className="space-y-2">
  637. <Label htmlFor="org-sendgrid-from-name">From Name</Label>
  638. <Input
  639. id="org-sendgrid-from-name"
  640. placeholder="Your Workshop"
  641. value={sendgridFromName}
  642. onChange={(e) => setSendgridFromName(e.target.value)}
  643. />
  644. </div>
  645. </div>
  646. </>
  647. )}
  648. {emailProvider === "ses" && (
  649. <>
  650. <div className="grid gap-4 sm:grid-cols-2">
  651. <div className="space-y-2">
  652. <Label htmlFor="org-ses-access-key">
  653. Access Key ID
  654. </Label>
  655. <Input
  656. id="org-ses-access-key"
  657. type="password"
  658. placeholder="AKIA••••••••••••••••"
  659. value={sesAccessKeyId}
  660. onChange={(e) => setSesAccessKeyId(e.target.value)}
  661. />
  662. </div>
  663. <div className="space-y-2">
  664. <Label htmlFor="org-ses-secret-key">
  665. Secret Access Key
  666. </Label>
  667. <Input
  668. id="org-ses-secret-key"
  669. type="password"
  670. placeholder="••••••••••••••••••••••••••••••••••••••••"
  671. value={sesSecretAccessKey}
  672. onChange={(e) => setSesSecretAccessKey(e.target.value)}
  673. />
  674. </div>
  675. </div>
  676. <div className="space-y-2">
  677. <Label htmlFor="org-ses-region">AWS Region</Label>
  678. <Input
  679. id="org-ses-region"
  680. placeholder="us-east-1"
  681. value={sesRegion}
  682. onChange={(e) => setSesRegion(e.target.value)}
  683. />
  684. <p className="text-xs text-muted-foreground">
  685. The AWS region where SES is configured (e.g. us-east-1,
  686. eu-west-1)
  687. </p>
  688. </div>
  689. <div className="grid gap-4 sm:grid-cols-2">
  690. <div className="space-y-2">
  691. <Label htmlFor="org-ses-from-email">From Email</Label>
  692. <Input
  693. id="org-ses-from-email"
  694. placeholder="noreply@yourdomain.com"
  695. value={sesFromEmail}
  696. onChange={(e) => setSesFromEmail(e.target.value)}
  697. />
  698. </div>
  699. <div className="space-y-2">
  700. <Label htmlFor="org-ses-from-name">From Name</Label>
  701. <Input
  702. id="org-ses-from-name"
  703. placeholder="Your Workshop"
  704. value={sesFromName}
  705. onChange={(e) => setSesFromName(e.target.value)}
  706. />
  707. </div>
  708. </div>
  709. </>
  710. )}
  711. <div className="flex items-center gap-2 pt-2">
  712. <Button
  713. type="button"
  714. variant="outline"
  715. onClick={handleTestEmail}
  716. disabled={isTestDisabled}
  717. >
  718. {isTesting ? (
  719. <Loader2 className="mr-2 h-4 w-4 animate-spin" />
  720. ) : (
  721. <Send className="mr-2 h-4 w-4" />
  722. )}
  723. Send Test Email
  724. </Button>
  725. <p className="text-xs text-muted-foreground">
  726. Save settings first, then send a test email to your account
  727. </p>
  728. </div>
  729. </>
  730. )}
  731. </CardContent>
  732. </Card>
  733. <div className="flex justify-end">
  734. <Button onClick={handleSave} disabled={isPending}>
  735. {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
  736. Save Settings
  737. </Button>
  738. </div>
  739. </div>
  740. );
  741. }