2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View 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
/>
);
};

View 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>
);
};

View 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;

View 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>
);
};

View 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=""
/>
);

View 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>
);

View 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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;"> &nbsp;</td></tr></table><![endif]-->`}
/>
</td>
</EmailCommonDivider>
);
export default EmailSchedulingBodyDivider;

View File

@@ -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;

View 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>
</>
);
};

View 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
}
/>
);
}

View 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;
}

View File

@@ -0,0 +1,6 @@
/** @see https://gist.github.com/zomars/4c366a0118a5b7fb391529ab1f27527a */
const RawHtml = ({ html = "" }) => (
<script dangerouslySetInnerHTML={{ __html: `</script>${html}<script>` }} />
);
export default RawHtml;

View 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;

View File

@@ -0,0 +1,3 @@
export const Separator = () => (
<p style={{ width: "16px", height: "16px", display: "inline-block" }}>&nbsp;</p>
);

View File

@@ -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
)}
</>
);
}

View 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>
);
};

View 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>
);
}

View 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
/>
);
}

View 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";

View 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;

View File

@@ -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>
);
};

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View 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&nbsp;
<a
href={
props.eventTypeId ? `${WEBAPP_URL}/event-types/${props.eventTypeId}` : `${WEBAPP_URL}/event-types`
}>
change your location on the event type
</a>
&nbsp;or try&nbsp;
<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}
/>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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}
/>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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 />
Theres 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>
);
};

View File

@@ -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&apos;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>
);
};

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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>
)}
</>
);
}

View File

@@ -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}
/>
);
};

View File

@@ -0,0 +1,5 @@
import { OrganizerRequestEmail } from "./OrganizerRequestEmail";
export const OrganizerRequestReminderEmail = (props: React.ComponentProps<typeof OrganizerRequestEmail>) => (
<OrganizerRequestEmail title="event_still_awaiting_approval" {...props} />
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);
};

View File

@@ -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>
);
};

View 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&apos;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&apos;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);
}
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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";