Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add react-email templates

authored by

Maximilian Kaske and committed by
Maximilian Kaske
c027c477 c7223592

+653 -22
+2
apps/web/src/app/status-page/[domain]/subscribe/route.ts
··· 5 5 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 6 6 import { SubscribeEmail, sendEmail } from "@openstatus/emails"; 7 7 8 + // TODO: use trpc route 9 + 8 10 export async function POST( 9 11 req: Request, 10 12 props: { params: Promise<{ domain: string }> },
+1
packages/db/src/schema/status_reports/validation.ts
··· 52 52 typeof insertStatusReportUpdateSchema 53 53 >; 54 54 export type StatusReportUpdate = z.infer<typeof selectStatusReportUpdateSchema>; 55 + export type StatusReportStatus = z.infer<typeof statusReportStatusSchema>;
+20
packages/emails/emails/_components/footer.tsx
··· 1 + import { Link, Section, Text } from "@react-email/components"; 2 + import { styles } from "./styles"; 3 + 4 + export function Footer() { 5 + return ( 6 + <Section style={{ textAlign: "center" }}> 7 + <Text> 8 + <Link style={styles.link} href="https://openstatus.dev"> 9 + Home Page 10 + </Link>{" "} 11 + ・{" "} 12 + <Link style={styles.link} href="mailto:ping@openstatus.dev"> 13 + Contact Support 14 + </Link> 15 + </Text> 16 + 17 + <Text>OpenStatus ・ 122 Rue Amelot ・ 75011 Paris, France</Text> 18 + </Section> 19 + ); 20 + }
+28
packages/emails/emails/_components/layout.tsx
··· 1 + import { Container, Img, Link, Section } from "@react-email/components"; 2 + import { Footer } from "./footer"; 3 + import { styles } from "./styles"; 4 + 5 + interface LayoutProps { 6 + children?: React.ReactNode; 7 + img?: { 8 + src: string; 9 + alt: string; 10 + }; 11 + } 12 + 13 + const defaultImg = { 14 + src: "https://openstatus.dev/assets/logos/OpenStatus.png", 15 + alt: "OpenStatus", 16 + }; 17 + 18 + export function Layout({ children, img = defaultImg }: LayoutProps) { 19 + return ( 20 + <Container style={styles.container}> 21 + <Link href="https://openstatus.dev"> 22 + <Img src={img.src} width="36" height="36" alt={img.alt} /> 23 + </Link> 24 + <Section style={styles.section}>{children}</Section> 25 + <Footer /> 26 + </Container> 27 + ); 28 + }
+42
packages/emails/emails/_components/styles.ts
··· 1 + export const colors = { 2 + success: "#51b363", 3 + danger: "#ec6041", 4 + warning: "#ffd60a", 5 + info: "#3d9eff", 6 + }; 7 + 8 + export const styles = { 9 + main: { 10 + backgroundColor: "#ffffff", 11 + color: "#24292e", 12 + fontFamily: 13 + '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', 14 + }, 15 + container: { 16 + maxWidth: "480px", 17 + margin: "0 auto", 18 + padding: "20px 0 48px", 19 + }, 20 + section: { 21 + padding: "24px", 22 + margin: "24px 0", 23 + border: "solid 1px #dedede", 24 + borderRadius: "5px", 25 + }, 26 + button: { 27 + backgroundColor: "#24292e", 28 + color: "#ffffff", 29 + padding: "8px 16px", 30 + borderRadius: "6px", 31 + }, 32 + link: { 33 + textDecoration: "underline", 34 + color: colors.info, 35 + }, 36 + bold: { 37 + fontWeight: "bold", 38 + }, 39 + row: { 40 + borderTop: "1px solid #dedede", 41 + }, 42 + } satisfies Record<string, React.CSSProperties>;
+17 -11
packages/emails/emails/alert.tsx
··· 13 13 Section, 14 14 Text, 15 15 } from "@react-email/components"; 16 - import { z } from "zod"; 17 16 18 - export const EmailDataSchema = z.object({ 19 - monitorName: z.string(), 20 - monitorUrl: z.string().url(), 21 - recipientName: z.string(), 22 - }); 17 + interface AlertProps { 18 + monitorName: string; 19 + monitorUrl: string; 20 + recipientName: string; 21 + } 23 22 24 - const Alert = ({ data }: { data: z.infer<typeof EmailDataSchema> }) => { 23 + const Alert = ({ monitorName, monitorUrl, recipientName }: AlertProps) => { 25 24 return ( 26 25 <Html> 27 26 <Head> 28 27 <title>New incident detected 🚨</title> 29 - <Preview>New incident detected : {data.monitorName} 🚨</Preview> 28 + <Preview>New incident detected : {monitorName} 🚨</Preview> 30 29 <Body className="mx-auto my-auto bg-white font-sans"> 31 30 <Container className="mx-auto my-[40px] w-[465px] rounded border border-[#eaeaea] border-solid p-[20px]"> 32 31 <Heading className="mx-0 my-[30px] p-0 text-center font-normal text-[24px] text-black"> 33 32 New incident detected! 34 33 </Heading> 35 34 <Text className="text-[14px] text-black leading-[24px]"> 36 - Hello {data.recipientName}, <br /> 35 + Hello {recipientName}, <br /> 37 36 We have detected a new incident. 38 37 </Text> 39 38 40 39 <Section className="my-[30px] rounded border border-gray-200 border-solid bg-gray-100 p-2"> 41 40 <Row> 42 41 <Column className="text-lg">Monitor</Column> 43 - <Column>{data.monitorName}</Column> 42 + <Column>{monitorName}</Column> 44 43 </Row> 45 44 <Row className="mt-2"> 46 45 <Column className="text-lg">URL</Column> 47 - <Column>{data.monitorUrl}</Column> 46 + <Column>{monitorUrl}</Column> 48 47 </Row> 49 48 </Section> 50 49 ··· 63 62 ); 64 63 }; 65 64 65 + Alert.PreviewProps = { 66 + monitorName: "My Monitor", 67 + monitorUrl: "https://www.example.com", 68 + recipientName: "John Doe", 69 + } satisfies AlertProps; 70 + 66 71 export { Alert }; 72 + export default Alert;
+1
packages/emails/emails/followup.tsx
··· 37 37 }; 38 38 39 39 export { FollowUpEmail }; 40 + export default FollowUpEmail;
+162
packages/emails/emails/monitor-alert.tsx
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Button, 6 + CodeInline, 7 + Column, 8 + Container, 9 + Head, 10 + Heading, 11 + Html, 12 + Img, 13 + Preview, 14 + Row, 15 + Text, 16 + } from "@react-email/components"; 17 + import { Layout } from "./_components/layout"; 18 + import { colors, styles } from "./_components/styles"; 19 + 20 + export interface MonitorAlertProps { 21 + type: "degraded" | "up" | "down"; 22 + name?: string; 23 + url?: string; 24 + method?: string; 25 + status?: string; 26 + latency?: string; 27 + location?: string; 28 + timestamp?: string; 29 + } 30 + 31 + function getIcon(type: MonitorAlertProps["type"]): { 32 + src: string; 33 + color: string; 34 + } { 35 + switch (type) { 36 + case "up": 37 + return { 38 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWNoZWNrIj48cGF0aCBkPSJNMjAgNiA5IDE3bC01LTUiLz48L3N2Zz4=", 39 + color: colors.success, 40 + }; 41 + case "down": 42 + return { 43 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXgiPjxwYXRoIGQ9Ik0xOCA2IDYgMTgiLz48cGF0aCBkPSJtNiA2IDEyIDEyIi8+PC9zdmc+", 44 + color: colors.danger, 45 + }; 46 + case "degraded": 47 + return { 48 + src: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXRyaWFuZ2xlLWFsZXJ0Ij48cGF0aCBkPSJtMjEuNzMgMTgtOC0xNGEyIDIgMCAwIDAtMy40OCAwbC04IDE0QTIgMiAwIDAgMCA0IDIxaDE2YTIgMiAwIDAgMCAxLjczLTMiLz48cGF0aCBkPSJNMTIgOXY0Ii8+PHBhdGggZD0iTTEyIDE3aC4wMSIvPjwvc3ZnPg==", 49 + color: colors.warning, 50 + }; 51 + } 52 + } 53 + 54 + export const MonitorAlertEmail = (props: MonitorAlertProps) => ( 55 + <Html> 56 + <Head /> 57 + <Preview> 58 + A fine-grained personal access token has been added to your account 59 + </Preview> 60 + <Body style={styles.main}> 61 + <Layout> 62 + <Container 63 + style={{ 64 + backgroundColor: getIcon(props.type).color, 65 + display: "flex", 66 + alignItems: "center", 67 + justifyContent: "center", 68 + width: "40px", 69 + height: "40px", 70 + borderRadius: "50%", 71 + }} 72 + > 73 + <Img src={getIcon(props.type).src} width="24" height="24" /> 74 + </Container> 75 + <Row> 76 + <Column> 77 + <Heading as="h4">{props.name}</Heading> 78 + </Column> 79 + <Column style={{ textAlign: "right" }}> 80 + <Text 81 + style={{ 82 + color: getIcon(props.type).color, 83 + textTransform: "uppercase", 84 + }} 85 + > 86 + {props.type} 87 + </Text> 88 + </Column> 89 + </Row> 90 + <Row style={styles.row}> 91 + <Column> 92 + <Text style={styles.bold}>Request</Text> 93 + </Column> 94 + <Column 95 + style={{ 96 + textAlign: "right", 97 + flexWrap: "wrap", 98 + wordWrap: "break-word", 99 + maxWidth: "300px", 100 + }} 101 + > 102 + <Text> 103 + <CodeInline>{props.method}</CodeInline> {props.url} 104 + </Text> 105 + </Column> 106 + </Row> 107 + <Row style={styles.row}> 108 + <Column> 109 + <Text style={styles.bold}>Status</Text> 110 + </Column> 111 + <Column style={{ textAlign: "right" }}> 112 + <Text>{props.status}</Text> 113 + </Column> 114 + </Row> 115 + <Row style={styles.row}> 116 + <Column> 117 + <Text style={styles.bold}>Latency</Text> 118 + </Column> 119 + <Column style={{ textAlign: "right" }}> 120 + <Text>{props.latency}</Text> 121 + </Column> 122 + </Row> 123 + <Row style={styles.row}> 124 + <Column> 125 + <Text style={styles.bold}>Location</Text> 126 + </Column> 127 + <Column style={{ textAlign: "right" }}> 128 + <Text>{props.location}</Text> 129 + </Column> 130 + </Row> 131 + <Row style={styles.row}> 132 + <Column> 133 + <Text style={styles.bold}>Timestamp</Text> 134 + </Column> 135 + <Column style={{ textAlign: "right" }}> 136 + <Text>{props.timestamp}</Text> 137 + </Column> 138 + </Row> 139 + <Row style={styles.row}> 140 + <Column> 141 + <Text style={{ textAlign: "center" }}> 142 + <Button style={styles.button}>View details</Button> 143 + </Text> 144 + </Column> 145 + </Row> 146 + </Layout> 147 + </Body> 148 + </Html> 149 + ); 150 + 151 + MonitorAlertEmail.PreviewProps = { 152 + type: "down", 153 + name: "Ping Pong", 154 + url: "https://openstatus.dev/ping", 155 + method: "GET", 156 + status: "200", 157 + latency: "300ms", 158 + location: "San Francisco", 159 + timestamp: "2021-10-13T17:29:00Z", 160 + } satisfies MonitorAlertProps; 161 + 162 + export default MonitorAlertEmail;
+70
packages/emails/emails/page-subscription.tsx
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Head, 6 + Heading, 7 + Html, 8 + Link, 9 + Preview, 10 + Text, 11 + } from "@react-email/components"; 12 + import { Layout } from "./_components/layout"; 13 + import { styles } from "./_components/styles"; 14 + 15 + export interface PageSubscriptionProps { 16 + token: string; 17 + page: string; 18 + domain: string; 19 + img?: { 20 + src: string; 21 + alt: string; 22 + }; 23 + } 24 + 25 + const PageSubscriptionEmail = ({ 26 + token, 27 + page, 28 + domain, 29 + img, 30 + }: PageSubscriptionProps) => { 31 + return ( 32 + <Html> 33 + <Head /> 34 + <Preview>Confirm your subscription to "{page}" Status Page</Preview> 35 + <Body style={styles.main}> 36 + <Layout img={img}> 37 + <Heading as="h3"> 38 + Confirm your subscription to "{page}" Status Page 39 + </Heading> 40 + <Text> 41 + You are receiving this email because you subscribed to receive 42 + updates from "{page}" Status Page. 43 + </Text> 44 + <Text> 45 + To confirm your subscription, please click the link below. The link 46 + is valid for 7 days. If you believe this is a mistake, please ignore 47 + this email. 48 + </Text> 49 + <Text> 50 + <Link 51 + style={styles.link} 52 + href={`https://${domain}.openstatus.dev/verify/${token}`} 53 + > 54 + Confirm subscription 55 + </Link> 56 + </Text> 57 + </Layout> 58 + </Body> 59 + </Html> 60 + ); 61 + }; 62 + 63 + PageSubscriptionEmail.PreviewProps = { 64 + token: "token", 65 + page: "OpenStatus", 66 + domain: "slug", 67 + } satisfies PageSubscriptionProps; 68 + 69 + export { PageSubscriptionEmail }; 70 + export default PageSubscriptionEmail;
+119
packages/emails/emails/status-report.tsx
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Column, 6 + Head, 7 + Heading, 8 + Html, 9 + Preview, 10 + Row, 11 + Text, 12 + } from "@react-email/components"; 13 + import { Layout } from "./_components/layout"; 14 + import { colors, styles } from "./_components/styles"; 15 + 16 + export interface StatusReportProps { 17 + pageTitle: string; 18 + status: "investigating" | "identified" | "monitoring" | "resolved"; 19 + date: string; 20 + message: string; 21 + reportTitle: string; 22 + monitors: string[]; // array of monitor names 23 + } 24 + 25 + function getStatusColor(status: string) { 26 + switch (status) { 27 + case "investigating": 28 + return colors.danger; 29 + case "identified": 30 + return colors.warning; 31 + case "resolved": 32 + return colors.success; 33 + case "monitoring": 34 + return colors.info; 35 + default: 36 + return colors.success; 37 + } 38 + } 39 + 40 + function StatusReportEmail({ 41 + status, 42 + date, 43 + message, 44 + reportTitle, 45 + pageTitle, 46 + monitors, 47 + }: StatusReportProps) { 48 + return ( 49 + <Html> 50 + <Head /> 51 + <Preview>There are new updates on "{pageTitle}" page</Preview> 52 + <Body style={styles.main}> 53 + <Layout> 54 + <Row> 55 + <Column> 56 + <Heading as="h3">{pageTitle}</Heading> 57 + </Column> 58 + <Column style={{ textAlign: "right" }}> 59 + <Text 60 + style={{ 61 + color: getStatusColor(status), 62 + textTransform: "uppercase", 63 + }} 64 + > 65 + {status} 66 + </Text> 67 + </Column> 68 + </Row> 69 + <Row style={styles.row}> 70 + <Column> 71 + <Text style={styles.bold}>Title</Text> 72 + </Column> 73 + <Column style={{ textAlign: "right" }}> 74 + <Text>{reportTitle}</Text> 75 + </Column> 76 + </Row> 77 + <Row style={styles.row}> 78 + <Column> 79 + <Text style={styles.bold}>Date</Text> 80 + </Column> 81 + <Column style={{ textAlign: "right" }}> 82 + <Text>{date}</Text> 83 + </Column> 84 + </Row> 85 + <Row style={styles.row}> 86 + <Column> 87 + <Text style={styles.bold}>Affected</Text> 88 + </Column> 89 + <Column style={{ textAlign: "right" }}> 90 + <Text style={{ flexWrap: "wrap", wordWrap: "break-word" }}> 91 + {monitors.join(", ")} 92 + </Text> 93 + </Column> 94 + </Row> 95 + <Row style={styles.row}> 96 + <Column> 97 + <Text>{message}</Text> 98 + </Column> 99 + </Row> 100 + </Layout> 101 + </Body> 102 + </Html> 103 + ); 104 + } 105 + 106 + // TODO: add unsubscribe link! 107 + 108 + StatusReportEmail.PreviewProps = { 109 + pageTitle: "OpenStatus Status", 110 + reportTitle: "API Unavaible", 111 + status: "investigating", 112 + date: "2021-07-19", 113 + message: 114 + "The API is down, including the webhook. We are actively investigating the issue and will provide updates as soon as possible.", 115 + monitors: ["OpenStatus API", "OpenStatus Webhook"], 116 + }; 117 + 118 + export { StatusReportEmail }; 119 + export default StatusReportEmail;
+16 -9
packages/emails/emails/subscribe.tsx
··· 2 2 3 3 import { Body, Head, Html, Link, Preview } from "@react-email/components"; 4 4 5 - export const SubscribeEmail = ({ 6 - token, 7 - page, 8 - domain, 9 - }: { 5 + interface SubscribeProps { 10 6 token: string; 11 7 page: string; 12 8 domain: string; 13 - }) => { 9 + } 10 + 11 + const SubscribeEmail = ({ token, page, domain }: SubscribeProps) => { 14 12 return ( 15 13 <Html> 16 14 <Head> 17 - <title>Confirm your subscription to {page} Status Page</title> 18 - <Preview>Confirm your subscription to {page} Status Page</Preview> 15 + <title>Confirm your subscription to "{page}" Status Page</title> 16 + <Preview>Confirm your subscription to "{page}" Status Page</Preview> 19 17 <Body> 20 - <p>Confirm your subscription to {page} Status Page</p> 18 + <p>Confirm your subscription to "{page}" Status Page</p> 21 19 <p> 22 20 You are receiving this email because you subscribed to receive 23 21 updates from {page} Status Page. ··· 38 36 </Html> 39 37 ); 40 38 }; 39 + 40 + SubscribeEmail.PreviewProps = { 41 + token: "token", 42 + page: "OpenStatus", 43 + domain: "slug", 44 + } satisfies SubscribeProps; 45 + 46 + export { SubscribeEmail }; 47 + export default SubscribeEmail;
+61
packages/emails/emails/team-invitation.tsx
··· 1 + /** @jsxImportSource react */ 2 + 3 + import { 4 + Body, 5 + Head, 6 + Heading, 7 + Html, 8 + Link, 9 + Preview, 10 + Text, 11 + } from "@react-email/components"; 12 + import { Layout } from "./_components/layout"; 13 + import { styles } from "./_components/styles"; 14 + 15 + export interface TeamInvitationProps { 16 + invitedBy: string; // email address 17 + workspaceName?: string; 18 + token: string; 19 + } 20 + 21 + const TeamInvitationEmail = ({ 22 + token, 23 + workspaceName, 24 + invitedBy, 25 + }: TeamInvitationProps) => { 26 + return ( 27 + <Html> 28 + <Head /> 29 + <Preview>You have been invited to join OpenStatus.dev</Preview> 30 + <Body style={styles.main}> 31 + <Layout> 32 + <Heading as="h3"> 33 + You have been invited to join{" "} 34 + {`"${workspaceName}" workspace` || "OpenStatus.dev"} by {invitedBy} 35 + </Heading> 36 + <Text> 37 + Click here to access the workspace:{" "} 38 + <Link 39 + style={styles.link} 40 + href={`https://openstatus.dev/app/invite?token=${token}`} 41 + > 42 + accept invitation 43 + </Link> 44 + </Text> 45 + <Text> 46 + If you don't have an account yet, it will require you to create one. 47 + </Text> 48 + </Layout> 49 + </Body> 50 + </Html> 51 + ); 52 + }; 53 + 54 + TeamInvitationEmail.PreviewProps = { 55 + token: "token", 56 + workspaceName: "OpenStatus", 57 + invitedBy: "max@openstatus.dev", 58 + } satisfies TeamInvitationProps; 59 + 60 + export { TeamInvitationEmail }; 61 + export default TeamInvitationEmail;
+1
packages/emails/emails/welcome.tsx
··· 58 58 }; 59 59 60 60 export { WelcomeEmail }; 61 + export default WelcomeEmail;
+112
packages/emails/src/client.tsx
··· 3 3 import { render } from "@react-email/render"; 4 4 import { Resend } from "resend"; 5 5 import { FollowUpEmail } from "../emails/followup"; 6 + import { 7 + MonitorAlertEmail, 8 + type MonitorAlertProps, 9 + } from "../emails/monitor-alert"; 10 + import { 11 + PageSubscriptionEmail, 12 + type PageSubscriptionProps, 13 + } from "../emails/page-subscription"; 14 + import { 15 + StatusReportEmail, 16 + type StatusReportProps, 17 + } from "../emails/status-report"; 18 + import { 19 + TeamInvitationEmail, 20 + type TeamInvitationProps, 21 + } from "../emails/team-invitation"; 6 22 7 23 export class EmailClient { 8 24 public readonly client: Resend; ··· 31 47 throw result.error; 32 48 } catch (err) { 33 49 console.error(`Error sending follow up email to ${req.to}: ${err}`); 50 + } 51 + } 52 + 53 + public async sendStatusReportUpdate(req: StatusReportProps & { to: string }) { 54 + if (process.env.NODE_ENV === "development") return; 55 + 56 + try { 57 + const html = await render(<StatusReportEmail {...req} />); 58 + const result = await this.client.emails.send({ 59 + from: `${req.pageTitle} <notifications@openstatus.dev>`, 60 + subject: req.reportTitle, 61 + to: req.to, 62 + html, 63 + }); 64 + 65 + if (!result.error) { 66 + console.log(`Sent status report update email to ${req.to}`); 67 + return; 68 + } 69 + 70 + throw result.error; 71 + } catch (err) { 72 + console.error( 73 + `Error sending status report update email to ${req.to}: ${err}`, 74 + ); 75 + } 76 + } 77 + 78 + public async sendTeamInvitation(req: TeamInvitationProps & { to: string }) { 79 + if (process.env.NODE_ENV === "development") return; 80 + 81 + try { 82 + const html = await render(<TeamInvitationEmail {...req} />); 83 + const result = await this.client.emails.send({ 84 + from: `${req.workspaceName ?? "OpenStatus"} <notifications@openstatus.dev>`, 85 + subject: `You've been invited to join ${req.workspaceName ?? "OpenStatus"}`, 86 + to: req.to, 87 + html, 88 + }); 89 + 90 + if (!result.error) { 91 + console.log(`Sent team invitation email to ${req.to}`); 92 + return; 93 + } 94 + 95 + throw result.error; 96 + } catch (err) { 97 + console.error(`Error sending team invitation email to ${req.to}: ${err}`); 98 + } 99 + } 100 + 101 + public async sendMonitorAlert(req: MonitorAlertProps & { to: string }) { 102 + if (process.env.NODE_ENV === "development") return; 103 + 104 + try { 105 + const html = await render(<MonitorAlertEmail {...req} />); 106 + const result = await this.client.emails.send({ 107 + from: "OpenStatus <notifications@openstatus.dev>", 108 + subject: `${req.name}: ${req.type.toUpperCase()}`, 109 + to: req.to, 110 + html, 111 + }); 112 + 113 + if (!result.error) { 114 + console.log(`Sent monitor alert email to ${req.to}`); 115 + return; 116 + } 117 + 118 + throw result.error; 119 + } catch (err) { 120 + console.error(`Error sending monitor alert to ${req.to}: ${err}`); 121 + } 122 + } 123 + 124 + public async sendPageSubscription( 125 + req: PageSubscriptionProps & { to: string }, 126 + ) { 127 + if (process.env.NODE_ENV === "development") return; 128 + 129 + try { 130 + const html = await render(<PageSubscriptionEmail {...req} />); 131 + const result = await this.client.emails.send({ 132 + from: `${req.page} <notifications@openstatus.dev>`, 133 + subject: "Status page subscription", 134 + to: req.to, 135 + html, 136 + }); 137 + 138 + if (!result.error) { 139 + console.log(`Sent page subscription email to ${req.to}`); 140 + return; 141 + } 142 + 143 + throw result.error; 144 + } catch (err) { 145 + console.error(`Error sending page subscription to ${req.to}: ${err}`); 34 146 } 35 147 } 36 148 }
+1 -2
packages/emails/src/index.ts
··· 1 - import { Alert, EmailDataSchema } from "../emails/alert"; 1 + import { Alert } from "../emails/alert"; 2 2 import { FollowUpEmail } from "../emails/followup"; 3 3 import { SubscribeEmail } from "../emails/subscribe"; 4 4 import { WelcomeEmail } from "../emails/welcome"; ··· 8 8 WelcomeEmail, 9 9 validateEmailNotDisposable, 10 10 Alert, 11 - EmailDataSchema, 12 11 SubscribeEmail, 13 12 FollowUpEmail, 14 13 };