first commit
This commit is contained in:
41
calcom/packages/emails/src/components/AppsStatus.tsx
Normal file
41
calcom/packages/emails/src/components/AppsStatus.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
export const AppsStatus = (props: { calEvent: CalendarEvent; t: TFunction }) => {
|
||||
const { t } = props;
|
||||
if (!props.calEvent.appsStatus) return null;
|
||||
return (
|
||||
<Info
|
||||
label={t("apps_status")}
|
||||
description={
|
||||
<ul style={{ lineHeight: "24px" }} data-testid="appsStatus">
|
||||
{props.calEvent.appsStatus.map((status) => (
|
||||
<li key={status.type} style={{ fontWeight: 400 }}>
|
||||
{status.appName}{" "}
|
||||
{status.success >= 1 && `✅ ${status.success > 1 ? `(x${status.success})` : ""}`}
|
||||
{status.failures >= 1 && `❌ ${status.failures > 1 ? `(x${status.failures})` : ""}`}
|
||||
{status.warnings && status.warnings.length >= 1 && (
|
||||
<ul style={{ fontSize: "14px" }}>
|
||||
{status.warnings.map((warning, i) => (
|
||||
<li key={i}>{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{status.errors.length >= 1 && (
|
||||
<ul>
|
||||
{status.errors.map((error, i) => (
|
||||
<li key={i}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
);
|
||||
};
|
||||
206
calcom/packages/emails/src/components/BaseEmailHtml.tsx
Normal file
206
calcom/packages/emails/src/components/BaseEmailHtml.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/* eslint-disable @next/next/no-head-element */
|
||||
import BaseTable from "./BaseTable";
|
||||
import EmailBodyLogo from "./EmailBodyLogo";
|
||||
import EmailHead from "./EmailHead";
|
||||
import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent";
|
||||
import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider";
|
||||
import type { BodyHeadType } from "./EmailSchedulingBodyHeader";
|
||||
import EmailSchedulingBodyHeader from "./EmailSchedulingBodyHeader";
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
const Html = (props: { children: React.ReactNode }) => (
|
||||
<>
|
||||
<RawHtml html="<!doctype html>" />
|
||||
<html>{props.children}</html>
|
||||
</>
|
||||
);
|
||||
|
||||
export const BaseEmailHtml = (props: {
|
||||
children: React.ReactNode;
|
||||
callToAction?: React.ReactNode;
|
||||
subject: string;
|
||||
title?: string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
headerType?: BodyHeadType;
|
||||
hideLogo?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Html>
|
||||
<EmailHead title={props.subject} />
|
||||
<body style={{ wordSpacing: "normal", backgroundColor: "#F3F4F6" }}>
|
||||
<div style={{ backgroundColor: "#F3F4F6" }}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div style={{ margin: "0px auto", maxWidth: 600 }}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: "0px",
|
||||
padding: "0px",
|
||||
paddingTop: "40px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->`}
|
||||
/>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #E5E7EB",
|
||||
padding: "2px",
|
||||
backgroundColor: "#FFFFFF",
|
||||
}}>
|
||||
{props.headerType && (
|
||||
<EmailSchedulingBodyHeader headerType={props.headerType} headStyles={{ border: 0 }} />
|
||||
)}
|
||||
{props.title && (
|
||||
<EmailScheduledBodyHeaderContent
|
||||
headStyles={{ border: 0 }}
|
||||
title={props.title}
|
||||
subtitle={props.subtitle}
|
||||
/>
|
||||
)}
|
||||
{(props.headerType || props.title || props.subtitle) && (
|
||||
<EmailSchedulingBodyDivider headStyles={{ border: 0 }} />
|
||||
)}
|
||||
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row
|
||||
align="center"
|
||||
border="0"
|
||||
style={{ background: "#FFFFFF", backgroundColor: "#FFFFFF", width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<Row border="0" style={{ verticalAlign: "top" }} width="100%">
|
||||
<td align="left" style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#101010",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
{props.callToAction && <EmailSchedulingBodyDivider headStyles={{ border: 0 }} />}
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row
|
||||
align="center"
|
||||
border="0"
|
||||
style={{ background: "#FFFFFF", backgroundColor: "#FFFFFF", width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
{props.callToAction && (
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<BaseTable border="0" style={{ verticalAlign: "top" }} width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
vertical-align="middle"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
{props.callToAction}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 13,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</BaseTable>
|
||||
</div>
|
||||
)}
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
{!Boolean(props.hideLogo) && <EmailBodyLogo />}
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</div>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
15
calcom/packages/emails/src/components/BaseTable.tsx
Normal file
15
calcom/packages/emails/src/components/BaseTable.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
type BaseTableProps = Omit<
|
||||
React.DetailedHTMLProps<React.TableHTMLAttributes<HTMLTableElement>, HTMLTableElement>,
|
||||
"border"
|
||||
> &
|
||||
Partial<Pick<HTMLTableElement, "align" | "border">>;
|
||||
|
||||
const BaseTable = ({ children, ...rest }: BaseTableProps) => (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<table cellPadding="0" cellSpacing="0" role="presentation" {...rest}>
|
||||
{children}
|
||||
</table>
|
||||
);
|
||||
|
||||
export default BaseTable;
|
||||
75
calcom/packages/emails/src/components/CallToAction.tsx
Normal file
75
calcom/packages/emails/src/components/CallToAction.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { CallToActionIcon } from "./CallToActionIcon";
|
||||
|
||||
export const CallToAction = (props: {
|
||||
label: string;
|
||||
href: string;
|
||||
secondary?: boolean;
|
||||
startIconName?: string;
|
||||
endIconName?: string;
|
||||
}) => {
|
||||
const { label, href, secondary, startIconName, endIconName } = props;
|
||||
|
||||
const calculatePadding = () => {
|
||||
const paddingTop = "0.625rem";
|
||||
const paddingBottom = "0.625rem";
|
||||
let paddingLeft = "1rem";
|
||||
let paddingRight = "1rem";
|
||||
|
||||
if (startIconName) {
|
||||
paddingLeft = "0.875rem";
|
||||
} else if (endIconName) {
|
||||
paddingRight = "0.875rem";
|
||||
}
|
||||
|
||||
return `${paddingTop} ${paddingRight} ${paddingBottom} ${paddingLeft}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<p
|
||||
style={{
|
||||
display: "inline-block",
|
||||
background: secondary ? "#FFFFFF" : "#292929",
|
||||
border: secondary ? "1px solid #d1d5db" : "",
|
||||
color: "#ffffff",
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 500,
|
||||
lineHeight: "1rem",
|
||||
margin: 0,
|
||||
textDecoration: "none",
|
||||
textTransform: "none",
|
||||
padding: calculatePadding(),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
msoPaddingAlt: "0px",
|
||||
borderRadius: "6px",
|
||||
boxSizing: "border-box",
|
||||
height: "2.25rem",
|
||||
}}>
|
||||
<a
|
||||
style={{
|
||||
color: secondary ? "#292929" : "#FFFFFF",
|
||||
textDecoration: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: "auto",
|
||||
}}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{startIconName && (
|
||||
<CallToActionIcon
|
||||
style={{
|
||||
marginRight: "0.5rem",
|
||||
marginLeft: 0,
|
||||
}}
|
||||
iconName={startIconName}
|
||||
/>
|
||||
)}
|
||||
{label}
|
||||
{endIconName && <CallToActionIcon iconName={endIconName} />}
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
18
calcom/packages/emails/src/components/CallToActionIcon.tsx
Normal file
18
calcom/packages/emails/src/components/CallToActionIcon.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
export const CallToActionIcon = ({ iconName, style }: { iconName: string; style?: React.CSSProperties }) => (
|
||||
<img
|
||||
src={`${WEBAPP_URL}/emails/${iconName}.png`}
|
||||
srcSet={`${WEBAPP_URL}/emails/${iconName}.svg`}
|
||||
width="1rem"
|
||||
style={{
|
||||
height: "1rem",
|
||||
width: "1rem",
|
||||
marginLeft: "0.5rem",
|
||||
...style,
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
22
calcom/packages/emails/src/components/CallToActionTable.tsx
Normal file
22
calcom/packages/emails/src/components/CallToActionTable.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
export const CallToActionTable = (props: { children: React.ReactNode }) => (
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
role="presentation"
|
||||
style={{
|
||||
border: "none",
|
||||
borderRadius: "3px",
|
||||
cursor: "auto",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
msoPaddingAlt: "10px 25px",
|
||||
}}
|
||||
valign="middle">
|
||||
{props.children}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
79
calcom/packages/emails/src/components/EmailBodyLogo.tsx
Normal file
79
calcom/packages/emails/src/components/EmailBodyLogo.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
const CommentIE = ({ html = "" }) => <RawHtml html={`<!--[if mso | IE]>${html}<![endif]-->`} />;
|
||||
|
||||
const EmailBodyLogo = () => {
|
||||
const image = `${WEBAPP_URL}/emails/logo.png`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommentIE
|
||||
html={`</td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"></td>`}
|
||||
/>
|
||||
<div style={{ margin: "0px auto", maxWidth: 600 }}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: "0px",
|
||||
padding: "0px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<CommentIE
|
||||
html={`<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:600px;" >`}
|
||||
/>
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: "0px",
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<Row border="0" style={{ verticalAlign: "top" }} width="100%">
|
||||
<td
|
||||
align="center"
|
||||
style={{
|
||||
fontSize: "0px",
|
||||
padding: "10px 25px",
|
||||
paddingTop: "32px",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<Row border="0" style={{ borderCollapse: "collapse", borderSpacing: "0px" }}>
|
||||
<td style={{ width: "89px" }}>
|
||||
<a href={WEBAPP_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
height="19"
|
||||
src={image}
|
||||
style={{
|
||||
border: "0",
|
||||
display: "block",
|
||||
outline: "none",
|
||||
textDecoration: "none",
|
||||
height: "19px",
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
width="89"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</td>
|
||||
</Row>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<CommentIE html="</td></tr></table>" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBodyLogo;
|
||||
71
calcom/packages/emails/src/components/EmailCommonDivider.tsx
Normal file
71
calcom/packages/emails/src/components/EmailCommonDivider.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
const EmailCommonDivider = ({
|
||||
children,
|
||||
mutipleRows = false,
|
||||
headStyles,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
mutipleRows?: boolean;
|
||||
headStyles?: React.DetailedHTMLProps<
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>,
|
||||
HTMLTableCellElement
|
||||
>["style"];
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row
|
||||
align="center"
|
||||
border="0"
|
||||
style={{
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
width: "100%",
|
||||
}}>
|
||||
<td
|
||||
style={{
|
||||
borderLeft: "1px solid #E1E1E1",
|
||||
borderRight: "1px solid #E1E1E1",
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: "15px 0px 0 0px",
|
||||
textAlign: "center",
|
||||
...headStyles,
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<Row border="0" style={{ verticalAlign: "top" }} width="100%" multiple={mutipleRows}>
|
||||
{children}
|
||||
</Row>
|
||||
</div>
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailCommonDivider;
|
||||
91
calcom/packages/emails/src/components/EmailHead.tsx
Normal file
91
calcom/packages/emails/src/components/EmailHead.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable @next/next/no-head-element */
|
||||
import RawHtml from "./RawHtml";
|
||||
|
||||
const EmailHead = ({ title = "" }) => {
|
||||
return (
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<RawHtml
|
||||
html={`<!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->`}
|
||||
/>
|
||||
<meta httpEquiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
{`
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<RawHtml html="<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->" />
|
||||
<RawHtml
|
||||
html={`<!--[if lte mso 11]><style type="text/css">.mj-outlook-group-fix { width:100% !important; }</style><![endif]-->`}
|
||||
/>
|
||||
<RawHtml
|
||||
html={`<!--[if !mso]><!--><link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet" type="text/css"/>
|
||||
<style type="text/css">@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700);</style><!--<![endif]-->`}
|
||||
/>
|
||||
<style type="text/css">
|
||||
{`
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
{`
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
{`
|
||||
@media only screen and (max-width: 480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</head>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailHead;
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import EmailCommonDivider from "./EmailCommonDivider";
|
||||
|
||||
const EmailScheduledBodyHeaderContent = (props: {
|
||||
title: string;
|
||||
subtitle?: React.ReactNode;
|
||||
headStyles?: CSSProperties;
|
||||
}) => (
|
||||
<EmailCommonDivider headStyles={{ padding: 0, ...props.headStyles }} mutipleRows>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
padding: "10px 25px",
|
||||
paddingTop: 24,
|
||||
paddingBottom: 0,
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<div
|
||||
data-testid="heading"
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
color: "#111827",
|
||||
}}>
|
||||
{props.title}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{props.subtitle && (
|
||||
<tr>
|
||||
<td align="center" style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
data-testid="subHeading"
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
color: "#4B5563",
|
||||
}}>
|
||||
{props.subtitle}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</EmailCommonDivider>
|
||||
);
|
||||
|
||||
export default EmailScheduledBodyHeaderContent;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
import EmailCommonDivider from "./EmailCommonDivider";
|
||||
import RawHtml from "./RawHtml";
|
||||
|
||||
export const EmailSchedulingBodyDivider = (props: { headStyles?: CSSProperties }) => (
|
||||
<EmailCommonDivider headStyles={props.headStyles}>
|
||||
<td
|
||||
align="center"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
padding: "10px 25px",
|
||||
paddingBottom: 15,
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
borderTop: "solid 1px #E1E1E1",
|
||||
fontSize: 1,
|
||||
margin: "0px auto",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;"> </td></tr></table><![endif]-->`}
|
||||
/>
|
||||
</td>
|
||||
</EmailCommonDivider>
|
||||
);
|
||||
|
||||
export default EmailSchedulingBodyDivider;
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import EmailCommonDivider from "./EmailCommonDivider";
|
||||
import Row from "./Row";
|
||||
|
||||
export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle" | "teamCircle";
|
||||
|
||||
export const getHeadImage = (headerType: BodyHeadType): string => {
|
||||
switch (headerType) {
|
||||
case "checkCircle":
|
||||
return IS_PRODUCTION
|
||||
? `${BASE_URL}/emails/checkCircle@2x.png`
|
||||
: "https://app.cal.com/emails/checkCircle@2x.png";
|
||||
case "xCircle":
|
||||
return IS_PRODUCTION
|
||||
? `${BASE_URL}/emails/xCircle@2x.png`
|
||||
: "https://app.cal.com/emails/xCircle@2x.png";
|
||||
case "calendarCircle":
|
||||
return IS_PRODUCTION
|
||||
? `${BASE_URL}/emails/calendarCircle@2x.png`
|
||||
: "https://app.cal.com/emails/calendarCircle@2x.png";
|
||||
case "teamCircle":
|
||||
return IS_PRODUCTION
|
||||
? `${BASE_URL}/emails/teamCircle@2x.png`
|
||||
: "https://app.cal.com/emails/teamCircle@2x.png";
|
||||
}
|
||||
};
|
||||
|
||||
const EmailSchedulingBodyHeader = (props: { headerType: BodyHeadType; headStyles?: CSSProperties }) => {
|
||||
const image = getHeadImage(props.headerType);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EmailCommonDivider
|
||||
headStyles={{ padding: "30px 30px 0 30px", borderTop: "1px solid #E1E1E1", ...props.headStyles }}>
|
||||
<td
|
||||
align="center"
|
||||
style={{
|
||||
fontSize: "0px",
|
||||
padding: "10px 25px",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<Row border="0" role="presentation" style={{ borderCollapse: "collapse", borderSpacing: "0px" }}>
|
||||
<td style={{ width: 64 }}>
|
||||
<img
|
||||
height="64"
|
||||
src={image}
|
||||
style={{
|
||||
border: "0",
|
||||
display: "block",
|
||||
outline: "none",
|
||||
textDecoration: "none",
|
||||
height: "64px",
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
width="64"
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
</Row>
|
||||
</td>
|
||||
</EmailCommonDivider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailSchedulingBodyHeader;
|
||||
49
calcom/packages/emails/src/components/Info.tsx
Normal file
49
calcom/packages/emails/src/components/Info.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
|
||||
const Spacer = () => <p style={{ height: 6 }} />;
|
||||
|
||||
export const Info = (props: {
|
||||
label: string;
|
||||
description: React.ReactNode | undefined | null;
|
||||
extraInfo?: React.ReactNode;
|
||||
withSpacer?: boolean;
|
||||
lineThrough?: boolean;
|
||||
formatted?: boolean;
|
||||
}) => {
|
||||
if (!props.description || props.description === "") return null;
|
||||
|
||||
const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;";
|
||||
|
||||
const safeDescription = markdownToSafeHTML(props.description.toString()) || "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.withSpacer && <Spacer />}
|
||||
<div>
|
||||
<p style={{ color: "#101010" }}>{props.label}</p>
|
||||
<p
|
||||
style={{
|
||||
color: "#101010",
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
whiteSpace: "pre-wrap",
|
||||
textDecoration: props.lineThrough ? "line-through" : undefined,
|
||||
}}>
|
||||
{props.formatted ? (
|
||||
<p
|
||||
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: safeDescription
|
||||
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
|
||||
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
props.description
|
||||
)}
|
||||
</p>
|
||||
{props.extraInfo}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
87
calcom/packages/emails/src/components/LocationInfo.tsx
Normal file
87
calcom/packages/emails/src/components/LocationInfo.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { guessEventLocationType } from "@calcom/app-store/locations";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
|
||||
const { t } = props;
|
||||
|
||||
// We would not be able to determine provider name for DefaultEventLocationTypes
|
||||
const providerName = guessEventLocationType(props.calEvent.location)?.label;
|
||||
|
||||
const location = props.calEvent.location;
|
||||
let meetingUrl = location?.search(/^https?:/) !== -1 ? location : undefined;
|
||||
|
||||
if (props.calEvent) {
|
||||
meetingUrl = getVideoCallUrlFromCalEvent(props.calEvent) || meetingUrl;
|
||||
}
|
||||
|
||||
const isPhone = location?.startsWith("+");
|
||||
|
||||
// Because of location being a value here, we can determine the app that generated the location only for Dynamic Link based apps where the value is integrations:*
|
||||
// For static link based location apps, the value is that URL itself. So, it is not straightforward to determine the app that generated the location.
|
||||
// If we know the App we can always provide the name of the app like we do it for Google Hangout/Google Meet
|
||||
|
||||
if (meetingUrl) {
|
||||
return (
|
||||
<Info
|
||||
label={t("where")}
|
||||
withSpacer
|
||||
description={
|
||||
<a
|
||||
href={meetingUrl}
|
||||
target="_blank"
|
||||
title={t("meeting_url")}
|
||||
style={{ color: "#101010" }}
|
||||
rel="noreferrer">
|
||||
{providerName || "Link"}
|
||||
</a>
|
||||
}
|
||||
extraInfo={
|
||||
meetingUrl && (
|
||||
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{t("meeting_url")}:{" "}
|
||||
<a href={meetingUrl} title={t("meeting_url")} style={{ color: "#3E3E3E" }}>
|
||||
{meetingUrl}
|
||||
</a>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPhone) {
|
||||
return (
|
||||
<Info
|
||||
label={t("where")}
|
||||
withSpacer
|
||||
description={
|
||||
<a href={`tel:${location}`} title="Phone" style={{ color: "#3E3E3E" }}>
|
||||
{location}
|
||||
</a>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Info
|
||||
label={t("where")}
|
||||
withSpacer
|
||||
description={providerName || location}
|
||||
extraInfo={
|
||||
(providerName === "Zoom" || providerName === "Google") && props.calEvent.requiresConfirmation ? (
|
||||
<p style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{t("meeting_url_provided_after_confirmed")}</>
|
||||
</p>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
100
calcom/packages/emails/src/components/ManageLink.tsx
Normal file
100
calcom/packages/emails/src/components/ManageLink.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { getCancelLink, getRescheduleLink, getBookingUrl } from "@calcom/lib/CalEventParser";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
export function ManageLink(props: { calEvent: CalendarEvent; attendee: Person }) {
|
||||
// Only the original attendee can make changes to the event
|
||||
// Guests cannot
|
||||
const t = props.attendee.language.translate;
|
||||
const cancelLink = getCancelLink(props.calEvent);
|
||||
const rescheduleLink = getRescheduleLink(props.calEvent);
|
||||
const bookingLink = getBookingUrl(props.calEvent);
|
||||
|
||||
const isOriginalAttendee = props.attendee.email === props.calEvent.attendees[0]?.email;
|
||||
const isOrganizer = props.calEvent.organizer.email === props.attendee.email;
|
||||
const hasCancelLink = Boolean(cancelLink);
|
||||
const hasRescheduleLink = Boolean(rescheduleLink);
|
||||
const hasBookingLink = Boolean(bookingLink);
|
||||
const isRecurringEvent = props.calEvent.recurringEvent;
|
||||
const shouldDisplayRescheduleLink = Boolean(hasRescheduleLink && !isRecurringEvent);
|
||||
|
||||
if (
|
||||
(isOriginalAttendee || isOrganizer) &&
|
||||
(hasCancelLink || (!isRecurringEvent && hasRescheduleLink) || hasBookingLink)
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
lineHeight: "0px",
|
||||
textAlign: "left",
|
||||
color: "#101010",
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
}}>
|
||||
<>{t("need_to_make_a_change")}</>
|
||||
|
||||
{shouldDisplayRescheduleLink && (
|
||||
<span>
|
||||
<a
|
||||
href={rescheduleLink}
|
||||
style={{
|
||||
color: "#374151",
|
||||
marginLeft: "5px",
|
||||
marginRight: "5px",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
<>{t("reschedule")}</>
|
||||
</a>
|
||||
{hasCancelLink && <>{t("or_lowercase")}</>}
|
||||
</span>
|
||||
)}
|
||||
{hasCancelLink && (
|
||||
<span>
|
||||
<a
|
||||
href={cancelLink}
|
||||
style={{
|
||||
color: "#374151",
|
||||
marginLeft: "5px",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
<>{t("cancel")}</>
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{props.calEvent.platformClientId && hasBookingLink && (
|
||||
<span>
|
||||
{(hasCancelLink || shouldDisplayRescheduleLink) && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "5px",
|
||||
}}>
|
||||
{t("or_lowercase")}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href={bookingLink}
|
||||
style={{
|
||||
color: "#374151",
|
||||
marginLeft: "5px",
|
||||
textDecoration: "underline",
|
||||
}}>
|
||||
<>{t("check_here")}</>
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't have the rights to the manage link
|
||||
return null;
|
||||
}
|
||||
6
calcom/packages/emails/src/components/RawHtml.tsx
Normal file
6
calcom/packages/emails/src/components/RawHtml.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @see https://gist.github.com/zomars/4c366a0118a5b7fb391529ab1f27527a */
|
||||
const RawHtml = ({ html = "" }) => (
|
||||
<script dangerouslySetInnerHTML={{ __html: `</script>${html}<script>` }} />
|
||||
);
|
||||
|
||||
export default RawHtml;
|
||||
15
calcom/packages/emails/src/components/Row.tsx
Normal file
15
calcom/packages/emails/src/components/Row.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import BaseTable from "./BaseTable";
|
||||
|
||||
const Row = ({
|
||||
children,
|
||||
multiple = false,
|
||||
...rest
|
||||
}: { children: React.ReactNode; multiple?: boolean } & ComponentProps<typeof BaseTable>) => (
|
||||
<BaseTable {...rest}>
|
||||
<tbody>{multiple ? children : <tr>{children}</tr>}</tbody>
|
||||
</BaseTable>
|
||||
);
|
||||
|
||||
export default Row;
|
||||
3
calcom/packages/emails/src/components/Separator.tsx
Normal file
3
calcom/packages/emails/src/components/Separator.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export const Separator = () => (
|
||||
<p style={{ width: "16px", height: "16px", display: "inline-block" }}> </p>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import getLabelValueMapFromResponses from "@calcom/lib/getLabelValueMapFromResponses";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
export function UserFieldsResponses(props: { calEvent: CalendarEvent; t: TFunction; isOrganizer?: boolean }) {
|
||||
const { t, isOrganizer = false } = props;
|
||||
const labelValueMap = getLabelValueMapFromResponses(props.calEvent, isOrganizer);
|
||||
|
||||
if (!labelValueMap) return null;
|
||||
return (
|
||||
<>
|
||||
{Object.keys(labelValueMap).map((key) =>
|
||||
labelValueMap[key] !== "" ? (
|
||||
<Info
|
||||
key={key}
|
||||
label={t(key)}
|
||||
description={
|
||||
typeof labelValueMap[key] === "boolean"
|
||||
? labelValueMap[key]
|
||||
? t("yes")
|
||||
: t("no")
|
||||
: `${labelValueMap[key] ? labelValueMap[key] : ""}`
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
191
calcom/packages/emails/src/components/V2BaseEmailHtml.tsx
Normal file
191
calcom/packages/emails/src/components/V2BaseEmailHtml.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
/* eslint-disable @next/next/no-head-element */
|
||||
import BaseTable from "./BaseTable";
|
||||
import EmailBodyLogo from "./EmailBodyLogo";
|
||||
import EmailHead from "./EmailHead";
|
||||
import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent";
|
||||
import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider";
|
||||
import EmailSchedulingBodyHeader, { BodyHeadType } from "./EmailSchedulingBodyHeader";
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
const Html = (props: { children: React.ReactNode }) => (
|
||||
<>
|
||||
<RawHtml html="<!doctype html>" />
|
||||
<html>{props.children}</html>
|
||||
</>
|
||||
);
|
||||
|
||||
export const V2BaseEmailHtml = (props: {
|
||||
children: React.ReactNode;
|
||||
callToAction?: React.ReactNode;
|
||||
subject: string;
|
||||
title?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
headerType?: BodyHeadType;
|
||||
}) => {
|
||||
return (
|
||||
<Html>
|
||||
<EmailHead title={props.subject} />
|
||||
<body style={{ wordSpacing: "normal", backgroundColor: "#F3F4F6" }}>
|
||||
<div style={{ backgroundColor: "#F3F4F6" }}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div style={{ margin: "0px auto", maxWidth: 600 }}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: "0px",
|
||||
padding: "0px",
|
||||
paddingTop: "40px",
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->`}
|
||||
/>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
{props.headerType && <EmailSchedulingBodyHeader headerType={props.headerType} />}
|
||||
{props.title && <EmailScheduledBodyHeaderContent title={props.title} subtitle={props.subtitle} />}
|
||||
{(props.headerType || props.title || props.subtitle) && <EmailSchedulingBodyDivider />}
|
||||
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
border: "1px solid #E1E1E1",
|
||||
borderRadius: "6px",
|
||||
}}>
|
||||
<Row
|
||||
border="0"
|
||||
style={{
|
||||
verticalAlign: "top",
|
||||
borderRadius: "6px",
|
||||
background: "#FFFFFF",
|
||||
backgroundColor: "#FFFFFF",
|
||||
}}
|
||||
width="100%">
|
||||
<td
|
||||
align="left"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
padding: "40px",
|
||||
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#3E3E3E",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
{props.callToAction && <EmailSchedulingBodyDivider />}
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" className="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
margin: "0px auto",
|
||||
maxWidth: 600,
|
||||
}}>
|
||||
<Row align="center" border="0" style={{ width: "100%" }}>
|
||||
<td
|
||||
style={{
|
||||
direction: "ltr",
|
||||
fontSize: 0,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
<RawHtml
|
||||
html={`<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td className="" style="vertical-align:top;width:598px;" ><![endif]-->`}
|
||||
/>
|
||||
{props.callToAction && (
|
||||
<div
|
||||
className="mj-column-per-100 mj-outlook-group-fix"
|
||||
style={{
|
||||
fontSize: 0,
|
||||
textAlign: "left",
|
||||
direction: "ltr",
|
||||
display: "inline-block",
|
||||
verticalAlign: "top",
|
||||
width: "100%",
|
||||
}}>
|
||||
<BaseTable border="0" style={{ verticalAlign: "top" }} width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
vertical-align="middle"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
{props.callToAction}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
align="left"
|
||||
style={{ fontSize: 0, padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: 13,
|
||||
lineHeight: 1,
|
||||
textAlign: "left",
|
||||
color: "#000000",
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</BaseTable>
|
||||
</div>
|
||||
)}
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</td>
|
||||
</Row>
|
||||
</div>
|
||||
<EmailBodyLogo />
|
||||
<RawHtml html="<!--[if mso | IE]></td></tr></table><![endif]-->" />
|
||||
</div>
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
74
calcom/packages/emails/src/components/WhenInfo.tsx
Normal file
74
calcom/packages/emails/src/components/WhenInfo.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { RRule } from "rrule";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
export function getRecurringWhen({
|
||||
recurringEvent,
|
||||
attendee,
|
||||
}: {
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
attendee: Pick<Person, "language">;
|
||||
}) {
|
||||
if (recurringEvent) {
|
||||
const t = attendee.language.translate;
|
||||
const rruleOptions = new RRule(recurringEvent).options;
|
||||
const recurringEventConfig: RecurringEvent = {
|
||||
freq: rruleOptions.freq,
|
||||
count: rruleOptions.count || 1,
|
||||
interval: rruleOptions.interval,
|
||||
};
|
||||
return `${getEveryFreqFor({ t, recurringEvent: recurringEventConfig })}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function WhenInfo(props: {
|
||||
calEvent: CalendarEvent;
|
||||
timeZone: string;
|
||||
t: TFunction;
|
||||
locale: string;
|
||||
timeFormat: TimeFormat;
|
||||
}) {
|
||||
const { timeZone, t, calEvent: { recurringEvent } = {}, locale, timeFormat } = props;
|
||||
|
||||
function getRecipientStart(format: string) {
|
||||
return dayjs(props.calEvent.startTime).tz(timeZone).locale(locale).format(format);
|
||||
}
|
||||
|
||||
function getRecipientEnd(format: string) {
|
||||
return dayjs(props.calEvent.endTime).tz(timeZone).locale(locale).format(format);
|
||||
}
|
||||
|
||||
const recurringInfo = getRecurringWhen({
|
||||
recurringEvent: props.calEvent.recurringEvent,
|
||||
attendee: props.calEvent.attendees[0],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Info
|
||||
label={`${t("when")} ${recurringInfo !== "" ? ` - ${recurringInfo}` : ""}`}
|
||||
lineThrough={
|
||||
!!props.calEvent.cancellationReason && !props.calEvent.cancellationReason.includes("$RCH$")
|
||||
}
|
||||
description={
|
||||
<span data-testid="when">
|
||||
{recurringEvent?.count ? `${t("starting")} ` : ""}
|
||||
{getRecipientStart(`dddd, LL | ${timeFormat}`)} - {getRecipientEnd(timeFormat)}{" "}
|
||||
<span style={{ color: "#4B5563" }}>({timeZone})</span>
|
||||
</span>
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
calcom/packages/emails/src/components/WhoInfo.tsx
Normal file
46
calcom/packages/emails/src/components/WhoInfo.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
const PersonInfo = ({ name = "", email = "", role = "" }) => (
|
||||
<div style={{ color: "#101010", fontWeight: 400, lineHeight: "24px" }}>
|
||||
{name} - {role}{" "}
|
||||
<span style={{ color: "#4B5563" }}>
|
||||
<a href={`mailto:${email}`} style={{ color: "#4B5563" }}>
|
||||
{email}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export function WhoInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
|
||||
const { t } = props;
|
||||
return (
|
||||
<Info
|
||||
label={t("who")}
|
||||
description={
|
||||
<>
|
||||
<PersonInfo
|
||||
name={props.calEvent.organizer.name}
|
||||
role={t("organizer")}
|
||||
email={props.calEvent.organizer.email}
|
||||
/>
|
||||
{props.calEvent.team?.members.map((member) => (
|
||||
<PersonInfo key={member.name} name={member.name} role={t("team_member")} email={member.email} />
|
||||
))}
|
||||
{props.calEvent.attendees.map((attendee) => (
|
||||
<PersonInfo
|
||||
key={attendee.id || attendee.name}
|
||||
name={attendee.name}
|
||||
role={t("guest")}
|
||||
email={attendee.email}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
calcom/packages/emails/src/components/index.ts
Normal file
14
calcom/packages/emails/src/components/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { BaseEmailHtml } from "./BaseEmailHtml";
|
||||
export { V2BaseEmailHtml } from "./V2BaseEmailHtml";
|
||||
export { CallToAction } from "./CallToAction";
|
||||
export { CallToActionTable } from "./CallToActionTable";
|
||||
export { UserFieldsResponses } from "./UserFieldsResponses";
|
||||
export { Info } from "./Info";
|
||||
export { CallToActionIcon } from "./CallToActionIcon";
|
||||
export { LocationInfo } from "./LocationInfo";
|
||||
export { ManageLink } from "./ManageLink";
|
||||
export { default as RawHtml } from "./RawHtml";
|
||||
export { WhenInfo } from "./WhenInfo";
|
||||
export { WhoInfo } from "./WhoInfo";
|
||||
export { AppsStatus } from "./AppsStatus";
|
||||
export { Separator } from "./Separator";
|
||||
22
calcom/packages/emails/src/renderEmail.ts
Normal file
22
calcom/packages/emails/src/renderEmail.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as templates from "./templates";
|
||||
|
||||
async function renderEmail<K extends keyof typeof templates>(
|
||||
template: K,
|
||||
props: React.ComponentProps<(typeof templates)[K]>
|
||||
) {
|
||||
const Component = templates[template];
|
||||
const ReactDOMServer = (await import("react-dom/server")).default;
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
ReactDOMServer.renderToStaticMarkup(Component(props))
|
||||
// Remove `<RawHtml />` injected scripts
|
||||
.replace(/<script><\/script>/g, "")
|
||||
.replace(
|
||||
"<html>",
|
||||
`<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default renderEmail;
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { Trans, type TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
type AdminOrganizationNotification = {
|
||||
language: TFunction;
|
||||
orgSlug: string;
|
||||
webappIPAddress: string;
|
||||
};
|
||||
|
||||
const dnsTable = (type: string, name: string, value: string, t: TFunction) => (
|
||||
<table
|
||||
role="presentation"
|
||||
border={0}
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
style={{
|
||||
verticalAlign: "top",
|
||||
marginTop: "10px",
|
||||
borderRadius: "6px",
|
||||
borderCollapse: "separate",
|
||||
border: "solid black 1px",
|
||||
}}
|
||||
width="100%">
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "black",
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
}}>
|
||||
<td
|
||||
align="center"
|
||||
width="33%"
|
||||
style={{ borderTopLeftRadius: "5px", borderRight: "1px solid white" }}>
|
||||
{t("type")}
|
||||
</td>
|
||||
<td align="center" width="33%" style={{ borderRight: "1px solid white" }}>
|
||||
{t("name")}
|
||||
</td>
|
||||
<td align="center" style={{ borderTopRightRadius: "5px" }}>
|
||||
{t("value")}
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr style={{ lineHeight: "24px" }}>
|
||||
<td align="center" style={{ borderBottomLeftRadius: "5px", borderRight: "1px solid black" }}>
|
||||
{type}
|
||||
</td>
|
||||
<td align="center" style={{ borderRight: "1px solid black" }}>
|
||||
{name}
|
||||
</td>
|
||||
<td align="center" style={{ borderBottomRightRadius: "5px" }}>
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
export const AdminOrganizationNotificationEmail = ({
|
||||
orgSlug,
|
||||
webappIPAddress,
|
||||
language,
|
||||
}: AdminOrganizationNotification) => {
|
||||
const webAppUrl = WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "");
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
subject={language("admin_org_notification_email_subject", { appName: APP_NAME })}
|
||||
callToAction={
|
||||
<CallToAction
|
||||
label={language("admin_org_notification_email_cta")}
|
||||
href={`${WEBAPP_URL}/settings/admin/organizations`}
|
||||
endIconName="white-arrow-right"
|
||||
/>
|
||||
}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "24px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{language("admin_org_notification_email_title")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{language("hi_admin")}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<Trans i18nKey="admin_org_notification_email_body_part1" t={language} values={{ orgSlug }}>
|
||||
An organization with slug {`"${orgSlug}"`} was created.
|
||||
<br />
|
||||
<br />
|
||||
Please be sure to configure your DNS registry to point the subdomain corresponding to the new
|
||||
organization to where the main app is running. Otherwise the organization will not work.
|
||||
<br />
|
||||
<br />
|
||||
Here are just the very basic options to configure a subdomain to point to their app so it loads the
|
||||
organization profile page.
|
||||
<br />
|
||||
<br />
|
||||
You can do it either with the A Record:
|
||||
</Trans>
|
||||
</p>
|
||||
{dnsTable("A", orgSlug, webappIPAddress, language)}
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{language("admin_org_notification_email_body_part2")}
|
||||
</p>
|
||||
{dnsTable("CNAME", orgSlug, webAppUrl, language)}
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{language("admin_org_notification_email_body_part3")}
|
||||
</p>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CallToAction, CallToActionTable } from "../components";
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
function ManageLink(props: React.ComponentProps<typeof AttendeeScheduledEmail>) {
|
||||
const manageText = props.attendee.language.translate("pay_now");
|
||||
|
||||
if (!props.calEvent.paymentInfo?.link) return null;
|
||||
|
||||
return (
|
||||
<CallToActionTable>
|
||||
<CallToAction label={manageText} href={props.calEvent.paymentInfo.link} endIconName="linkIcon" />
|
||||
</CallToActionTable>
|
||||
);
|
||||
}
|
||||
|
||||
export const AttendeeAwaitingPaymentEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => {
|
||||
return props.calEvent.paymentInfo?.paymentOption === "HOLD" ? (
|
||||
<AttendeeScheduledEmail
|
||||
title="meeting_awaiting_payment_method"
|
||||
headerType="calendarCircle"
|
||||
subject="awaiting_payment_subject"
|
||||
callToAction={<ManageLink {...props} />}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<AttendeeScheduledEmail
|
||||
title="meeting_awaiting_payment"
|
||||
headerType="calendarCircle"
|
||||
subject="awaiting_payment_subject"
|
||||
callToAction={<ManageLink {...props} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeCancelledEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title="event_request_cancelled"
|
||||
headerType="xCircle"
|
||||
subject="event_cancelled_subject"
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeCancelledSeatEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title="no_longer_attending"
|
||||
headerType="xCircle"
|
||||
subject="event_no_longer_attending_subject"
|
||||
subtitle=""
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeDeclinedEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title={
|
||||
props.calEvent.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
|
||||
}
|
||||
headerType="xCircle"
|
||||
subject="event_declined_subject"
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeLocationChangeEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title="event_location_changed"
|
||||
headerType="calendarCircle"
|
||||
subject="location_changed_event_type_subject"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import AttendeeScheduledEmailClass from "../../templates/attendee-rescheduled-email";
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeRequestEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => {
|
||||
const date = new AttendeeScheduledEmailClass(props.calEvent, props.attendee).getFormattedDate();
|
||||
|
||||
return (
|
||||
<AttendeeScheduledEmail
|
||||
title={props.calEvent.attendees[0].language.translate(
|
||||
props.calEvent.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted"
|
||||
)}
|
||||
subtitle={
|
||||
<>
|
||||
{props.calEvent.attendees[0].language.translate(
|
||||
props.calEvent.recurringEvent?.count
|
||||
? "user_needs_to_confirm_or_reject_booking_recurring"
|
||||
: "user_needs_to_confirm_or_reject_booking",
|
||||
{ user: props.calEvent.organizer.name }
|
||||
)}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject={props.calEvent.attendees[0].language.translate("booking_submitted_subject", {
|
||||
title: props.calEvent.title,
|
||||
date,
|
||||
})}
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
|
||||
export const AttendeeRescheduledEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title="event_has_been_rescheduled"
|
||||
headerType="calendarCircle"
|
||||
subject="event_type_has_been_rescheduled_on_time_date"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
export const AttendeeScheduledEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
locale={props.attendee.language.locale}
|
||||
timeZone={props.attendee.timeZone}
|
||||
t={props.attendee.language.translate}
|
||||
timeFormat={props.attendee?.timeFormat}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CallToAction, CallToActionTable } from "../components";
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const AttendeeWasRequestedToRescheduleEmail = (
|
||||
props: { metadata: { rescheduleLink: string } } & React.ComponentProps<typeof OrganizerScheduledEmail>
|
||||
) => {
|
||||
const t = props.attendee.language.translate;
|
||||
return (
|
||||
<OrganizerScheduledEmail
|
||||
t={t}
|
||||
title="request_reschedule_booking"
|
||||
subtitle={
|
||||
<>
|
||||
{t("request_reschedule_subtitle", {
|
||||
organizer: props.calEvent.organizer.name,
|
||||
})}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject="rescheduled_event_type_subject"
|
||||
callToAction={
|
||||
<CallToActionTable>
|
||||
<CallToAction label="Book a new time" href={props.metadata.rescheduleLink} endIconName="linkIcon" />
|
||||
</CallToActionTable>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
104
calcom/packages/emails/src/templates/BaseScheduledEmail.tsx
Normal file
104
calcom/packages/emails/src/templates/BaseScheduledEmail.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { formatPrice } from "@calcom/lib/price";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import {
|
||||
BaseEmailHtml,
|
||||
Info,
|
||||
LocationInfo,
|
||||
ManageLink,
|
||||
WhenInfo,
|
||||
WhoInfo,
|
||||
AppsStatus,
|
||||
UserFieldsResponses,
|
||||
} from "../components";
|
||||
|
||||
export const BaseScheduledEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
timeZone: string;
|
||||
includeAppsStatus?: boolean;
|
||||
t: TFunction;
|
||||
locale: string;
|
||||
timeFormat: TimeFormat | undefined;
|
||||
isOrganizer?: boolean;
|
||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
const { t, timeZone, locale, timeFormat: timeFormat_ } = props;
|
||||
|
||||
const timeFormat = timeFormat_ ?? TimeFormat.TWELVE_HOUR;
|
||||
|
||||
function getRecipientStart(format: string) {
|
||||
return dayjs(props.calEvent.startTime).tz(timeZone).format(format);
|
||||
}
|
||||
|
||||
function getRecipientEnd(format: string) {
|
||||
return dayjs(props.calEvent.endTime).tz(timeZone).format(format);
|
||||
}
|
||||
|
||||
const subject = t(props.subject || "confirmed_event_type_subject", {
|
||||
eventType: props.calEvent.type,
|
||||
name: props.calEvent.team?.name || props.calEvent.organizer.name,
|
||||
date: `${getRecipientStart("h:mma")} - ${getRecipientEnd("h:mma")}, ${t(
|
||||
getRecipientStart("dddd").toLowerCase()
|
||||
)}, ${t(getRecipientStart("MMMM").toLowerCase())} ${getRecipientStart("D, YYYY")}`,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
hideLogo={Boolean(props.calEvent.platformClientId)}
|
||||
headerType={props.headerType || "checkCircle"}
|
||||
subject={props.subject || subject}
|
||||
title={t(
|
||||
props.title
|
||||
? props.title
|
||||
: props.calEvent.recurringEvent?.count
|
||||
? "your_event_has_been_scheduled_recurring"
|
||||
: "your_event_has_been_scheduled"
|
||||
)}
|
||||
callToAction={
|
||||
props.callToAction === null
|
||||
? null
|
||||
: props.callToAction || <ManageLink attendee={props.attendee} calEvent={props.calEvent} />
|
||||
}
|
||||
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
||||
{props.calEvent.cancellationReason && (
|
||||
<Info
|
||||
label={t(
|
||||
props.calEvent.cancellationReason.startsWith("$RCH$")
|
||||
? "reason_for_reschedule"
|
||||
: "cancellation_reason"
|
||||
)}
|
||||
description={
|
||||
!!props.calEvent.cancellationReason && props.calEvent.cancellationReason.replace("$RCH$", "")
|
||||
} // Removing flag to distinguish reschedule from cancellation
|
||||
withSpacer
|
||||
/>
|
||||
)}
|
||||
<Info label={t("rejection_reason")} description={props.calEvent.rejectionReason} withSpacer />
|
||||
<Info label={t("what")} description={props.calEvent.title} withSpacer />
|
||||
<WhenInfo timeFormat={timeFormat} calEvent={props.calEvent} t={t} timeZone={timeZone} locale={locale} />
|
||||
<WhoInfo calEvent={props.calEvent} t={t} />
|
||||
<LocationInfo calEvent={props.calEvent} t={t} />
|
||||
<Info label={t("description")} description={props.calEvent.description} withSpacer formatted />
|
||||
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||
<UserFieldsResponses t={t} calEvent={props.calEvent} isOrganizer={props.isOrganizer} />
|
||||
{props.calEvent.paymentInfo?.amount && (
|
||||
<Info
|
||||
label={props.calEvent.paymentInfo.paymentOption === "HOLD" ? t("no_show_fee") : t("price")}
|
||||
description={formatPrice(
|
||||
props.calEvent.paymentInfo.amount,
|
||||
props.calEvent.paymentInfo.currency,
|
||||
props.attendee.language.locale
|
||||
)}
|
||||
withSpacer
|
||||
/>
|
||||
)}
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { IBookingRedirect } from "../../templates/booking-redirect-notification";
|
||||
import { BaseEmailHtml } from "../components";
|
||||
|
||||
export const BookingRedirectEmailNotification = (
|
||||
props: IBookingRedirect & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
subject={props.language("booking_redirect_email_subject")}
|
||||
title={props.language("booking_redirect_email_title")}>
|
||||
<p
|
||||
style={{
|
||||
color: "black",
|
||||
fontSize: "16px",
|
||||
lineHeight: "24px",
|
||||
fontWeight: "400",
|
||||
}}>
|
||||
{props.language("booking_redirect_email_description", {
|
||||
toName: props.toName,
|
||||
})}
|
||||
{props.dates}
|
||||
<br />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "16px",
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
127
calcom/packages/emails/src/templates/BrokenIntegrationEmail.tsx
Normal file
127
calcom/packages/emails/src/templates/BrokenIntegrationEmail.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { AppStoreLocationType } from "@calcom/app-store/locations";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
// https://stackoverflow.com/questions/56263980/get-key-of-an-enum-from-its-value-in-typescript
|
||||
export function getEnumKeyByEnumValue(myEnum: any, enumValue: number | string): string {
|
||||
const keys = Object.keys(myEnum).filter((x) => myEnum[x] == enumValue);
|
||||
return keys.length > 0 ? keys[0] : "";
|
||||
}
|
||||
|
||||
const BrokenVideoIntegration = (props: { location: string; eventTypeId?: number | null; t: TFunction }) => {
|
||||
return (
|
||||
<Trans i18nKey="broken_video_action" t={props.t}>
|
||||
We could not add the <span>{props.location}</span> meeting link to your scheduled event. Contact your
|
||||
invitees or update your calendar event to add the details. You can either
|
||||
<a
|
||||
href={
|
||||
props.eventTypeId ? `${WEBAPP_URL}/event-types/${props.eventTypeId}` : `${WEBAPP_URL}/event-types`
|
||||
}>
|
||||
change your location on the event type
|
||||
</a>
|
||||
or try
|
||||
<a href={`${WEBAPP_URL}/apps/installed`}>removing and adding the app again.</a>
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
const BrokenCalendarIntegration = (props: {
|
||||
calendar: string;
|
||||
eventTypeId?: number | null;
|
||||
t: TFunction;
|
||||
}) => {
|
||||
const { t } = props;
|
||||
|
||||
return (
|
||||
<Trans i18nKey="broken_calendar_action" t={props.t}>
|
||||
We could not update your <span>{props.calendar}</span>.{" "}
|
||||
<a href={`${WEBAPP_URL}/apps/installed`}>
|
||||
Please check your calendar settings or remove and add your calendar again
|
||||
</a>
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
export const BrokenIntegrationEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
type: "video" | "calendar";
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
const { calEvent, type } = props;
|
||||
const t = calEvent.organizer.language.translate;
|
||||
const locale = calEvent.organizer.language.locale;
|
||||
const timeFormat = calEvent.organizer?.timeFormat;
|
||||
|
||||
if (type === "video") {
|
||||
let location = calEvent.location ? getEnumKeyByEnumValue(AppStoreLocationType, calEvent.location) : " ";
|
||||
if (location === "Daily") {
|
||||
location = "Cal Video";
|
||||
}
|
||||
if (location === "GoogleMeet") {
|
||||
location = `${location.slice(0, 5)} ${location.slice(5)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
timeFormat={timeFormat}
|
||||
locale={locale}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_adding_video_link")}
|
||||
subtitle={<BrokenVideoIntegration location={location} eventTypeId={calEvent.eventTypeId} t={t} />}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "calendar") {
|
||||
// The calendar name is stored as name_calendar
|
||||
const [mainHostDestinationCalendar] = calEvent.destinationCalendar ?? [];
|
||||
let calendar = mainHostDestinationCalendar
|
||||
? mainHostDestinationCalendar?.integration.split("_")
|
||||
: "calendar";
|
||||
|
||||
if (Array.isArray(calendar)) {
|
||||
const calendarCap = calendar.map((name) => name.charAt(0).toUpperCase() + name.slice(1));
|
||||
calendar = `${calendarCap[0]} ${calendarCap[1]}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
timeFormat={timeFormat}
|
||||
locale={locale}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_updating_calendar")}
|
||||
subtitle={<BrokenCalendarIntegration calendar={calendar} eventTypeId={calEvent.eventTypeId} t={t} />}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
timeFormat={timeFormat}
|
||||
locale={locale}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_updating_calendar")}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { WEBAPP_URL, APP_NAME, COMPANY_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { V2BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
interface DailyVideoDownloadRecordingEmailProps {
|
||||
language: TFunction;
|
||||
downloadLink: string;
|
||||
title: string;
|
||||
date: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const DailyVideoDownloadRecordingEmail = (
|
||||
props: DailyVideoDownloadRecordingEmailProps & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
const image = `${WEBAPP_URL}/emails/logo.png`;
|
||||
return (
|
||||
<V2BaseEmailHtml
|
||||
subject={props.language("download_your_recording", {
|
||||
title: props.title,
|
||||
date: props.date,
|
||||
})}>
|
||||
<div style={{ width: "89px", marginBottom: "35px" }}>
|
||||
<a href={WEBAPP_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
height="19"
|
||||
src={image}
|
||||
style={{
|
||||
border: "0",
|
||||
display: "block",
|
||||
outline: "none",
|
||||
textDecoration: "none",
|
||||
height: "19px",
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
width="89"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "32px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "38.5px",
|
||||
marginBottom: "40px",
|
||||
color: "black",
|
||||
}}>
|
||||
<>{props.language("download_your_recording")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("hi_user_name", { name: props.name })},</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "40px" }}>
|
||||
<>{props.language("recording_from_your_recent_call", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#F3F4F6",
|
||||
padding: "32px",
|
||||
marginBottom: "40px",
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 600,
|
||||
marginBottom: "8px",
|
||||
color: "black",
|
||||
}}>
|
||||
<>{props.title}</>
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "24px",
|
||||
marginTop: "0px",
|
||||
color: "black",
|
||||
}}>
|
||||
{props.date}
|
||||
</p>
|
||||
<CallToAction label={props.language("download_recording")} href={props.downloadLink} />
|
||||
</div>
|
||||
|
||||
<p style={{ fontWeight: 500, lineHeight: "20px", marginTop: "8px" }}>
|
||||
<Trans i18nKey="link_valid_for_12_hrs">
|
||||
Note: The download link is valid only for 12 hours. You can generate new download link by following
|
||||
instructions
|
||||
<a href="https://cal.com/docs/enterprise-features/teams/cal-video-recordings"> here</a>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginTop: "32px", marginBottom: "8px" }}>
|
||||
<>{props.language("happy_scheduling")},</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginTop: "0px" }}>
|
||||
<>{props.language("the_calcom_team", { companyName: COMPANY_NAME })}</>
|
||||
</p>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { WEBAPP_URL, APP_NAME, COMPANY_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { V2BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
interface DailyVideoDownloadTranscriptEmailProps {
|
||||
language: TFunction;
|
||||
transcriptDownloadLinks: Array<string>;
|
||||
title: string;
|
||||
date: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const DailyVideoDownloadTranscriptEmail = (
|
||||
props: DailyVideoDownloadTranscriptEmailProps & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
const image = `${WEBAPP_URL}/emails/logo.png`;
|
||||
return (
|
||||
<V2BaseEmailHtml
|
||||
subject={props.language("download_transcript_email_subject", {
|
||||
title: props.title,
|
||||
date: props.date,
|
||||
})}>
|
||||
<div style={{ width: "89px", marginBottom: "35px" }}>
|
||||
<a href={WEBAPP_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
height="19"
|
||||
src={image}
|
||||
style={{
|
||||
border: "0",
|
||||
display: "block",
|
||||
outline: "none",
|
||||
textDecoration: "none",
|
||||
height: "19px",
|
||||
width: "100%",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
width="89"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "32px",
|
||||
fontWeight: "600",
|
||||
lineHeight: "38.5px",
|
||||
marginBottom: "40px",
|
||||
color: "black",
|
||||
}}>
|
||||
<>{props.language("download_your_transcripts")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("hi_user_name", { name: props.name })},</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "40px" }}>
|
||||
<>{props.language("transcript_from_previous_call", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
|
||||
{props.transcriptDownloadLinks.map((downloadLink, index) => {
|
||||
return (
|
||||
<div
|
||||
key={downloadLink}
|
||||
style={{
|
||||
backgroundColor: "#F3F4F6",
|
||||
padding: "32px",
|
||||
marginBottom: "40px",
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
lineHeight: "20px",
|
||||
fontWeight: 600,
|
||||
marginBottom: "8px",
|
||||
color: "black",
|
||||
}}>
|
||||
<>{props.title}</>
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "24px",
|
||||
marginTop: "0px",
|
||||
color: "black",
|
||||
}}>
|
||||
{props.date} Transcript {index + 1}
|
||||
</p>
|
||||
<CallToAction label={props.language("download_transcript")} href={downloadLink} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginTop: "32px", marginBottom: "8px" }}>
|
||||
<>{props.language("happy_scheduling")},</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", marginTop: "0px" }}>
|
||||
<>{props.language("the_calcom_team", { companyName: COMPANY_NAME })}</>
|
||||
</p>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
85
calcom/packages/emails/src/templates/DisabledAppEmail.tsx
Normal file
85
calcom/packages/emails/src/templates/DisabledAppEmail.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export const DisabledAppEmail = (
|
||||
props: {
|
||||
appName: string;
|
||||
appType: string[];
|
||||
t: TFunction;
|
||||
title?: string;
|
||||
eventTypeId?: number;
|
||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
const { title, appName, eventTypeId, t, appType } = props;
|
||||
|
||||
return (
|
||||
<BaseEmailHtml subject={t("app_disabled", { appName: appName })}>
|
||||
{appType.some((type) => type === "payment") ? (
|
||||
<>
|
||||
<p>
|
||||
<>{t("disabled_app_affects_event_type", { appName: appName, eventType: title })}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{t("payment_disabled_still_able_to_book")}</>
|
||||
</p>
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction
|
||||
label={t("edit_event_type")}
|
||||
href={`${WEBAPP_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
/>
|
||||
</>
|
||||
) : title && eventTypeId ? (
|
||||
<>
|
||||
<p>
|
||||
<>{(t("app_disabled_with_event_type"), { appName: appName, title: title })}</>
|
||||
</p>
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction
|
||||
label={t("edit_event_type")}
|
||||
href={`${WEBAPP_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||
/>
|
||||
</>
|
||||
) : appType.some((type) => type === "video") ? (
|
||||
<>
|
||||
<p>
|
||||
<>{t("app_disabled_video", { appName: appName })}</>
|
||||
</p>
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
) : appType.some((type) => type === "calendar") ? (
|
||||
<>
|
||||
<p>
|
||||
<>{t("admin_has_disabled", { appName: appName })}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{t("disabled_calendar")}</>
|
||||
</p>
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<>{t("admin_has_disabled", { appName: appName })}</>
|
||||
</p>
|
||||
|
||||
<hr style={{ marginBottom: "24px" }} />
|
||||
|
||||
<CallToAction label={t("navigate_installed_apps")} href={`${WEBAPP_URL}/apps/installed`} />
|
||||
</>
|
||||
)}
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
19
calcom/packages/emails/src/templates/FeedbackEmail.tsx
Normal file
19
calcom/packages/emails/src/templates/FeedbackEmail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { BaseEmailHtml, Info } from "../components";
|
||||
|
||||
export interface Feedback {
|
||||
username: string;
|
||||
email: string;
|
||||
rating: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export const FeedbackEmail = (props: Feedback & Partial<React.ComponentProps<typeof BaseEmailHtml>>) => {
|
||||
return (
|
||||
<BaseEmailHtml subject="Feedback" title="Feedback">
|
||||
<Info label="Username" description={props.username} withSpacer />
|
||||
<Info label="Email" description={props.email} withSpacer />
|
||||
<Info label="Rating" description={props.rating} withSpacer />
|
||||
<Info label="Comment" description={props.comment} withSpacer />
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
49
calcom/packages/emails/src/templates/ForgotPasswordEmail.tsx
Normal file
49
calcom/packages/emails/src/templates/ForgotPasswordEmail.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export type PasswordReset = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name?: string | null;
|
||||
email: string;
|
||||
};
|
||||
resetLink: string;
|
||||
};
|
||||
export const ForgotPasswordEmail = (
|
||||
props: PasswordReset & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("reset_password_subject", { appName: APP_NAME })}>
|
||||
<p>
|
||||
<>{props.language("hi_user_name", { name: props.user.name })}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("someone_requested_password_reset")}</>
|
||||
</p>
|
||||
<CallToAction label={props.language("change_password")} href={props.resetLink} />
|
||||
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("password_reset_instructions")}</>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("have_any_questions")}{" "}
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("contact_our_support_team")}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
204
calcom/packages/emails/src/templates/MonthlyDigestEmail.tsx
Normal file
204
calcom/packages/emails/src/templates/MonthlyDigestEmail.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export type MonthlyDigestEmailData = {
|
||||
language: TFunction;
|
||||
Created: number;
|
||||
Completed: number;
|
||||
Rescheduled: number;
|
||||
Cancelled: number;
|
||||
mostBookedEvents: {
|
||||
eventTypeId?: number | null;
|
||||
eventTypeName?: string | null;
|
||||
count?: number | null;
|
||||
}[];
|
||||
membersWithMostBookings: {
|
||||
userId: number | null;
|
||||
user: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
username: string | null;
|
||||
};
|
||||
count: number;
|
||||
}[];
|
||||
admin: { email: string; name: string };
|
||||
team: { name: string; id: number };
|
||||
};
|
||||
|
||||
export const MonthlyDigestEmail = (
|
||||
props: MonthlyDigestEmailData & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
const EventsDetails = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "50px",
|
||||
marginTop: "30px",
|
||||
marginBottom: "30px",
|
||||
}}>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "48px",
|
||||
lineHeight: "48px",
|
||||
}}>
|
||||
{props.Created}
|
||||
</p>
|
||||
<p style={{ fontSize: "16px", fontWeight: 500, lineHeight: "20px" }}>
|
||||
{props.language("events_created")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "48px",
|
||||
lineHeight: "48px",
|
||||
}}>
|
||||
{props.Completed}
|
||||
</p>
|
||||
<p style={{ fontSize: "16px", fontWeight: 500, lineHeight: "20px" }}>
|
||||
{props.language("completed")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "48px",
|
||||
lineHeight: "48px",
|
||||
}}>
|
||||
{props.Rescheduled}
|
||||
</p>
|
||||
<p style={{ fontSize: "16px", fontWeight: 500, lineHeight: "20px" }}>
|
||||
{props.language("rescheduled")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "48px",
|
||||
lineHeight: "48px",
|
||||
}}>
|
||||
{props.Cancelled}
|
||||
</p>
|
||||
<p style={{ fontSize: "16px", fontWeight: 500, lineHeight: "20px" }}>
|
||||
{props.language("cancelled")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("verify_email_subject", { appName: APP_NAME })}>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
width: "100%",
|
||||
marginBottom: "30px",
|
||||
}}>
|
||||
{props.language("your_monthly_digest")}
|
||||
</p>
|
||||
<p style={{ fontWeight: "normal", fontSize: "16px", lineHeight: "24px" }}>
|
||||
{props.language("hi_user_name", { name: props.admin.name })}!
|
||||
</p>
|
||||
<p style={{ fontWeight: "normal", fontSize: "16px", lineHeight: "24px" }}>
|
||||
{props.language("summary_of_events_for_your_team_for_the_last_30_days", {
|
||||
teamName: props.team.name,
|
||||
})}
|
||||
</p>
|
||||
<EventsDetails />
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #D1D5DB",
|
||||
fontSize: "16px",
|
||||
}}>
|
||||
<p style={{ fontWeight: 500 }}>{props.language("most_popular_events")}</p>
|
||||
<p style={{ fontWeight: 500 }}>{props.language("bookings")}</p>
|
||||
</div>
|
||||
{props.mostBookedEvents
|
||||
? props.mostBookedEvents.map((ev, idx) => (
|
||||
<div
|
||||
key={ev.eventTypeId}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `${idx === props.mostBookedEvents.length - 1 ? "" : "1px solid #D1D5DB"}`,
|
||||
}}>
|
||||
<p style={{ fontWeight: "normal" }}>{ev.eventTypeName}</p>
|
||||
<p style={{ fontWeight: "normal" }}>{ev.count}</p>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<div style={{ width: "100%", marginTop: "30px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #D1D5DB",
|
||||
}}>
|
||||
<p style={{ fontWeight: 500 }}>{props.language("most_booked_members")}</p>
|
||||
<p style={{ fontWeight: 500 }}>{props.language("bookings")}</p>
|
||||
</div>
|
||||
{props.membersWithMostBookings
|
||||
? props.membersWithMostBookings.map((it, idx) => (
|
||||
<div
|
||||
key={it.userId}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: `${
|
||||
idx === props.membersWithMostBookings.length - 1 ? "" : "1px solid #D1D5DB"
|
||||
}`,
|
||||
}}>
|
||||
<p style={{ fontWeight: "normal" }}>{it.user.name}</p>
|
||||
<p style={{ fontWeight: "normal" }}>{it.count}</p>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<div style={{ marginTop: "30px", marginBottom: "30px" }}>
|
||||
<CallToAction
|
||||
label="View all stats"
|
||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/insights?teamId=${props.team.id}`}
|
||||
endIconName="white-arrow-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")}, <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
export const NoShowFeeChargedEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
const { calEvent } = props;
|
||||
const t = props.attendee.language.translate;
|
||||
const locale = props.attendee.language.locale;
|
||||
const timeFormat = props.attendee?.timeFormat;
|
||||
|
||||
if (!calEvent.paymentInfo?.amount) throw new Error("No payment info");
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
locale={locale}
|
||||
title={t("no_show_fee_charged_text_body")}
|
||||
headerType="calendarCircle"
|
||||
timeFormat={timeFormat}
|
||||
subtitle={
|
||||
<>
|
||||
{t("no_show_fee_charged_subtitle", {
|
||||
amount: calEvent.paymentInfo.amount / 100,
|
||||
formatParams: { amount: { currency: calEvent.paymentInfo?.currency } },
|
||||
})}
|
||||
</>
|
||||
}
|
||||
timeZone={props.attendee.timeZone}
|
||||
{...props}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
103
calcom/packages/emails/src/templates/OrgAutoInviteEmail.tsx
Normal file
103
calcom/packages/emails/src/templates/OrgAutoInviteEmail.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { V2BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
type TeamInvite = {
|
||||
language: TFunction;
|
||||
from: string;
|
||||
to: string;
|
||||
orgName: string;
|
||||
joinLink: string;
|
||||
};
|
||||
|
||||
export const OrgAutoInviteEmail = (
|
||||
props: TeamInvite & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<V2BaseEmailHtml
|
||||
subject={props.language("user_invited_you", {
|
||||
user: props.from,
|
||||
team: props.orgName,
|
||||
appName: APP_NAME,
|
||||
entity: "organization",
|
||||
})}>
|
||||
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
|
||||
<>
|
||||
{props.language("organization_admin_invited_heading", {
|
||||
orgName: props.orgName,
|
||||
})}
|
||||
</>
|
||||
</p>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "16px",
|
||||
height: "270px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
src={
|
||||
IS_PRODUCTION
|
||||
? `${WEBAPP_URL}/emails/calendar-email-hero.png`
|
||||
: "http://localhost:3000/emails/calendar-email-hero.png"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
<>
|
||||
{props.language("organization_admin_invited_body", {
|
||||
orgName: props.orgName,
|
||||
})}
|
||||
</>
|
||||
</p>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<CallToAction
|
||||
label={props.language("email_user_cta", {
|
||||
entity: "organization",
|
||||
})}
|
||||
href={props.joinLink}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
<>
|
||||
{props.language("email_no_user_signoff", {
|
||||
appName: APP_NAME,
|
||||
entity: props.language("organization").toLowerCase(),
|
||||
})}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
|
||||
<p style={{ fontWeight: 400, margin: 0 }}>
|
||||
<>
|
||||
{props.language("have_any_questions")}{" "}
|
||||
<a href="mailto:support@cal.com" style={{ color: "#3E3E3E" }} target="_blank" rel="noreferrer">
|
||||
<>{props.language("contact")}</>
|
||||
</a>{" "}
|
||||
{props.language("our_support_team")}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, SUPPORT_MAIL_ADDRESS, COMPANY_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml } from "../components";
|
||||
|
||||
export type OrganizationEmailVerify = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
email: string;
|
||||
};
|
||||
code: string;
|
||||
};
|
||||
|
||||
export const OrganisationAccountVerifyEmail = (
|
||||
props: OrganizationEmailVerify & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("organization_verify_header", { appName: APP_NAME })}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{props.language("organization_verify_header")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{props.language("hi_user_name", { name: props.user.email })}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("organization_verify_email_body")}</>
|
||||
</p>
|
||||
|
||||
<div style={{ display: "flex" }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "6px",
|
||||
backgroundColor: "#101010",
|
||||
padding: "6px 2px 6px 8px",
|
||||
flexShrink: 1,
|
||||
}}>
|
||||
<b style={{ fontWeight: 400, lineHeight: "24px", color: "white", letterSpacing: "6px" }}>
|
||||
{props.code}
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")} <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: COMPANY_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export type OrganizationAdminNoSlotsEmailInput = {
|
||||
language: TFunction;
|
||||
to: {
|
||||
email: string;
|
||||
};
|
||||
user: string;
|
||||
slug: string;
|
||||
startTime: string;
|
||||
editLink: string;
|
||||
};
|
||||
|
||||
export const OrganizationAdminNoSlotsEmail = (
|
||||
props: OrganizationAdminNoSlotsEmailInput & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={`No availability found for ${props.user}`}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{props.language("org_admin_no_slots|heading", { name: props.user })}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, fontSize: "16px", lineHeight: "24px" }}>
|
||||
<Trans i18nKey="org_admin_no_slots|content" values={{ username: props.user, slug: props.slug }}>
|
||||
Hello Organization Admins,
|
||||
<br />
|
||||
<br />
|
||||
Please note: It has been brought to our attention that {props.user} has not had any availability
|
||||
when a user has visited {props.user}/{props.slug}
|
||||
<br />
|
||||
<br />
|
||||
There’s a few reasons why this could be happening
|
||||
<br />
|
||||
The user does not have any calendars connected
|
||||
<br />
|
||||
Their schedules attached to this event are not enabled
|
||||
</Trans>
|
||||
</p>
|
||||
<div style={{ marginTop: "3rem", marginBottom: "0.75rem" }}>
|
||||
<CallToAction
|
||||
label={props.language("org_admin_no_slots|cta")}
|
||||
href={props.editLink}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import type { OrganizationCreation } from "../../templates/organization-creation-email";
|
||||
import { V2BaseEmailHtml } from "../components";
|
||||
|
||||
export const OrganizationCreationEmail = (
|
||||
props: OrganizationCreation & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
const { prevLink, newLink, orgName: teamName } = props;
|
||||
const prevLinkWithoutProtocol = props.prevLink?.replace(/https?:\/\//, "");
|
||||
const newLinkWithoutProtocol = props.newLink?.replace(/https?:\/\//, "");
|
||||
const isNewUser = props.ownerOldUsername === null;
|
||||
return (
|
||||
<V2BaseEmailHtml subject={props.language(`email_organization_created|subject`)}>
|
||||
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
|
||||
<>{props.language(`You have created ${props.orgName} organization.`)}</>
|
||||
</p>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "16px",
|
||||
height: "270px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
src={`${WEBAPP_URL}/emails/calendar-email-hero.png`}
|
||||
alt=""
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
You have been added as an owner of the organization. To publish your new organization, visit{" "}
|
||||
<a href={`${WEBAPP_URL}/upgrade`}>{WEBAPP_URL}/upgrade</a>
|
||||
</p>
|
||||
<p
|
||||
data-testid="organization-link-info"
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "48px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
{isNewUser ? (
|
||||
<Trans>
|
||||
Enjoy your new organization link: <a href={`${newLink}`}>{newLinkWithoutProtocol}</a>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="email|existing_user_added_link_changed">
|
||||
Your link has been changed from <a href={prevLink ?? ""}>{prevLinkWithoutProtocol}</a> to{" "}
|
||||
<a href={newLink ?? ""}>{newLinkWithoutProtocol}</a> but don't worry, all previous links
|
||||
still work and redirect appropriately.
|
||||
<br />
|
||||
<br />
|
||||
Please note: All of your personal event types have been moved into the <strong>
|
||||
{teamName}
|
||||
</strong>{" "}
|
||||
organisation, which may also include potential personal link.
|
||||
<br />
|
||||
<br />
|
||||
Please log in and make sure you have no private events on your new organisational account.
|
||||
<br />
|
||||
<br />
|
||||
For personal events we recommend creating a new account with a personal email address.
|
||||
<br />
|
||||
<br />
|
||||
Enjoy your new clean link: <a href={`${newLink}?orgRedirection=true`}>{newLinkWithoutProtocol}</a>
|
||||
</Trans>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="">
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
<>
|
||||
{props.language("email_no_user_signoff", {
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
|
||||
<p style={{ fontWeight: 400, margin: 0 }}>
|
||||
<>
|
||||
{props.language("have_any_questions")}{" "}
|
||||
<a href="mailto:support@cal.com" style={{ color: "#3E3E3E" }} target="_blank" rel="noreferrer">
|
||||
<>{props.language("contact")}</>
|
||||
</a>{" "}
|
||||
{props.language("our_support_team")}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerAttendeeCancelledSeatEmail = (
|
||||
props: React.ComponentProps<typeof OrganizerScheduledEmail>
|
||||
) => (
|
||||
<OrganizerScheduledEmail
|
||||
title="attendee_no_longer_attending"
|
||||
headerType="xCircle"
|
||||
subject="event_cancelled_subject"
|
||||
callToAction={null}
|
||||
attendeeCancelled
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerCancelledEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
|
||||
<OrganizerScheduledEmail
|
||||
title="event_request_cancelled"
|
||||
headerType="xCircle"
|
||||
subject="event_cancelled_subject"
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerLocationChangeEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
|
||||
<OrganizerScheduledEmail
|
||||
title="event_location_changed"
|
||||
headerType="calendarCircle"
|
||||
subject="location_changed_event_type_subject"
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { BaseEmailHtml } from "../components";
|
||||
import type { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerPaymentRefundFailedEmail = (
|
||||
props: React.ComponentProps<typeof OrganizerScheduledEmail>
|
||||
) => {
|
||||
const t = props.calEvent.organizer.language.translate;
|
||||
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
headerType="xCircle"
|
||||
subject="refund_failed_subject"
|
||||
title={t("a_refund_failed")}
|
||||
callToAction={null}
|
||||
subtitle={
|
||||
<>
|
||||
{t("check_with_provider_and_user", {
|
||||
user: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
</>
|
||||
}>
|
||||
<RefundInformation {...props} />
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
|
||||
function RefundInformation(props: React.ComponentProps<typeof OrganizerPaymentRefundFailedEmail>) {
|
||||
const { paymentInfo } = props.calEvent;
|
||||
const t = props.calEvent.organizer.language.translate;
|
||||
|
||||
if (!paymentInfo) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{paymentInfo.reason && (
|
||||
<tr>
|
||||
<td align="center" style={{ fontSize: "0px", padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
color: "#494949",
|
||||
}}>
|
||||
{t("error_message", { errorMessage: paymentInfo.reason }).toString()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{paymentInfo.id && (
|
||||
<tr>
|
||||
<td align="center" style={{ fontSize: "0px", padding: "10px 25px", wordBreak: "break-word" }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "Roboto, Helvetica, sans-serif",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
textAlign: "center",
|
||||
color: "#494949",
|
||||
}}>
|
||||
Payment {paymentInfo.id}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
|
||||
import { CallToAction, Separator, CallToActionTable } from "../components";
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerRequestEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
|
||||
const seedData = { bookingUid: props.calEvent.uid, userId: props.calEvent.organizer.id };
|
||||
const token = symmetricEncrypt(JSON.stringify(seedData), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||
//TODO: We should switch to using org domain if available
|
||||
const actionHref = `${WEBAPP_URL}/api/link/?token=${encodeURIComponent(token)}`;
|
||||
return (
|
||||
<OrganizerScheduledEmail
|
||||
title={
|
||||
props.title || props.calEvent.recurringEvent?.count
|
||||
? "event_awaiting_approval_recurring"
|
||||
: "event_awaiting_approval"
|
||||
}
|
||||
subtitle={<>{props.calEvent.organizer.language.translate("someone_requested_an_event")}</>}
|
||||
headerType="calendarCircle"
|
||||
subject="event_awaiting_approval_subject"
|
||||
callToAction={
|
||||
<CallToActionTable>
|
||||
<CallToAction
|
||||
label={props.calEvent.organizer.language.translate("confirm")}
|
||||
href={`${actionHref}&action=accept`}
|
||||
startIconName="confirmIcon"
|
||||
/>
|
||||
<Separator />
|
||||
<CallToAction
|
||||
label={props.calEvent.organizer.language.translate("reject")}
|
||||
href={`${actionHref}&action=reject`}
|
||||
startIconName="rejectIcon"
|
||||
secondary
|
||||
/>
|
||||
</CallToActionTable>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { OrganizerRequestEmail } from "./OrganizerRequestEmail";
|
||||
|
||||
export const OrganizerRequestReminderEmail = (props: React.ComponentProps<typeof OrganizerRequestEmail>) => (
|
||||
<OrganizerRequestEmail title="event_still_awaiting_approval" {...props} />
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerRequestedToRescheduleEmail = (
|
||||
props: React.ComponentProps<typeof OrganizerScheduledEmail>
|
||||
) => (
|
||||
<OrganizerScheduledEmail
|
||||
title={props.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
|
||||
attendee: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
subtitle={
|
||||
<>
|
||||
{props.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
||||
attendee: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject="rescheduled_event_type_subject"
|
||||
callToAction={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerRescheduledEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
|
||||
<OrganizerScheduledEmail
|
||||
title="event_has_been_rescheduled"
|
||||
headerType="calendarCircle"
|
||||
subject="event_type_has_been_rescheduled_on_time_date"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
export const OrganizerScheduledEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
newSeat?: boolean;
|
||||
attendeeCancelled?: boolean;
|
||||
teamMember?: Person;
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
let subject;
|
||||
let title;
|
||||
|
||||
if (props.newSeat) {
|
||||
subject = "new_seat_subject";
|
||||
} else {
|
||||
subject = "confirmed_event_type_subject";
|
||||
}
|
||||
|
||||
if (props.calEvent.recurringEvent?.count) {
|
||||
title = "new_event_scheduled_recurring";
|
||||
} else if (props.newSeat) {
|
||||
title = "new_seat_title";
|
||||
} else {
|
||||
title = "new_event_scheduled";
|
||||
}
|
||||
|
||||
const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate;
|
||||
const locale = props.teamMember?.language.locale || props.calEvent.organizer.language.locale;
|
||||
const timeFormat = props.teamMember?.timeFormat || props.calEvent.organizer?.timeFormat;
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
locale={locale}
|
||||
timeZone={props.teamMember?.timeZone || props.calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
subject={t(subject)}
|
||||
title={t(title)}
|
||||
includeAppsStatus
|
||||
timeFormat={timeFormat}
|
||||
isOrganizer
|
||||
subtitle={
|
||||
<>
|
||||
{props.attendeeCancelled
|
||||
? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name })
|
||||
: ""}
|
||||
</>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export const SlugReplacementEmail = (
|
||||
props: {
|
||||
slug: string;
|
||||
name: string;
|
||||
teamName: string;
|
||||
t: TFunction;
|
||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
const { slug, name, teamName, t } = props;
|
||||
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
subject={t("email_subject_slug_replacement", { slug: slug })}
|
||||
headerType="teamCircle"
|
||||
title={t("event_replaced_notice")}>
|
||||
<>
|
||||
<Trans i18nKey="hi_user_name" name={name}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px", display: "inline-block" }}>Hi {name}</p>
|
||||
<p style={{ display: "inline" }}>,</p>
|
||||
</Trans>
|
||||
<Trans i18nKey="email_body_slug_replacement_notice" slug={slug}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
An administrator on the <strong>{teamName}</strong> team has replaced your event type{" "}
|
||||
<strong>/{slug}</strong> with a managed event type that they control.
|
||||
</p>
|
||||
</Trans>
|
||||
<Trans i18nKey="email_body_slug_replacement_info">
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
Your link will continue to work but some settings for it may have changed. You can review it in
|
||||
event types.
|
||||
</p>
|
||||
</Trans>
|
||||
<table
|
||||
role="presentation"
|
||||
border={0}
|
||||
style={{ verticalAlign: "top", marginTop: "25px" }}
|
||||
width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<CallToAction
|
||||
label={t("review_event_type")}
|
||||
href={`${WEBAPP_URL}/event-types`}
|
||||
endIconName="white-arrow-right"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
style={{
|
||||
borderTop: "solid 1px #E1E1E1",
|
||||
fontSize: 1,
|
||||
margin: "35px auto",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
<Trans i18nKey="email_body_slug_replacement_suggestion">
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
If you have any questions about the event type, please reach out to your administrator.
|
||||
<br />
|
||||
<br />
|
||||
Happy scheduling, <br />
|
||||
The Cal.com team
|
||||
</p>
|
||||
</Trans>
|
||||
{/*<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{t("email_body_slug_replacement_suggestion")}</>
|
||||
</p>*/}
|
||||
</>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
251
calcom/packages/emails/src/templates/TeamInviteEmail.tsx
Normal file
251
calcom/packages/emails/src/templates/TeamInviteEmail.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { getSubject, getTypeOfInvite } from "../../templates/team-invite-email";
|
||||
import { V2BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
type TeamInvite = {
|
||||
language: TFunction;
|
||||
from: string;
|
||||
to: string;
|
||||
teamName: string;
|
||||
joinLink: string;
|
||||
isCalcomMember: boolean;
|
||||
isAutoJoin: boolean;
|
||||
isOrg: boolean;
|
||||
parentTeamName: string | undefined;
|
||||
isExistingUserMovedToOrg: boolean;
|
||||
prevLink: string | null;
|
||||
newLink: string | null;
|
||||
};
|
||||
|
||||
export const TeamInviteEmail = (
|
||||
props: TeamInvite & Partial<React.ComponentProps<typeof V2BaseEmailHtml>>
|
||||
) => {
|
||||
const typeOfInvite = getTypeOfInvite(props);
|
||||
|
||||
const heading = getHeading();
|
||||
const content = getContent();
|
||||
return (
|
||||
<V2BaseEmailHtml subject={getSubject(props)}>
|
||||
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
|
||||
<>{heading}</>
|
||||
</p>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "16px",
|
||||
height: "270px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
src={
|
||||
IS_PRODUCTION
|
||||
? `${WEBAPP_URL}/emails/calendar-email-hero.png`
|
||||
: "http://localhost:3000/emails/calendar-email-hero.png"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
<>{content}</>
|
||||
</p>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<CallToAction
|
||||
label={props.language(
|
||||
props.isCalcomMember ? (props.isAutoJoin ? "login" : "email_user_cta") : "create_your_account"
|
||||
)}
|
||||
href={props.joinLink}
|
||||
endIconName="linkIcon"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "48px",
|
||||
lineHeightStep: "24px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="">
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
lineHeight: "24px",
|
||||
marginBottom: "32px",
|
||||
marginTop: "32px",
|
||||
lineHeightStep: "24px",
|
||||
}}>
|
||||
<>
|
||||
{props.language("email_no_user_signoff", {
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
|
||||
<p style={{ fontWeight: 400, margin: 0 }}>
|
||||
<>
|
||||
{props.language("have_any_questions")}{" "}
|
||||
<a href="mailto:support@cal.com" style={{ color: "#3E3E3E" }} target="_blank" rel="noreferrer">
|
||||
<>{props.language("contact")}</>
|
||||
</a>{" "}
|
||||
{props.language("our_support_team")}
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</V2BaseEmailHtml>
|
||||
);
|
||||
|
||||
function getHeading() {
|
||||
const autoJoinType = props.isAutoJoin ? "added" : "invited";
|
||||
const variables = {
|
||||
appName: APP_NAME,
|
||||
parentTeamName: props.parentTeamName,
|
||||
};
|
||||
|
||||
if (typeOfInvite === "TO_ORG") {
|
||||
return props.language(`email_team_invite|heading|${autoJoinType}_to_org`, variables);
|
||||
}
|
||||
|
||||
if (typeOfInvite === "TO_SUBTEAM") {
|
||||
return props.language(`email_team_invite|heading|${autoJoinType}_to_subteam`, variables);
|
||||
}
|
||||
|
||||
return props.language(`email_team_invite|heading|invited_to_regular_team`, variables);
|
||||
}
|
||||
|
||||
function getContent() {
|
||||
const autoJoinType = props.isAutoJoin ? "added" : "invited";
|
||||
|
||||
const variables = {
|
||||
invitedBy: props.from.toString(),
|
||||
appName: APP_NAME,
|
||||
teamName: props.teamName,
|
||||
parentTeamName: props.parentTeamName,
|
||||
prevLink: props.prevLink,
|
||||
newLink: props.newLink,
|
||||
orgName: props.parentTeamName ?? props.isOrg ? props.teamName : "",
|
||||
prevLinkWithoutProtocol: props.prevLink?.replace(/https?:\/\//, ""),
|
||||
newLinkWithoutProtocol: props.newLink?.replace(/https?:\/\//, ""),
|
||||
};
|
||||
|
||||
const {
|
||||
prevLink,
|
||||
newLink,
|
||||
teamName,
|
||||
invitedBy,
|
||||
appName,
|
||||
parentTeamName,
|
||||
prevLinkWithoutProtocol,
|
||||
newLinkWithoutProtocol,
|
||||
} = variables;
|
||||
if (typeOfInvite === "TO_ORG") {
|
||||
if (props.isExistingUserMovedToOrg) {
|
||||
return (
|
||||
<>
|
||||
{autoJoinType == "added" ? (
|
||||
<>
|
||||
<Trans i18nKey="email_team_invite|content|added_to_org">
|
||||
{invitedBy} has added you to the <strong>{teamName}</strong> organization.
|
||||
</Trans>{" "}
|
||||
<Trans
|
||||
i18nKey="email_team_invite|content_addition|existing_user_added"
|
||||
values={{ prevLink: props.prevLink, newLink: props.newLink, teamName: props.teamName }}>
|
||||
Your link has been changed from <a href={prevLink ?? ""}>{prevLinkWithoutProtocol}</a> to{" "}
|
||||
<a href={newLink ?? ""}>{newLinkWithoutProtocol}</a> but don't worry, all previous
|
||||
links still work and redirect appropriately.
|
||||
<br />
|
||||
<br />
|
||||
Please note: All of your personal event types have been moved into the{" "}
|
||||
<strong>{teamName}</strong> organisation, which may also include potential personal link.
|
||||
<br />
|
||||
<br />
|
||||
Please log in and make sure you have no private events on your new organisational account.
|
||||
<br />
|
||||
<br />
|
||||
For personal events we recommend creating a new account with a personal email address.
|
||||
<br />
|
||||
<br />
|
||||
Enjoy your new clean link:{" "}
|
||||
<a href={`${newLink}?orgRedirection=true`}>{newLinkWithoutProtocol}</a>
|
||||
</Trans>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trans i18nKey="email_team_invite|content|invited_to_org">
|
||||
{invitedBy} has invited you to join the <strong>{teamName}</strong> organization.
|
||||
</Trans>{" "}
|
||||
<Trans
|
||||
i18nKey="existing_user_added_link_will_change"
|
||||
values={{ prevLink: props.prevLink, newLink: props.newLink, teamName: props.teamName }}>
|
||||
On accepting the invite, your link will change to your organization domain but don't
|
||||
worry, all previous links will still work and redirect appropriately.
|
||||
<br />
|
||||
<br />
|
||||
Please note: All of your personal event types will be moved into the{" "}
|
||||
<strong>{teamName}</strong> organisation, which may also include potential personal link.
|
||||
<br />
|
||||
<br />
|
||||
For personal events we recommend creating a new account with a personal email address.
|
||||
</Trans>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{autoJoinType === "added" ? (
|
||||
<Trans i18nKey="email_team_invite|content|added_to_org">
|
||||
{invitedBy} has added you to the <strong>{teamName}</strong> organization.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="email_team_invite|content|invited_to_org">
|
||||
{invitedBy} has invited you to join the <strong>{teamName}</strong> organization.
|
||||
</Trans>
|
||||
)}{" "}
|
||||
<Trans>
|
||||
{appName} is the event-juggling scheduler that enables you and your team to schedule meetings
|
||||
without the email tennis.
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeOfInvite === "TO_SUBTEAM") {
|
||||
return (
|
||||
<>
|
||||
{autoJoinType === "added" ? (
|
||||
<Trans i18nKey="email_team_invite|content|added_to_subteam">
|
||||
{invitedBy} has added you to the team <strong>{teamName}</strong> in their organization{" "}
|
||||
<strong>{parentTeamName}</strong>.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="email_team_invite|content|invited_to_subteam">
|
||||
{invitedBy} has invited you to the team <strong>{teamName}</strong> in their organization{" "}
|
||||
<strong>{parentTeamName}</strong>.
|
||||
</Trans>
|
||||
)}{" "}
|
||||
<Trans>
|
||||
{appName} is the event-juggling scheduler that enables you and your team to schedule meetings
|
||||
without the email tennis.
|
||||
</Trans>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Regular team doesn't support auto-join. So, they have to be invited always
|
||||
return props.language(`email_team_invite|content|invited_to_regular_team`, variables);
|
||||
}
|
||||
};
|
||||
60
calcom/packages/emails/src/templates/VerifyAccountEmail.tsx
Normal file
60
calcom/packages/emails/src/templates/VerifyAccountEmail.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export type EmailVerifyLink = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name?: string | null;
|
||||
email: string;
|
||||
};
|
||||
verificationEmailLink: string;
|
||||
};
|
||||
|
||||
export const VerifyAccountEmail = (
|
||||
props: EmailVerifyLink & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("verify_email_subject", { appName: APP_NAME })}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{props.language("verify_email_email_header")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{props.language("hi_user_name", { name: props.user.name })}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("verify_email_email_body", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
<CallToAction label={props.language("verify_email_email_button")} href={props.verificationEmailLink} />
|
||||
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("verify_email_email_link_text")}</>
|
||||
<br />
|
||||
<a href={props.verificationEmailLink}>{props.verificationEmailLink}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")}, <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
48
calcom/packages/emails/src/templates/VerifyEmailByCode.tsx
Normal file
48
calcom/packages/emails/src/templates/VerifyEmailByCode.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import type { EmailVerifyCode } from "../../templates/attendee-verify-email";
|
||||
import { BaseEmailHtml } from "../components";
|
||||
|
||||
export const VerifyEmailByCode = (
|
||||
props: EmailVerifyCode & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
subject={props.language(`verify_email_subject${props.isVerifyingEmail ? "_verifying_email" : ""}`, {
|
||||
appName: APP_NAME,
|
||||
})}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "32px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{props.language("verify_email_email_header")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{props.language("hi_user_name", { name: props.user.name })}!</>
|
||||
</p>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("verify_email_by_code_email_body")}</>
|
||||
<br />
|
||||
<p>{props.verificationEmailCode}</p>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")}, <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
103
calcom/packages/emails/src/templates/VerifyEmailChangeEmail.tsx
Normal file
103
calcom/packages/emails/src/templates/VerifyEmailChangeEmail.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
export type EmailVerifyLink = {
|
||||
language: TFunction;
|
||||
user: {
|
||||
name?: string | null;
|
||||
emailFrom: string;
|
||||
emailTo: string;
|
||||
};
|
||||
verificationEmailLink: string;
|
||||
};
|
||||
|
||||
export const VerifyEmailChangeEmail = (
|
||||
props: EmailVerifyLink & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
return (
|
||||
<BaseEmailHtml subject={props.language("change_of_email", { appName: APP_NAME })}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "24px",
|
||||
lineHeight: "32px",
|
||||
}}>
|
||||
<>{props.language("change_of_email", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{props.language("hi_user_name", { name: props.user.name })}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>{props.language("verify_email_change_description", { appName: APP_NAME })}</>
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "2rem",
|
||||
marginBottom: "2rem",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "14px",
|
||||
lineHeight: 0.5,
|
||||
}}>
|
||||
{props.language("old_email_address")}
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
color: `#6B7280`,
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}>
|
||||
{props.user.emailFrom}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "14px",
|
||||
lineHeight: 0.5,
|
||||
}}>
|
||||
{props.language("new_email_address")}
|
||||
</span>
|
||||
<p
|
||||
style={{
|
||||
color: `#6B7280`,
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}>
|
||||
{props.user.emailTo}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CallToAction label={props.language("verify_email_email_button")} href={props.verificationEmailLink} />
|
||||
<div style={{ lineHeight: "6px" }}>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<>
|
||||
{props.language("happy_scheduling")}, <br />
|
||||
<a
|
||||
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
|
||||
style={{ color: "#3E3E3E" }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
|
||||
</a>
|
||||
</>
|
||||
</p>
|
||||
</div>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
38
calcom/packages/emails/src/templates/index.ts
Normal file
38
calcom/packages/emails/src/templates/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export { AttendeeAwaitingPaymentEmail } from "./AttendeeAwaitingPaymentEmail";
|
||||
export { AttendeeCancelledEmail } from "./AttendeeCancelledEmail";
|
||||
export { AttendeeCancelledSeatEmail } from "./AttendeeCancelledSeatEmail";
|
||||
export { AttendeeDeclinedEmail } from "./AttendeeDeclinedEmail";
|
||||
export { AttendeeLocationChangeEmail } from "./AttendeeLocationChangeEmail";
|
||||
export { AttendeeRequestEmail } from "./AttendeeRequestEmail";
|
||||
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
|
||||
export { AttendeeRescheduledEmail } from "./AttendeeRescheduledEmail";
|
||||
export { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||
export { DisabledAppEmail } from "./DisabledAppEmail";
|
||||
export { SlugReplacementEmail } from "./SlugReplacementEmail";
|
||||
export { FeedbackEmail } from "./FeedbackEmail";
|
||||
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
||||
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
||||
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
|
||||
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
|
||||
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
|
||||
export { OrganizerRequestReminderEmail } from "./OrganizerRequestReminderEmail";
|
||||
export { OrganizerRequestedToRescheduleEmail } from "./OrganizerRequestedToRescheduleEmail";
|
||||
export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
|
||||
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export { TeamInviteEmail } from "./TeamInviteEmail";
|
||||
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
|
||||
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
|
||||
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
|
||||
export { VerifyAccountEmail } from "./VerifyAccountEmail";
|
||||
export { VerifyEmailByCode } from "./VerifyEmailByCode";
|
||||
export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
|
||||
export { DailyVideoDownloadTranscriptEmail } from "./DailyVideoDownloadTranscriptEmail";
|
||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
||||
export { MonthlyDigestEmail } from "./MonthlyDigestEmail";
|
||||
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
||||
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
|
||||
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
|
||||
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
|
||||
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
|
||||
Reference in New Issue
Block a user