Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a67e5617e | ||
|
|
f401aa7669 | ||
|
|
a6e0678e59 | ||
|
|
e832015f26 | ||
|
|
5e86881e15 | ||
|
|
3b71f98460 | ||
|
|
144d57adb2 | ||
|
|
e1f1c15318 | ||
|
|
d88b1b2f8f | ||
|
|
2e61c15f2c | ||
|
|
df73473c7f | ||
|
|
7167973afd | ||
|
|
3c17bc2d1a | ||
|
|
a9a295dc54 | ||
|
+2 |
21dd1f4e95 | ||
|
|
5d775bd4a3 | ||
|
|
4e02b22472 | ||
|
|
4686d3ff48 | ||
|
|
02e9d8d45f | ||
|
|
7ce5096f13 | ||
|
|
83e51e9b9d | ||
|
|
dca3278c40 | ||
|
|
7501635fbf | ||
|
|
80c96ea5ac | ||
|
|
8c001e0d34 | ||
|
|
bf275497a2 | ||
|
|
6e351a4b20 | ||
|
|
5360cea3bf | ||
|
+2 |
2f36985462 | ||
|
|
6b26e9a7de | ||
|
|
d7655f0521 | ||
|
|
f4ed345a87 | ||
|
|
c3122c82e4 | ||
|
|
cc9b49131e | ||
|
|
190db1a0f6 | ||
|
|
bd48a72e99 | ||
|
|
a6836a1488 | ||
|
|
2b67ea9635 | ||
|
|
a9117c2d36 | ||
|
+1 |
10d3b5f1b9 | ||
|
|
b757cb6ba1 | ||
|
|
5968e0aa19 | ||
|
|
a06fffc4ec | ||
|
|
3312c84e7c | ||
|
|
d86710d5e9 | ||
|
|
2d8e65c160 | ||
|
|
f74eaf7cb3 | ||
|
|
bcfd57c953 | ||
|
|
c38588b93d | ||
|
|
32ed43574d | ||
|
|
38a5a22b9e | ||
|
|
2120e301c3 | ||
|
|
dd0aec4e97 | ||
|
|
3815b86258 | ||
|
|
7a6545bd03 | ||
|
|
11f467af5d | ||
|
|
0930cf50b6 | ||
|
|
c37cddd0b7 | ||
|
|
09253f83bf | ||
|
|
1cdcebac01 | ||
|
|
58270fa653 | ||
|
|
1a26a254ab | ||
|
|
a36d15894d | ||
|
|
cf077fd33c | ||
|
|
0c979d3604 | ||
|
|
33bc786bb3 | ||
|
|
a7f16462db | ||
|
|
2c7fcbcf51 | ||
|
|
d3e4d26fa5 | ||
|
|
3be6c05722 | ||
|
|
e293e590bd | ||
|
+1 |
d8c1c103a1 | ||
|
|
19d00e691c | ||
|
|
78aa4aecef | ||
|
|
3ab002e547 | ||
|
|
7f461bc275 | ||
|
|
571176102e | ||
|
|
8f1d3fbc50 | ||
|
|
5e88ad50ea | ||
|
|
056406bdd9 | ||
|
|
58af5a18a4 | ||
|
|
90b6581d76 | ||
|
|
b33dcc8841 | ||
|
|
d951a5b872 | ||
|
|
7122a9cfbf | ||
|
|
f1b92c49fc | ||
|
|
ef3ad36c2c | ||
|
|
072ce452b7 | ||
|
|
0e0940f5f0 | ||
|
|
310936125d | ||
|
|
a9df3309b9 | ||
|
|
148d658f14 | ||
|
|
242d571d7d | ||
|
|
dff49ec28a | ||
|
|
0c746ec10f | ||
|
|
901f54ca3a | ||
|
|
16fda5f599 | ||
|
|
d3e9adf382 | ||
|
|
cf559e0726 | ||
|
|
8447b1bbd5 | ||
|
|
0d03006653 | ||
|
|
d44c5fd8b0 | ||
|
|
74aa366f1e | ||
|
|
7ebea57602 | ||
|
|
26d675e162 | ||
|
|
e16d29e916 | ||
|
|
0e89fb9f6a |
@@ -140,3 +140,8 @@ SENDGRID_SYNC_API_KEY=
|
|||||||
# Sentry
|
# Sentry
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
NEXT_PUBLIC_SENTRY_DSN=
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
||||||
|
|
||||||
|
# Change your Brand
|
||||||
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
|
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||||
|
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Add bugs to Bug Hunting
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- labeled
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add-to-project:
|
|
||||||
name: Add issue to project
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/add-to-project@v0.1.0
|
|
||||||
with:
|
|
||||||
project-url: https://github.com/orgs/calcom/projects/9
|
|
||||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
||||||
labeled: 🙋🏻♂️help wanted
|
|
||||||
@@ -11,6 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
|
ref: main # Always checkout main even for tagged releases
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
- run: git push origin +main:production
|
- run: git push origin +main:production
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* This is a Playwright script Checkly generated for you based on your Vercel project.
|
||||||
|
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Create a Chromium browser
|
||||||
|
const { chromium } = require("playwright");
|
||||||
|
|
||||||
|
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
|
||||||
|
async function run() {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const targetUrl = process.env.ENVIRONMENT_URL || "https://cal.com";
|
||||||
|
await page.goto(`${targetUrl}/rick/test-location-link`);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="day"][data-disabled="false"]');
|
||||||
|
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="time"]');
|
||||||
|
await page.click('[data-testid="time"]');
|
||||||
|
|
||||||
|
await page.waitForSelector("#name");
|
||||||
|
await page.click("#name");
|
||||||
|
|
||||||
|
await page.type("#name", "Calcom");
|
||||||
|
|
||||||
|
await page.type('[name="email"]', "cal@cal.com");
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="confirm-book-button"]');
|
||||||
|
await page.click('[data-testid="confirm-book-button"]');
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
+1
-1
Submodule apps/api updated: 055699f612...8b74f463f4
+1
-1
Submodule apps/console updated: 9174a0c261...564f9b2faa
@@ -4,6 +4,7 @@ import { NextRouter, useRouter } from "next/router";
|
|||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
||||||
import { components, ControlProps } from "react-select";
|
import { components, ControlProps } from "react-select";
|
||||||
|
|
||||||
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -20,8 +21,6 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
|
||||||
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
import ColorPicker from "@components/ui/colorpicker";
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -498,7 +497,9 @@ const tabs = [
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
|
<small className="flex py-4 text-neutral-500">
|
||||||
|
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="embed-code"
|
data-testid="embed-code"
|
||||||
@@ -619,13 +620,13 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<DialogContent type="creation" useOwnActionButtons size="lg">
|
<DialogContent type="creation" size="lg">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
{t("how_you_want_add_cal_site")}
|
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
@@ -797,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons>
|
<DialogContent size="xl" className="p-0.5" type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||||
@@ -1095,9 +1096,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{t("copy_code")}
|
{t("copy_code")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<DialogClose asChild>
|
<DialogClose />
|
||||||
<Button color="secondary">{t("Close")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -156,12 +156,8 @@ export default function ImageUploader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<DialogClose asChild>
|
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
|
||||||
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
|
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -61,12 +61,10 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<Select
|
<Select
|
||||||
name="additionalCalendar"
|
name="additionalCalendar"
|
||||||
placeholder={
|
placeholder={
|
||||||
<div className="flex justify-start text-black">
|
<Button StartIcon={Icon.FiPlus} color="secondary">
|
||||||
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
|
{t("add")}
|
||||||
<p>{t("add")}</p>
|
</Button>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
className="min-w-44"
|
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -80,11 +78,16 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
width: "max-content",
|
width: "max-content",
|
||||||
right: "0",
|
right: "0",
|
||||||
}),
|
}),
|
||||||
|
indicatorSeparator: (defaultStyles) => ({
|
||||||
|
...defaultStyles,
|
||||||
|
display: "none",
|
||||||
|
}),
|
||||||
control: (defaultStyles) => ({
|
control: (defaultStyles) => ({
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
padding: "0",
|
padding: "0",
|
||||||
border: "0",
|
border: "0",
|
||||||
borderRadius: "inherit",
|
borderRadius: "6px",
|
||||||
|
minHeight: "auto",
|
||||||
}),
|
}),
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
dropdownIndicator: (defaultStyles) => ({
|
||||||
...defaultStyles,
|
...defaultStyles,
|
||||||
|
|||||||
@@ -4,20 +4,14 @@ import React, { useState } from "react";
|
|||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import {
|
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
||||||
Button,
|
|
||||||
DisconnectIntegration,
|
|
||||||
Icon,
|
|
||||||
Shell,
|
|
||||||
showToast,
|
|
||||||
SkeletonButton,
|
|
||||||
SkeletonText,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
|
|
||||||
import HeadSeo from "@components/seo/head-seo";
|
import HeadSeo from "@components/seo/head-seo";
|
||||||
|
|
||||||
@@ -48,7 +42,7 @@ const Component = ({
|
|||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.setupPending) return;
|
if (data?.setupPending) return;
|
||||||
showToast(t("app_successfully_installed"), "success");
|
showToast(t("app_successfully_installed"), "success");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@@ -201,7 +195,7 @@ const Component = ({
|
|||||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||||
<span>
|
<span>
|
||||||
{price === 0 ? (
|
{price === 0 ? (
|
||||||
"Free"
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Intl.NumberFormat("en-US", {
|
{Intl.NumberFormat("en-US", {
|
||||||
@@ -279,8 +273,10 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
<span className="leading-1 block text-xs text-gray-500">{t("every_app_published")}</span>
|
<span className="leading-1 block text-xs text-gray-500">
|
||||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||||
|
</span>
|
||||||
|
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||||
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import Link from "next/link";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
DisconnectIntegration,
|
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
List,
|
List,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode } from "react";
|
import { FC, ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon, Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import { UserAvatars } from "@components/booking/UserAvatars";
|
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||||
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||||
@@ -39,7 +42,35 @@ interface Props {
|
|||||||
|
|
||||||
const BookingDescription: FC<Props> = (props) => {
|
const BookingDescription: FC<Props> = (props) => {
|
||||||
const { profile, eventType, isBookingPage = false, children } = props;
|
const { profile, eventType, isBookingPage = false, children } = props;
|
||||||
|
const { date: bookingDate } = useRouterQuery("date");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
||||||
|
useEffect(() => {
|
||||||
|
if (eventType.metadata?.multipleDuration !== undefined) {
|
||||||
|
if (!duration) {
|
||||||
|
setDuration(eventType.length);
|
||||||
|
} else {
|
||||||
|
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
|
||||||
|
setDuration(eventType.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||||
|
let requiresConfirmationText = t("requires_confirmation");
|
||||||
|
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||||
|
if (rcThreshold) {
|
||||||
|
if (isBookingPage) {
|
||||||
|
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
|
||||||
|
requiresConfirmation = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requiresConfirmationText = t("requires_confirmation_threshold", {
|
||||||
|
...rcThreshold,
|
||||||
|
unit: rcThreshold.unit.slice(0, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<UserAvatars
|
<UserAvatars
|
||||||
@@ -75,29 +106,61 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex items-center",
|
"items-top flex",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
||||||
</div>
|
</div>
|
||||||
{t("requires_confirmation")}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<AvailableEventLocations
|
<AvailableEventLocations
|
||||||
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
||||||
/>
|
/>
|
||||||
<p
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-sm font-medium",
|
"flex flex-nowrap text-sm font-medium",
|
||||||
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
<Icon.FiClock
|
||||||
{eventType.length} {t("minutes")}
|
className={classNames(
|
||||||
</p>
|
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
|
||||||
|
isBookingPage && "mt-[2px]"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{eventType.metadata?.multipleDuration !== undefined ? (
|
||||||
|
!isBookingPage ? (
|
||||||
|
<ul className="-mt-1 flex flex-wrap gap-1">
|
||||||
|
{eventType.metadata.multipleDuration.map((dur, idx) => (
|
||||||
|
<li key={idx}>
|
||||||
|
<Badge
|
||||||
|
variant="gray"
|
||||||
|
size="lg"
|
||||||
|
className={classNames(
|
||||||
|
duration === dur.toString()
|
||||||
|
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||||
|
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
|
||||||
|
"cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setDuration(dur);
|
||||||
|
}}>
|
||||||
|
{dur} {t("minute_timeUnit")}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
`${duration} ${t("minutes")}`
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
`${eventType.length} ${t("minutes")}`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
||||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||||
cancel all remaining bookings or just that booking instance. */
|
cancel all remaining bookings or just that booking instance. */
|
||||||
href: `/success?uid=${booking.uid}&cancel=true${
|
href: `/booking/${booking.uid}?cancel=true${
|
||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}`,
|
}`,
|
||||||
icon: Icon.FiX,
|
icon: Icon.FiX,
|
||||||
@@ -195,11 +195,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const onClickTableData = () => {
|
const onClickTableData = () => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${booking.uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid: booking.uid,
|
|
||||||
allRemainingBookings: isTabRecurring,
|
allRemainingBookings: isTabRecurring,
|
||||||
listingStatus: booking.listingStatus,
|
|
||||||
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -237,9 +235,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose>
|
<DialogClose />
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
@@ -284,6 +280,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{t("pending_payment")}
|
{t("pending_payment")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{booking.paid && (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
|
{t("paid")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventType } from "@prisma/client";
|
import { EventType } from "@prisma/client";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { TFunction } from "next-i18next";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
@@ -31,9 +29,7 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import { Icon, DatePicker } from "@calcom/ui";
|
import { Icon, DatePicker } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
// import { timeZone } from "@lib/clock";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
@@ -46,23 +42,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
// Get router variables
|
|
||||||
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const path = router.asPath.split("/");
|
|
||||||
path.pop(); // Remove the last item (where we currently are)
|
|
||||||
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
|
|
||||||
const slug = path.join("/");
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col justify-end">
|
|
||||||
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
|
||||||
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
|
|
||||||
<p className="sr-only">Go Back</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSlots = ({
|
const useSlots = ({
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
eventTypeSlug,
|
eventTypeSlug,
|
||||||
@@ -70,6 +49,7 @@ const useSlots = ({
|
|||||||
endTime,
|
endTime,
|
||||||
usernameList,
|
usernameList,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
}: {
|
}: {
|
||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
eventTypeSlug: string;
|
eventTypeSlug: string;
|
||||||
@@ -77,6 +57,7 @@ const useSlots = ({
|
|||||||
endTime?: Dayjs;
|
endTime?: Dayjs;
|
||||||
usernameList: string[];
|
usernameList: string[];
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
|
duration?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
||||||
{
|
{
|
||||||
@@ -86,6 +67,7 @@ const useSlots = ({
|
|||||||
startTime: startTime?.toISOString() || "",
|
startTime: startTime?.toISOString() || "",
|
||||||
endTime: endTime?.toISOString() || "",
|
endTime: endTime?.toISOString() || "",
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!startTime && !!endTime,
|
enabled: !!startTime && !!endTime,
|
||||||
@@ -126,6 +108,7 @@ const SlotPicker = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
|
const { duration } = useRouterQuery("duration");
|
||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -151,7 +134,7 @@ const SlotPicker = ({
|
|||||||
setSelectedDate(dayjs.tz(date, timeZone));
|
setSelectedDate(dayjs.tz(date, timeZone));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [router.isReady, month, date, timeZone]);
|
}, [router.isReady, month, date, duration, timeZone]);
|
||||||
|
|
||||||
const { i18n, isLocaleReady } = useLocale();
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
const { slots: _1 } = useSlots({
|
const { slots: _1 } = useSlots({
|
||||||
@@ -161,6 +144,7 @@ const SlotPicker = ({
|
|||||||
startTime: selectedDate?.startOf("day"),
|
startTime: selectedDate?.startOf("day"),
|
||||||
endTime: selectedDate?.endOf("day"),
|
endTime: selectedDate?.endOf("day"),
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
});
|
});
|
||||||
const { slots: _2, isLoading } = useSlots({
|
const { slots: _2, isLoading } = useSlots({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
@@ -169,6 +153,7 @@ const SlotPicker = ({
|
|||||||
startTime: browsingDate?.startOf("month"),
|
startTime: browsingDate?.startOf("month"),
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
|
duration,
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
||||||
@@ -229,14 +214,14 @@ function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
||||||
<p className="text-sm font-medium">
|
<p className="flex items-center text-sm font-medium">
|
||||||
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
||||||
{timeZone}
|
{timeZone}
|
||||||
{isTimeOptionsOpen ? (
|
{isTimeOptionsOpen ? (
|
||||||
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" />
|
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||||
) : (
|
) : (
|
||||||
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
|
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
@@ -258,23 +243,6 @@ const dateQuerySchema = z.object({
|
|||||||
timeZone: z.string().optional().default(""),
|
timeZone: z.string().optional().default(""),
|
||||||
});
|
});
|
||||||
|
|
||||||
const useRouterQuery = <T extends string>(name: T) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const existingQueryParams = router.asPath.split("?")[1];
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(existingQueryParams);
|
|
||||||
const query = Object.fromEntries(urlParams);
|
|
||||||
|
|
||||||
const setQuery = (newValue: string | number | null | undefined) => {
|
|
||||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
|
||||||
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
return { [name]: query[name], setQuery } as {
|
|
||||||
[K in T]: string | undefined;
|
|
||||||
} & { setQuery: typeof setQuery };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||||
@@ -309,9 +277,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// TODO: Improve this;
|
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
|
||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -432,12 +397,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
{timezoneDropdown}
|
{timezoneDropdown}
|
||||||
</BookingDescription>
|
</BookingDescription>
|
||||||
|
|
||||||
{!isEmbed && (
|
|
||||||
<div className="mt-auto hidden md:block">
|
|
||||||
<GoBackToPreviousPage t={t} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
@@ -477,7 +436,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
{(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
@@ -38,11 +39,12 @@ import { HttpError } from "@calcom/lib/http-error";
|
|||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
||||||
|
import { Group, RadioField } from "@calcom/ui";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { ensureArray } from "@lib/ensureArray";
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
@@ -85,9 +87,7 @@ const BookingPage = ({
|
|||||||
...restProps
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
// Get user so we can determine 12/24 hour format preferences
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
const query = useMeQuery();
|
|
||||||
const user = query.data;
|
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
@@ -103,6 +103,11 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const stripeAppData = getStripeAppData(eventType);
|
||||||
|
// Define duration now that we support multiple duration eventTypes
|
||||||
|
let duration = eventType.length;
|
||||||
|
if (queryDuration && !isNaN(Number(queryDuration))) {
|
||||||
|
duration = Number(queryDuration);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
@@ -131,9 +136,8 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid,
|
|
||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
@@ -147,9 +151,8 @@ const BookingPage = ({
|
|||||||
const { uid } = responseData[0] || {};
|
const { uid } = responseData[0] || {};
|
||||||
|
|
||||||
return router.push({
|
return router.push({
|
||||||
pathname: "/success",
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
uid,
|
|
||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
@@ -237,7 +240,7 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().trim().email(),
|
||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
@@ -318,7 +321,7 @@ const BookingPage = ({
|
|||||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||||
...booking,
|
...booking,
|
||||||
start: dayjs(recurringDate).format(),
|
start: dayjs(recurringDate).format(),
|
||||||
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
|
end: dayjs(recurringDate).add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
recurringEventId,
|
recurringEventId,
|
||||||
@@ -351,7 +354,7 @@ const BookingPage = ({
|
|||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
...booking,
|
...booking,
|
||||||
start: dayjs(date).format(),
|
start: dayjs(date).format(),
|
||||||
end: dayjs(date).add(eventType.length, "minute").format(),
|
end: dayjs(date).add(duration, "minute").format(),
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
timeZone: timeZone(),
|
timeZone: timeZone(),
|
||||||
@@ -426,7 +429,7 @@ const BookingPage = ({
|
|||||||
eventTypeTitle: eventType.title,
|
eventTypeTitle: eventType.title,
|
||||||
profileName: profile.name,
|
profileName: profile.name,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
| Cal.com
|
| {APP_NAME}
|
||||||
</title>
|
</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
@@ -741,6 +744,27 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
||||||
|
<div className="">
|
||||||
|
<div className="flex">
|
||||||
|
<Group
|
||||||
|
onValueChange={(e) => {
|
||||||
|
bookingForm.setValue(`customInputs.${input.id}`, e);
|
||||||
|
}}>
|
||||||
|
<>
|
||||||
|
{input.options.map((option, i) => (
|
||||||
|
<RadioField
|
||||||
|
label={option.label}
|
||||||
|
key={`option.${i}.radio`}
|
||||||
|
value={option.label}
|
||||||
|
id={`option.${i}.radio`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!eventType.disableGuests && guestToggle && (
|
{!eventType.disableGuests && guestToggle && (
|
||||||
@@ -839,7 +863,7 @@ const BookingPage = ({
|
|||||||
) : (
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
{...bookingForm.register("notes")}
|
{...bookingForm.register("notes")}
|
||||||
required={!!eventType.metadata.additionalNotesRequired}
|
required={!!eventType.metadata?.additionalNotesRequired}
|
||||||
id="notes"
|
id="notes"
|
||||||
name="notes"
|
name="notes"
|
||||||
rows={3}
|
rows={3}
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog}>
|
<Dialog open={isOpenDialog}>
|
||||||
<DialogContent useOwnActionButtons>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||||
@@ -297,7 +297,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
control={locationFormMethods.control}
|
control={locationFormMethods.control}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Select<{ label: string; value: string; icon?: string }>
|
<Select<{ label: string; value: string; icon?: string }>
|
||||||
maxMenuHeight={150}
|
maxMenuHeight={300}
|
||||||
name="location"
|
name="location"
|
||||||
defaultValue={selection}
|
defaultValue={selection}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import { Dispatch, SetStateAction, useState } from "react";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, Icon, showToast, TextArea } from "@calcom/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
Icon,
|
||||||
|
showToast,
|
||||||
|
TextArea,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -30,44 +40,41 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
<DialogContent
|
<DialogContent>
|
||||||
Icon={Icon.FiClock}
|
<div className="flex flex-row space-x-3">
|
||||||
useOwnActionButtons
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
title={t("send_reschedule_request")}
|
<Icon.FiClock className="m-auto h-6 w-6" />
|
||||||
description={t("reschedule_modal_description")}>
|
</div>
|
||||||
<div>
|
<div className="pt-1">
|
||||||
<p className="text-sm font-medium text-black">
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
{t("reason_for_reschedule_request")}
|
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
||||||
</p>
|
{t("reason_for_reschedule_request")}
|
||||||
<TextArea
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||||
data-testid="reschedule_reason"
|
</p>
|
||||||
name={t("reschedule_reason")}
|
<TextArea
|
||||||
value={rescheduleReason}
|
data-testid="reschedule_reason"
|
||||||
onChange={(e) => setRescheduleReason(e.target.value)}
|
name={t("reschedule_reason")}
|
||||||
className="mt-2"
|
value={rescheduleReason}
|
||||||
/>
|
onChange={(e) => setRescheduleReason(e.target.value)}
|
||||||
</div>
|
className="mb-5 sm:mb-6"
|
||||||
<div className="flex justify-end space-x-2">
|
/>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<DialogFooter>
|
||||||
setRescheduleReason("");
|
<DialogClose />
|
||||||
setIsOpenDialog(false);
|
<Button
|
||||||
}}
|
data-testid="send_request"
|
||||||
color="secondary">
|
disabled={isLoading}
|
||||||
{t("cancel")}
|
onClick={() => {
|
||||||
</Button>
|
rescheduleApi({
|
||||||
<Button
|
bookingId,
|
||||||
data-testid="send_request"
|
rescheduleReason,
|
||||||
disabled={isLoading}
|
});
|
||||||
onClick={() => {
|
}}>
|
||||||
rescheduleApi({
|
{t("send_reschedule_request")}
|
||||||
bookingId,
|
</Button>
|
||||||
rescheduleReason,
|
</DialogFooter>
|
||||||
});
|
</div>
|
||||||
}}>
|
|
||||||
{t("send_reschedule_request")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ type AvailabilityOption = {
|
|||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.Option>
|
</components.Option>
|
||||||
@@ -34,12 +35,13 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault } = props.data;
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
{isDefault && (
|
{isDefault && (
|
||||||
<Badge variant="blue" className="ml-2">
|
<Badge variant="blue" className="ml-2">
|
||||||
Default
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</components.SingleValue>
|
</components.SingleValue>
|
||||||
@@ -57,6 +59,8 @@ const AvailabilitySelect = ({
|
|||||||
onChange: (value: AvailabilityOption | null) => void;
|
onChange: (value: AvailabilityOption | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <SelectSkeletonLoader />;
|
return <SelectSkeletonLoader />;
|
||||||
}
|
}
|
||||||
@@ -77,6 +81,7 @@ const AvailabilitySelect = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
|||||||
@@ -1,352 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
Dropdown,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Form,
|
|
||||||
Icon,
|
|
||||||
InputLeading,
|
|
||||||
showToast,
|
|
||||||
TextAreaField,
|
|
||||||
TextField,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import { slugify } from "@lib/slugify";
|
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
|
||||||
import * as RadioArea from "@components/ui/form/radio-area";
|
|
||||||
|
|
||||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
|
||||||
export interface EventTypeParent {
|
|
||||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
|
||||||
name?: string | null;
|
|
||||||
slug?: string | null;
|
|
||||||
image?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateEventTypeBtnProps {
|
|
||||||
// set true for use on the team settings page
|
|
||||||
canAddEvents: boolean;
|
|
||||||
// set true when in use on the team settings page
|
|
||||||
isIndividualTeam?: boolean;
|
|
||||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
|
||||||
options: EventTypeParent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|
||||||
const { t, isLocaleReady } = useLocale();
|
|
||||||
const router = useRouter();
|
|
||||||
// URL encoded params
|
|
||||||
const teamId: number | undefined =
|
|
||||||
typeof router.query.teamId === "string" && router.query.teamId
|
|
||||||
? parseInt(router.query.teamId)
|
|
||||||
: undefined;
|
|
||||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
|
||||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
|
||||||
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
|
||||||
resolver: zodResolver(createEventTypeInput),
|
|
||||||
});
|
|
||||||
const { setValue, watch, register } = form;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) return;
|
|
||||||
|
|
||||||
const title: string =
|
|
||||||
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
|
|
||||||
const length: number =
|
|
||||||
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
|
|
||||||
const description: string =
|
|
||||||
typeof router.query.description === "string" && router.query.description
|
|
||||||
? router.query.description
|
|
||||||
: "";
|
|
||||||
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
|
|
||||||
|
|
||||||
setValue("title", title);
|
|
||||||
setValue("length", length);
|
|
||||||
setValue("description", description);
|
|
||||||
setValue("slug", slug);
|
|
||||||
// If query params change, update the form
|
|
||||||
}, [router.isReady, router.query, setValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const subscription = watch((value, { name, type }) => {
|
|
||||||
if (name === "title" && type === "change") {
|
|
||||||
if (value.title) setValue("slug", slugify(value.title));
|
|
||||||
else setValue("slug", "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}, [watch, setValue]);
|
|
||||||
|
|
||||||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
|
||||||
onSuccess: async ({ eventType }) => {
|
|
||||||
await router.replace("/event-types/" + eventType.id);
|
|
||||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
if (err instanceof HttpError) {
|
|
||||||
const message = `${err.statusCode}: ${err.message}`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.data?.code === "BAD_REQUEST") {
|
|
||||||
const message = `${err.data.code}: URL already exists.`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.data?.code === "UNAUTHORIZED") {
|
|
||||||
const message = `${err.data.code}: You are not able to create this event`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// inject selection data into url for correct router history
|
|
||||||
const openModal = (option: EventTypeParent) => {
|
|
||||||
const query = {
|
|
||||||
...router.query,
|
|
||||||
dialog: "new-eventtype",
|
|
||||||
eventPage: option.slug,
|
|
||||||
teamId: option.teamId,
|
|
||||||
};
|
|
||||||
if (!option.teamId) {
|
|
||||||
delete query.teamId;
|
|
||||||
}
|
|
||||||
router.push(
|
|
||||||
{
|
|
||||||
pathname: router.pathname,
|
|
||||||
query,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
{ shallow: true }
|
|
||||||
);
|
|
||||||
};
|
|
||||||
if (!isLocaleReady) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name="new-eventtype"
|
|
||||||
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
|
|
||||||
<CreateEventTypeTrigger
|
|
||||||
hasTeams={hasTeams}
|
|
||||||
canAddEvents={props.canAddEvents}
|
|
||||||
isIndividualTeam={props.isIndividualTeam}
|
|
||||||
openModal={openModal}
|
|
||||||
options={props.options}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
|
||||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
handleSubmit={(values) => {
|
|
||||||
createMutation.mutate(values);
|
|
||||||
}}>
|
|
||||||
<div className="mt-3 space-y-4">
|
|
||||||
{teamId && (
|
|
||||||
<TextField
|
|
||||||
type="hidden"
|
|
||||||
labelProps={{ style: { display: "none" } }}
|
|
||||||
{...register("teamId", { valueAsNumber: true })}
|
|
||||||
value={teamId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
|
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
|
||||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
|
||||||
<TextField
|
|
||||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
|
||||||
required
|
|
||||||
addOnLeading={<InputLeading>/{pageSlug}/</InputLeading>}
|
|
||||||
{...register("slug")}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextField
|
|
||||||
label={t("url")}
|
|
||||||
required
|
|
||||||
addOnLeading={
|
|
||||||
<InputLeading>
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
|
||||||
</InputLeading>
|
|
||||||
}
|
|
||||||
{...register("slug")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextAreaField
|
|
||||||
label={t("description")}
|
|
||||||
placeholder={t("quick_video_meeting")}
|
|
||||||
{...register("description")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<TextField
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="10"
|
|
||||||
placeholder="15"
|
|
||||||
label={t("length")}
|
|
||||||
className="pr-20"
|
|
||||||
{...register("length", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 mt-1.5 flex items-center pt-4 pr-3 text-sm text-gray-400">
|
|
||||||
{t("minutes")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{teamId && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
|
||||||
{t("scheduling_type")}
|
|
||||||
</label>
|
|
||||||
{form.formState.errors.schedulingType && (
|
|
||||||
<Alert
|
|
||||||
className="mt-1"
|
|
||||||
severity="error"
|
|
||||||
message={form.formState.errors.schedulingType.message}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<RadioArea.Group
|
|
||||||
{...register("schedulingType")}
|
|
||||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
|
||||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
|
||||||
<RadioArea.Item
|
|
||||||
value={SchedulingType.COLLECTIVE}
|
|
||||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
|
||||||
className="w-1/2 text-sm">
|
|
||||||
<strong className="mb-1 block">{t("collective")}</strong>
|
|
||||||
<p>{t("collective_description")}</p>
|
|
||||||
</RadioArea.Item>
|
|
||||||
<RadioArea.Item
|
|
||||||
value={SchedulingType.ROUND_ROBIN}
|
|
||||||
defaultChecked={type === SchedulingType.ROUND_ROBIN}
|
|
||||||
className="w-1/2 text-sm">
|
|
||||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
|
||||||
<p>{t("round_robin_description")}</p>
|
|
||||||
</RadioArea.Item>
|
|
||||||
</RadioArea.Group>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
|
||||||
<Button type="submit" loading={createMutation.isLoading}>
|
|
||||||
{t("continue")}
|
|
||||||
</Button>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button color="secondary">{t("cancel")}</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateEventTypeTrigger = {
|
|
||||||
isIndividualTeam?: boolean;
|
|
||||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
|
||||||
options: EventTypeParent[];
|
|
||||||
hasTeams: boolean;
|
|
||||||
// set true for use on the team settings page
|
|
||||||
canAddEvents: boolean;
|
|
||||||
openModal: (option: EventTypeParent) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!props.hasTeams || props.isIndividualTeam ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => props.openModal(props.options[0])}
|
|
||||||
data-testid="new-event-type"
|
|
||||||
StartIcon={Icon.FiPlus}
|
|
||||||
disabled={!props.canAddEvents}>
|
|
||||||
{t("new_event_type_btn")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
|
||||||
{props.options.map((option) => (
|
|
||||||
<CreateEventTeamsItem
|
|
||||||
key={option.slug}
|
|
||||||
option={option}
|
|
||||||
openModal={() => props.openModal(option)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateEventTeamsItem(props: {
|
|
||||||
openModal: (option: EventTypeParent) => void;
|
|
||||||
option: EventTypeParent;
|
|
||||||
}) {
|
|
||||||
const session = useSession();
|
|
||||||
const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({
|
|
||||||
memberId: session.data?.user.id as number,
|
|
||||||
teamId: props.option.teamId as number,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDisabled = membershipQuery.data?.role === "MEMBER";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
key={props.option.slug}
|
|
||||||
className={classNames(
|
|
||||||
"cursor-pointer px-3 py-2 focus:outline-none",
|
|
||||||
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
|
|
||||||
)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
onSelect={() => props.openModal(props.option)}>
|
|
||||||
<Avatar
|
|
||||||
alt={props.option.name || ""}
|
|
||||||
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
|
|
||||||
size={6}
|
|
||||||
className="inline ltr:mr-2 rtl:ml-2"
|
|
||||||
/>
|
|
||||||
{props.option.name ? props.option.name : props.option.slug}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,33 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import { EventTypeCustomInputType } from "@prisma/client";
|
||||||
|
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { Button, Select, TextField } from "@calcom/ui";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
interface OptionTypeBase {
|
||||||
label: string;
|
label: string;
|
||||||
value: EventTypeCustomInputType;
|
value: EventTypeCustomInputType;
|
||||||
|
options?: { label: string; type: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
onSubmit: (output: CustomInputParsed) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
selectedCustomInput?: CustomInputParsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
type IFormInput = CustomInputParsed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getting a random ID gives us the option to know WHICH field is changed
|
||||||
|
* when the user edits a custom field.
|
||||||
|
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
|
||||||
|
* There is very very very slim chance that this will cause a collision
|
||||||
|
* */
|
||||||
|
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
const CustomInputTypeForm: FC<Props> = (props) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -26,10 +36,22 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
||||||
|
{
|
||||||
|
value: EventTypeCustomInputType.RADIO,
|
||||||
|
label: t("radio"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
const { selectedCustomInput } = props;
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
const defaultValues = selectedCustomInput
|
||||||
|
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
|
||||||
|
: {
|
||||||
|
id: randomId(),
|
||||||
|
type: EventTypeCustomInputType.TEXT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { register, control, getValues } = useForm<IFormInput>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
const selectedInputType = useWatch({ name: "type", control });
|
||||||
@@ -40,7 +62,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(props.onSubmit)} className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
||||||
{t("input_type")}
|
{t("input_type")}
|
||||||
@@ -83,6 +105,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
{...register("placeholder")}
|
{...register("placeholder")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{selectedInputType === EventTypeCustomInputType.RADIO && (
|
||||||
|
<RadioInputHandler control={control} register={register} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
<input
|
<input
|
||||||
id="required"
|
id="required"
|
||||||
@@ -111,10 +137,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">{t("save")}</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onSubmit(getValues());
|
||||||
|
}}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function RadioInputHandler({
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
}: {
|
||||||
|
register: UseFormRegister<IFormInput>;
|
||||||
|
control: Control<IFormInput>;
|
||||||
|
}) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { fields, append, remove } = useFieldArray<IFormInput>({
|
||||||
|
control,
|
||||||
|
name: "options",
|
||||||
|
shouldUnregister: true,
|
||||||
|
});
|
||||||
|
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col ">
|
||||||
|
<Label htmlFor="radio_options">{t("options")}</Label>
|
||||||
|
<ul
|
||||||
|
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
|
||||||
|
ref={animateRef}>
|
||||||
|
<>
|
||||||
|
{fields.map((option, index) => (
|
||||||
|
<li key={`${option.id}`}>
|
||||||
|
<TextField
|
||||||
|
id={option.id}
|
||||||
|
placeholder={t("enter_option", { index: index + 1 })}
|
||||||
|
addOnFilled={false}
|
||||||
|
label={t("option", { index: index + 1 })}
|
||||||
|
labelSrOnly
|
||||||
|
{...register(`options.${index}.label` as const, { required: true })}
|
||||||
|
addOnSuffix={
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiX}
|
||||||
|
onClick={() => {
|
||||||
|
remove(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
className="!text-sm !font-medium"
|
||||||
|
onClick={() => {
|
||||||
|
append({ label: "", type: "text" });
|
||||||
|
}}>
|
||||||
|
{t("add_an_option")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
export default CustomInputTypeForm;
|
||||||
|
|||||||
@@ -1,372 +0,0 @@
|
|||||||
import { ErrorMessage } from "@hookform/error-message";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
|
||||||
import { components } from "react-select";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import {
|
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
|
||||||
getHumanReadableLocationValue,
|
|
||||||
getMessageForOrganizer,
|
|
||||||
LocationObject,
|
|
||||||
LocationType,
|
|
||||||
} from "@calcom/app-store/locations";
|
|
||||||
import { classNames } from "@calcom/lib";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
|
||||||
import { Button, Dialog, DialogContent, Form, Icon, Label, PhoneInput, Select } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
import { LinkText } from "@components/ui/LinkText";
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
|
||||||
|
|
||||||
type OptionTypeBase = {
|
|
||||||
label: string;
|
|
||||||
value: EventLocationType["type"];
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ISetLocationDialog {
|
|
||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
|
||||||
selection?: OptionTypeBase;
|
|
||||||
booking?: BookingItem;
|
|
||||||
defaultValues?: LocationObject[];
|
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
isOpenDialog: boolean;
|
|
||||||
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocationInput = (props: {
|
|
||||||
eventLocationType: EventLocationType;
|
|
||||||
locationFormMethods: ReturnType<typeof useForm>;
|
|
||||||
id: string;
|
|
||||||
required: boolean;
|
|
||||||
placeholder: string;
|
|
||||||
className?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
}): JSX.Element | null => {
|
|
||||||
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
|
|
||||||
if (eventLocationType?.organizerInputType === "text") {
|
|
||||||
return (
|
|
||||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
|
||||||
);
|
|
||||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
|
||||||
return (
|
|
||||||
<PhoneInput
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
{...remainingProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|
||||||
const {
|
|
||||||
saveLocation,
|
|
||||||
selection,
|
|
||||||
booking,
|
|
||||||
setShowLocationModal,
|
|
||||||
isOpenDialog,
|
|
||||||
defaultValues,
|
|
||||||
setSelectedLocation,
|
|
||||||
} = props;
|
|
||||||
const { t } = useLocale();
|
|
||||||
const locationsQuery = trpc.viewer.locationOptions.useQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selection) {
|
|
||||||
locationFormMethods.setValue("locationType", selection?.value);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selection]);
|
|
||||||
|
|
||||||
const locationFormSchema = z.object({
|
|
||||||
locationType: z.string(),
|
|
||||||
phone: z.string().optional(),
|
|
||||||
locationAddress: z.string().optional(),
|
|
||||||
locationLink: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.superRefine((val, ctx) => {
|
|
||||||
if (
|
|
||||||
eventLocationType &&
|
|
||||||
!eventLocationType.default &&
|
|
||||||
eventLocationType.linkType === "static" &&
|
|
||||||
eventLocationType.urlRegExp
|
|
||||||
) {
|
|
||||||
const valid = z
|
|
||||||
.string()
|
|
||||||
.regex(new RegExp(eventLocationType.urlRegExp || ""))
|
|
||||||
.safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL for ${eventLocationType.label}. ${
|
|
||||||
sampleUrl ? "Sample URL: " + sampleUrl : ""
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const valid = z.string().url().optional().safeParse(val).success;
|
|
||||||
if (!valid) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: `Invalid URL`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
displayLocationPublicly: z.boolean().optional(),
|
|
||||||
locationPhoneNumber: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const locationFormMethods = useForm({
|
|
||||||
mode: "onSubmit",
|
|
||||||
resolver: zodResolver(locationFormSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedLocation = useWatch({
|
|
||||||
control: locationFormMethods.control,
|
|
||||||
name: "locationType",
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventLocationType = getEventLocationType(selectedLocation);
|
|
||||||
|
|
||||||
const defaultLocation = defaultValues?.find(
|
|
||||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
|
||||||
);
|
|
||||||
|
|
||||||
const LocationOptions = (() => {
|
|
||||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
|
||||||
if (!eventLocationType.variable) {
|
|
||||||
console.error("eventLocationType.variable can't be undefined");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
|
|
||||||
<LocationInput
|
|
||||||
locationFormMethods={locationFormMethods}
|
|
||||||
eventLocationType={eventLocationType}
|
|
||||||
id="locationInput"
|
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
|
||||||
required
|
|
||||||
className="block w-full rounded-md border-gray-300 text-sm"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
defaultValue={
|
|
||||||
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ErrorMessage
|
|
||||||
errors={locationFormMethods.formState.errors}
|
|
||||||
name={eventLocationType.variable}
|
|
||||||
className="mt-1 text-sm text-red-500"
|
|
||||||
as="p"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!booking && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<CheckboxField
|
|
||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) =>
|
|
||||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
|
||||||
}
|
|
||||||
informationIconText={t("display_location_info_badge")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpenDialog}>
|
|
||||||
<DialogContent
|
|
||||||
type="creation"
|
|
||||||
Icon={Icon.FiMapPin}
|
|
||||||
useOwnActionButtons
|
|
||||||
title={t("edit_location")}
|
|
||||||
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
|
|
||||||
{booking && (
|
|
||||||
<>
|
|
||||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
|
||||||
<p className="mb-2 ml-1 text-sm text-black">
|
|
||||||
{getHumanReadableLocationValue(booking.location, t)}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Form
|
|
||||||
form={locationFormMethods}
|
|
||||||
className="space-y-4"
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
|
||||||
|
|
||||||
let details = {};
|
|
||||||
if (newLocation === LocationType.AttendeeInPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.InPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const eventLocationType = getEventLocationType(newLocation);
|
|
||||||
|
|
||||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
|
||||||
if (
|
|
||||||
newLocation === LocationType.Link ||
|
|
||||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
|
||||||
) {
|
|
||||||
details = { link: values.locationLink };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLocationType?.organizerInputType) {
|
|
||||||
details = {
|
|
||||||
...details,
|
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveLocation(newLocation, details);
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationType",
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
}}>
|
|
||||||
<QueryCell
|
|
||||||
query={locationsQuery}
|
|
||||||
success={({ data: locationOptions }) => {
|
|
||||||
if (!locationOptions.length) return null;
|
|
||||||
if (booking) {
|
|
||||||
locationOptions.forEach((location) => {
|
|
||||||
if (location.label === "phone") {
|
|
||||||
location.options.filter((l) => l.value !== "phone");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Controller
|
|
||||||
name="locationType"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<Select<{ label: string; value: string; icon?: string }>
|
|
||||||
maxMenuHeight={150}
|
|
||||||
name="location"
|
|
||||||
defaultValue={selection}
|
|
||||||
options={locationOptions}
|
|
||||||
components={{
|
|
||||||
Option: (props) => (
|
|
||||||
<components.Option {...props}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{props.data.icon && (
|
|
||||||
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"text-sm font-medium",
|
|
||||||
props.isSelected ? "text-white" : "text-gray-900"
|
|
||||||
)}>
|
|
||||||
{props.data.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</components.Option>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
formatOptionLabel={(e) => (
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
|
|
||||||
<span>{e.label}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) {
|
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
|
||||||
locationFormMethods.unregister([
|
|
||||||
"locationLink",
|
|
||||||
"locationAddress",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
]);
|
|
||||||
locationFormMethods.clearErrors([
|
|
||||||
"locationLink",
|
|
||||||
"locationPhoneNumber",
|
|
||||||
"locationAddress",
|
|
||||||
]);
|
|
||||||
setSelectedLocation?.(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{selectedLocation && LocationOptions}
|
|
||||||
<div className="mt-5">
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
|
||||||
Can't find the right video app? Visit our
|
|
||||||
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
|
|
||||||
App Store
|
|
||||||
</LinkText>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setShowLocationModal(false);
|
|
||||||
setSelectedLocation?.(undefined);
|
|
||||||
locationFormMethods.unregister("locationType");
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
color="secondary">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { EventTypeCustomInput } from "@prisma/client/";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +14,9 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
CustomInputItem,
|
CustomInputItem,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
Icon,
|
Icon,
|
||||||
Label,
|
Label,
|
||||||
SettingsToggle,
|
SettingsToggle,
|
||||||
@@ -26,6 +27,8 @@ import {
|
|||||||
|
|
||||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||||
|
|
||||||
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
const generateHashedLink = (id: number) => {
|
const generateHashedLink = (id: number) => {
|
||||||
const translator = short();
|
const translator = short();
|
||||||
const seed = `${id}:${new Date().getTime()}`;
|
const seed = `${id}:${new Date().getTime()}`;
|
||||||
@@ -41,11 +44,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
|
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
|
||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
@@ -99,7 +103,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
@@ -123,15 +127,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
if (e && customInputs.length === 0) {
|
if (e && customInputs.length === 0) {
|
||||||
// Push a placeholders
|
// Push a placeholders
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
} else if (!e) {
|
} else if (!e) {
|
||||||
setCustomInputs([]);
|
|
||||||
formMethods.setValue("customInputs", []);
|
formMethods.setValue("customInputs", []);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<ul className="my-4">
|
<ul className="my-4 rounded-md border">
|
||||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
{customInputs.map((customInput, idx) => (
|
||||||
<CustomInputItem
|
<CustomInputItem
|
||||||
key={idx}
|
key={idx}
|
||||||
question={customInput.label}
|
question={customInput.label}
|
||||||
@@ -154,24 +156,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
setSelectedCustomInput(undefined);
|
setSelectedCustomInput(undefined);
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
}}>
|
}}>
|
||||||
Add an input
|
{t("add_input")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<RequiresConfirmationController
|
||||||
name="requiresConfirmation"
|
seatsEnabled={seatsEnabled}
|
||||||
defaultValue={eventType.requiresConfirmation}
|
metadata={eventType.metadata}
|
||||||
render={({ field: { value, onChange } }) => (
|
requiresConfirmation={requiresConfirmation}
|
||||||
<SettingsToggle
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
title={t("requires_confirmation")}
|
|
||||||
description={t("requires_confirmation_description")}
|
|
||||||
checked={value}
|
|
||||||
onCheckedChange={(e) => onChange(e)}
|
|
||||||
disabled={seatsEnabled}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
@@ -254,7 +249,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
description={t("private_link_description")}
|
description={t("private_link_description", { appName: APP_NAME })}
|
||||||
checked={hashedLinkVisible}
|
checked={hashedLinkVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||||
@@ -307,6 +302,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
if (e) {
|
if (e) {
|
||||||
formMethods.setValue("disableGuests", true);
|
formMethods.setValue("disableGuests", true);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
@@ -352,15 +348,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
title={t("custom_event_name")}
|
title={t("custom_event_name")}
|
||||||
description={t("custom_event_name_description")}
|
description={t("custom_event_name_description")}
|
||||||
type="creation"
|
type="creation">
|
||||||
actionText="Create"
|
|
||||||
// Set event name back to what it was on close
|
|
||||||
actionOnClose={() => formMethods.setValue("eventName", eventType.eventName ?? "")}
|
|
||||||
actionOnClick={() => setShowEventNameTip(false)}>
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name")}
|
label={t("event_name")}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
/>
|
/>
|
||||||
@@ -370,6 +362,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
||||||
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
|
||||||
|
{t("create")}
|
||||||
|
</Button>
|
||||||
|
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
@@ -383,29 +381,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
type="creation"
|
type="creation"
|
||||||
Icon={Icon.FiPlus}
|
Icon={Icon.FiPlus}
|
||||||
title={t("add_new_custom_input_field")}
|
title={t("add_new_custom_input_field")}
|
||||||
useOwnActionButtons
|
|
||||||
description={t("this_input_will_shown_booking_this_event")}>
|
description={t("this_input_will_shown_booking_this_event")}>
|
||||||
<CustomInputTypeForm
|
<CustomInputTypeForm
|
||||||
selectedCustomInput={selectedCustomInput}
|
selectedCustomInput={selectedCustomInput}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
const customInput: EventTypeCustomInput = {
|
const customInput: CustomInputParsed = {
|
||||||
id: -1,
|
id: -1,
|
||||||
eventTypeId: -1,
|
eventTypeId: -1,
|
||||||
label: values.label,
|
label: values.label,
|
||||||
placeholder: values.placeholder,
|
placeholder: values.placeholder,
|
||||||
required: values.required,
|
required: values.required,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
|
options: values.options,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedCustomInput) {
|
if (selectedCustomInput) {
|
||||||
selectedCustomInput.label = customInput.label;
|
selectedCustomInput.label = customInput.label;
|
||||||
selectedCustomInput.placeholder = customInput.placeholder;
|
selectedCustomInput.placeholder = customInput.placeholder;
|
||||||
selectedCustomInput.required = customInput.required;
|
selectedCustomInput.required = customInput.required;
|
||||||
selectedCustomInput.type = customInput.type;
|
selectedCustomInput.type = customInput.type;
|
||||||
|
selectedCustomInput.options = customInput.options || undefined;
|
||||||
|
// Update by id
|
||||||
|
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
||||||
|
customInputs[inputIndex] = selectedCustomInput;
|
||||||
|
setCustomInputs(customInputs);
|
||||||
|
formMethods.setValue("customInputs", customInputs);
|
||||||
} else {
|
} else {
|
||||||
setCustomInputs(customInputs.concat(customInput));
|
const concatted = customInputs.concat({
|
||||||
formMethods.setValue("customInputs", customInputs.concat(customInput));
|
...customInput,
|
||||||
|
options: customInput.options,
|
||||||
|
});
|
||||||
|
console.log(concatted);
|
||||||
|
setCustomInputs(concatted);
|
||||||
|
formMethods.setValue("customInputs", concatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedCustomInputModalOpen(false);
|
setSelectedCustomInputModalOpen(false);
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
|
import { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Icon, Label, Select, Skeleton, TextField } from "@calcom/ui";
|
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
import { slugify } from "@lib/slugify";
|
import { slugify } from "@lib/slugify";
|
||||||
|
|
||||||
@@ -30,6 +31,22 @@ export const EventSetupTab = (
|
|||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||||
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
|
value: mins,
|
||||||
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
|
||||||
|
MultiValue<{
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}>
|
||||||
|
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
|
||||||
|
const [defaultDuration, setDefaultDuration] = useState(
|
||||||
|
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||||
|
);
|
||||||
|
|
||||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
const openLocationModal = (type: EventLocationType["type"]) => {
|
||||||
setSelectedLocation(locationOptions.find((option) => option.value === type));
|
setSelectedLocation(locationOptions.find((option) => option.value === type));
|
||||||
@@ -113,6 +130,7 @@ export const EventSetupTab = (
|
|||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Select
|
<Select
|
||||||
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
@@ -195,7 +213,7 @@ export const EventSetupTab = (
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("Title")}
|
label={t("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
@@ -219,17 +237,92 @@ export const EventSetupTab = (
|
|||||||
setValueAs: (v) => slugify(v),
|
setValueAs: (v) => slugify(v),
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<TextField
|
{multipleDuration ? (
|
||||||
required
|
<div className="space-y-4">
|
||||||
name="length"
|
<div>
|
||||||
type="number"
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
label={t("duration")}
|
{t("available_durations")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
</Skeleton>
|
||||||
defaultValue={eventType.length ?? 15}
|
<Select
|
||||||
onChange={(e) => {
|
isMulti
|
||||||
formMethods.setValue("length", Number(e.target.value));
|
defaultValue={selectedMultipleDuration}
|
||||||
}}
|
name="metadata.multipleDuration"
|
||||||
/>
|
isSearchable={false}
|
||||||
|
className="h-auto !min-h-[36px] text-sm"
|
||||||
|
options={multipleDurationOptions}
|
||||||
|
value={selectedMultipleDuration}
|
||||||
|
onChange={(options) => {
|
||||||
|
let newOptions = [...options];
|
||||||
|
newOptions = newOptions.sort((a, b) => {
|
||||||
|
return a?.value - b?.value;
|
||||||
|
});
|
||||||
|
const values = newOptions.map((opt) => opt.value);
|
||||||
|
setMultipleDuration(values);
|
||||||
|
setSelectedMultipleDuration(newOptions);
|
||||||
|
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||||
|
if (newOptions.length > 0) {
|
||||||
|
setDefaultDuration(newOptions[0]);
|
||||||
|
} else {
|
||||||
|
setDefaultDuration(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newOptions.length === 1 && defaultDuration === null) {
|
||||||
|
setDefaultDuration(newOptions[0]);
|
||||||
|
}
|
||||||
|
formMethods.setValue("metadata.multipleDuration", values);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
|
{t("default_duration")}
|
||||||
|
</Skeleton>
|
||||||
|
<Select
|
||||||
|
value={defaultDuration}
|
||||||
|
isSearchable={false}
|
||||||
|
name="length"
|
||||||
|
className="text-sm"
|
||||||
|
noOptionsMessage={() => t("default_duration_no_options")}
|
||||||
|
options={selectedMultipleDuration}
|
||||||
|
onChange={(option) => {
|
||||||
|
setDefaultDuration(
|
||||||
|
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||||
|
);
|
||||||
|
formMethods.setValue("length", Number(option?.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="length"
|
||||||
|
type="number"
|
||||||
|
label={t("duration")}
|
||||||
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
|
defaultValue={eventType.length ?? 15}
|
||||||
|
onChange={(e) => {
|
||||||
|
formMethods.setValue("length", Number(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("allow_booker_to_select_duration")}
|
||||||
|
checked={multipleDuration !== undefined}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (multipleDuration !== undefined) {
|
||||||
|
setMultipleDuration(undefined);
|
||||||
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
|
formMethods.setValue("length", eventType.length);
|
||||||
|
} else {
|
||||||
|
setMultipleDuration([]);
|
||||||
|
formMethods.setValue("metadata.multipleDuration", []);
|
||||||
|
formMethods.setValue("length", 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function getNavigation(props: {
|
|||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
icon: Icon.FiCalendar,
|
icon: Icon.FiCalendar,
|
||||||
info: `Working Hours`, // TODO: Get this from props
|
info: `default_schedule_name`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import { UnitTypeLongPlural } from "dayjs";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
|
type RequiresConfirmationControllerProps = {
|
||||||
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
|
requiresConfirmation: boolean;
|
||||||
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
|
seatsEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RequiresConfirmationController({
|
||||||
|
metadata,
|
||||||
|
requiresConfirmation,
|
||||||
|
onRequiresConfirmation,
|
||||||
|
seatsEnabled,
|
||||||
|
}: RequiresConfirmationControllerProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
|
||||||
|
metadata?.requiresConfirmationThreshold
|
||||||
|
);
|
||||||
|
const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requiresConfirmation) {
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
}
|
||||||
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="block items-start sm:flex">
|
||||||
|
<div className={!seatsEnabled ? "w-full" : ""}>
|
||||||
|
{seatsEnabled ? (
|
||||||
|
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
name="requiresConfirmation"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("requires_confirmation")}
|
||||||
|
description={t("requires_confirmation_description")}
|
||||||
|
checked={requiresConfirmation}
|
||||||
|
onCheckedChange={(val) => {
|
||||||
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
|
onRequiresConfirmation(val);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={
|
||||||
|
requiresConfirmation
|
||||||
|
? requiresConfirmationSetup === undefined
|
||||||
|
? "always"
|
||||||
|
: "notice"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
if (val === "always") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||||
|
setRequiresConfirmationSetup(undefined);
|
||||||
|
} else if (val === "notice") {
|
||||||
|
formMethods.setValue("requiresConfirmation", true);
|
||||||
|
onRequiresConfirmation(true);
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold",
|
||||||
|
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="always"
|
||||||
|
value="always"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="always" className="!m-0 flex items-center">
|
||||||
|
{t("always_requires_confirmation")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RadioGroup.Item
|
||||||
|
id="notice"
|
||||||
|
value="notice"
|
||||||
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<Label htmlFor="notice" className="!m-0 flex items-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
|
defaults="When booked with less than <time></time> notice"
|
||||||
|
components={{
|
||||||
|
time: (
|
||||||
|
<div className="mx-2 flex">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = Number(evt.target?.value);
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
unit:
|
||||||
|
requiresConfirmationSetup?.unit ??
|
||||||
|
defaultRequiresConfirmationSetup.unit,
|
||||||
|
time: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
||||||
|
}}
|
||||||
|
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
||||||
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
onChange={(evt) => {
|
||||||
|
const val = evt.target.value as UnitTypeLongPlural;
|
||||||
|
setRequiresConfirmationSetup({
|
||||||
|
time:
|
||||||
|
requiresConfirmationSetup?.time ??
|
||||||
|
defaultRequiresConfirmationSetup.time,
|
||||||
|
unit: val,
|
||||||
|
});
|
||||||
|
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
|
||||||
|
}}
|
||||||
|
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||||
|
defaultValue={
|
||||||
|
metadata?.requiresConfirmationThreshold?.unit ||
|
||||||
|
defaultRequiresConfirmationSetup.unit
|
||||||
|
}>
|
||||||
|
<option value="minutes">{t("minute_timeUnit")}</option>
|
||||||
|
<option value="hours">{t("hour_timeUnit")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,21 +4,20 @@ import { FormEvent, useRef, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
||||||
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserProfile {
|
|
||||||
user?: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
bio: string;
|
bio: string;
|
||||||
};
|
};
|
||||||
|
interface IUserProfileProps {
|
||||||
|
user: IOnboardingPageProps["user"];
|
||||||
|
}
|
||||||
|
|
||||||
const UserProfile = (props: IUserProfile) => {
|
const UserProfile = (props: IUserProfileProps) => {
|
||||||
const { user } = props;
|
const { user } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||||
@@ -100,7 +99,14 @@ const UserProfile = (props: IUserProfile) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
<div className="flex flex-row items-center justify-start rtl:justify-end">
|
||||||
{user && <AvatarSSR user={user} alt="Profile picture" className="h-16 w-16" />}
|
{user && (
|
||||||
|
<Avatar
|
||||||
|
alt={user.username || "user avatar"}
|
||||||
|
gravatarFallbackMd5={user.emailMd5}
|
||||||
|
size="lg"
|
||||||
|
imageSrc={imageSrc}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
type="hidden"
|
type="hidden"
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
interface IUserSettingsProps {
|
interface IUserSettingsProps {
|
||||||
user: User;
|
user: IOnboardingPageProps["user"];
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +24,9 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
control,
|
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: user?.name || "",
|
name: user?.name || "",
|
||||||
username: user?.username || "",
|
|
||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
@@ -41,39 +40,19 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: onSuccess,
|
onSuccess: onSuccess,
|
||||||
});
|
});
|
||||||
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
|
|
||||||
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
|
|
||||||
const onSubmit = handleSubmit((data) => {
|
const onSubmit = handleSubmit((data) => {
|
||||||
if (paymentRequired) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
timeZone: selectedTimeZone,
|
timeZone: selectedTimeZone,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentUsername, setCurrentUsername] = useState(user.username || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Username textfield */}
|
{/* Username textfield */}
|
||||||
<Controller
|
<UsernameAvailabilityField user={user} />
|
||||||
control={control}
|
|
||||||
name="username"
|
|
||||||
render={({ field: { value, ref, onChange } }) => (
|
|
||||||
<UsernameAvailability
|
|
||||||
readonly={true}
|
|
||||||
currentUsername={currentUsername}
|
|
||||||
setCurrentUsername={setCurrentUsername}
|
|
||||||
inputUsernameValue={value}
|
|
||||||
usernameRef={ref}
|
|
||||||
setInputUsernameValue={onChange}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Full name textfield */}
|
{/* Full name textfield */}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
|
||||||
|
|
||||||
interface OptionTypeBase {
|
|
||||||
label: string;
|
|
||||||
value: EventTypeCustomInputType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onSubmit: SubmitHandler<IFormInput>;
|
|
||||||
onCancel: () => void;
|
|
||||||
selectedCustomInput?: EventTypeCustomInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IFormInput = EventTypeCustomInput;
|
|
||||||
|
|
||||||
const CustomInputTypeForm: FC<Props> = (props) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const inputOptions: OptionTypeBase[] = [
|
|
||||||
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
|
|
||||||
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
|
|
||||||
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
|
|
||||||
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
|
|
||||||
];
|
|
||||||
const { selectedCustomInput } = props;
|
|
||||||
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
|
|
||||||
const { register, control, handleSubmit } = useForm<IFormInput>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const selectedInputType = useWatch({ name: "type", control });
|
|
||||||
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
|
|
||||||
|
|
||||||
const onCancel = () => {
|
|
||||||
props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(props.onSubmit)}>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("input_type")}
|
|
||||||
</label>
|
|
||||||
<Controller
|
|
||||||
name="type"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
id="type"
|
|
||||||
defaultValue={selectedInputOption}
|
|
||||||
options={inputOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
|
||||||
onChange={(option) => option && field.onChange(option.value)}
|
|
||||||
value={selectedInputOption}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("label")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="label"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.label}
|
|
||||||
{...register("label", { required: true })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{(selectedInputType === EventTypeCustomInputType.TEXT ||
|
|
||||||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("placeholder")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="placeholder"
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={selectedCustomInput?.placeholder}
|
|
||||||
{...register("placeholder")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="required"
|
|
||||||
type="checkbox"
|
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultChecked={selectedCustomInput?.required ?? true}
|
|
||||||
{...register("required")}
|
|
||||||
/>
|
|
||||||
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("is_required")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="eventTypeId"
|
|
||||||
value={selectedCustomInput?.eventTypeId || -1}
|
|
||||||
{...register("eventTypeId", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id"
|
|
||||||
value={selectedCustomInput?.id || -1}
|
|
||||||
{...register("id", { valueAsNumber: true })}
|
|
||||||
/>
|
|
||||||
<div className="mt-5 flex space-x-2 sm:mt-4">
|
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("save")}</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomInputTypeForm;
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Attendee, Booking, User } from "@prisma/client";
|
||||||
|
import type { FC } from "react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { JsonLd } from "react-schemaorg";
|
||||||
|
import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
|
||||||
|
|
||||||
|
type EventSchemaUser = Pick<User, "name" | "email">;
|
||||||
|
type EventSchemaAttendee = Pick<Attendee, "name" | "email">;
|
||||||
|
|
||||||
|
interface EventReservationSchemaInterface {
|
||||||
|
reservationId: Booking["uid"];
|
||||||
|
eventName: Booking["title"];
|
||||||
|
startTime: Booking["startTime"];
|
||||||
|
endTime: Booking["endTime"];
|
||||||
|
organizer: EventSchemaUser | null;
|
||||||
|
attendees: EventSchemaAttendee[];
|
||||||
|
location: Booking["location"];
|
||||||
|
description: Booking["description"];
|
||||||
|
status: Booking["status"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
|
||||||
|
reservationId,
|
||||||
|
eventName,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
organizer,
|
||||||
|
attendees,
|
||||||
|
location,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
const reservationStatus = useMemo<ReservationStatusType>(() => {
|
||||||
|
switch (status) {
|
||||||
|
case "ACCEPTED":
|
||||||
|
return "ReservationConfirmed";
|
||||||
|
case "REJECTED":
|
||||||
|
case "CANCELLED":
|
||||||
|
return "ReservationCancelled";
|
||||||
|
case "PENDING":
|
||||||
|
return "ReservationPending";
|
||||||
|
default:
|
||||||
|
return "ReservationHold";
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<JsonLd<EventReservation>
|
||||||
|
item={{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "EventReservation",
|
||||||
|
reservationId,
|
||||||
|
reservationStatus,
|
||||||
|
reservationFor: {
|
||||||
|
"@type": "Event",
|
||||||
|
name: eventName,
|
||||||
|
startDate: startTime.toString(),
|
||||||
|
endDate: endTime.toString(),
|
||||||
|
organizer: organizer
|
||||||
|
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
|
||||||
|
: undefined,
|
||||||
|
attendee: attendees?.map(
|
||||||
|
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
|
||||||
|
),
|
||||||
|
location: location || undefined,
|
||||||
|
description: description || undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventReservationSchema;
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
MeetingImageProps,
|
MeetingImageProps,
|
||||||
} from "@calcom/lib/OgImages";
|
} from "@calcom/lib/OgImages";
|
||||||
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
|
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { truncateOnWord } from "@calcom/lib/text";
|
import { truncateOnWord } from "@calcom/lib/text";
|
||||||
|
|
||||||
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
||||||
@@ -79,7 +80,7 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
|||||||
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
|
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
|
||||||
const truncatedDescription = truncateOnWord(description, 158);
|
const truncatedDescription = truncateOnWord(description, 158);
|
||||||
|
|
||||||
const pageTitle = title + " | Cal.com";
|
const pageTitle = title + " | " + APP_NAME;
|
||||||
let seoObject = buildSeoMeta({
|
let seoObject = buildSeoMeta({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
image,
|
image,
|
||||||
|
|||||||
@@ -72,11 +72,7 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
|
||||||
title={t("disable_2fa")}
|
|
||||||
description={t("disable_2fa_recommendation")}
|
|
||||||
type="creation"
|
|
||||||
useOwnActionButtons>
|
|
||||||
<Form form={form} handleSubmit={handleDisable}>
|
<Form form={form} handleSubmit={handleDisable}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
|||||||
@@ -130,11 +130,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent
|
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
|
||||||
title={t("enable_2fa")}
|
|
||||||
description={setupDescriptions[step]}
|
|
||||||
type="creation"
|
|
||||||
useOwnActionButtons>
|
|
||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<form onSubmit={handleSetup}>
|
<form onSubmit={handleSetup}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { objectKeys } from "@calcom/lib/objectKeys";
|
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Alert, Button, Icon, showToast, TextField } from "@calcom/ui";
|
|
||||||
|
|
||||||
import ImageUploader from "@components/ImageUploader";
|
|
||||||
import SettingInputContainer from "@components/ui/SettingInputContainer";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
team: TeamWithMembers | null | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TeamSettings(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const [hasErrors, setHasErrors] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
|
||||||
|
|
||||||
const team = props.team;
|
|
||||||
const hasLogo = !!team?.logo;
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const mutation = trpc.viewer.teams.update.useMutation({
|
|
||||||
onError: (err) => {
|
|
||||||
setHasErrors(true);
|
|
||||||
setErrorMessage(err.message);
|
|
||||||
},
|
|
||||||
async onSuccess() {
|
|
||||||
await utils.viewer.teams.get.invalidate();
|
|
||||||
showToast(t("your_team_updated_successfully"), "success");
|
|
||||||
setHasErrors(false);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const teamUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const descriptionRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>;
|
|
||||||
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
function updateTeamData() {
|
|
||||||
if (!team) return;
|
|
||||||
const variables = {
|
|
||||||
name: nameRef.current?.value,
|
|
||||||
slug: teamUrlRef.current?.value,
|
|
||||||
bio: descriptionRef.current?.value,
|
|
||||||
hideBranding: hideBrandingRef.current?.checked,
|
|
||||||
};
|
|
||||||
// remove unchanged variables
|
|
||||||
objectKeys(variables).forEach((key) => {
|
|
||||||
if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key];
|
|
||||||
});
|
|
||||||
mutation.mutate({ id: team.id, ...variables });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLogo(newLogo: string) {
|
|
||||||
if (!team) return;
|
|
||||||
logoRef.current.value = newLogo;
|
|
||||||
mutation.mutate({ id: team.id, logo: newLogo });
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeLogo = () => updateLogo("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
|
||||||
<div className="">
|
|
||||||
{hasErrors && <Alert severity="error" title={errorMessage} />}
|
|
||||||
<form
|
|
||||||
className="divide-y divide-gray-200 lg:col-span-9"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
updateTeamData();
|
|
||||||
}}>
|
|
||||||
<div className="py-6">
|
|
||||||
<div className="flex flex-col lg:flex-row">
|
|
||||||
<div className="flex-grow space-y-6">
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiLink}
|
|
||||||
label="Team URL"
|
|
||||||
htmlFor="team-url"
|
|
||||||
Input={
|
|
||||||
<TextField
|
|
||||||
name="" // typescript requires name but we don't want component to render name label
|
|
||||||
id="team-url"
|
|
||||||
addOnLeading={
|
|
||||||
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
ref={teamUrlRef}
|
|
||||||
defaultValue={team?.slug as string}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiHash}
|
|
||||||
label="Team Name"
|
|
||||||
htmlFor="name"
|
|
||||||
Input={
|
|
||||||
<input
|
|
||||||
ref={nameRef}
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder={t("your_team_name")}
|
|
||||||
required
|
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
|
|
||||||
defaultValue={team?.name as string}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiInfo}
|
|
||||||
label={t("about")}
|
|
||||||
htmlFor="about"
|
|
||||||
Input={
|
|
||||||
<>
|
|
||||||
<textarea
|
|
||||||
ref={descriptionRef}
|
|
||||||
id="about"
|
|
||||||
name="about"
|
|
||||||
rows={3}
|
|
||||||
defaultValue={team?.bio as string}
|
|
||||||
className="mt-1 block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
/>
|
|
||||||
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<SettingInputContainer
|
|
||||||
Icon={Icon.FiImage}
|
|
||||||
label="Logo"
|
|
||||||
htmlFor="avatar"
|
|
||||||
Input={
|
|
||||||
<>
|
|
||||||
<div className="mt-1 flex">
|
|
||||||
<input
|
|
||||||
ref={logoRef}
|
|
||||||
type="hidden"
|
|
||||||
name="avatar"
|
|
||||||
id="avatar"
|
|
||||||
placeholder="URL"
|
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
|
|
||||||
defaultValue={team?.logo ?? undefined}
|
|
||||||
/>
|
|
||||||
<ImageUploader
|
|
||||||
target="logo"
|
|
||||||
id="logo-upload"
|
|
||||||
buttonMsg={hasLogo ? t("edit_logo") : t("upload_a_logo")}
|
|
||||||
handleAvatarChange={updateLogo}
|
|
||||||
imageSrc={team?.logo ?? undefined}
|
|
||||||
/>
|
|
||||||
{hasLogo && (
|
|
||||||
<Button
|
|
||||||
onClick={removeLogo}
|
|
||||||
color="secondary"
|
|
||||||
type="button"
|
|
||||||
className="ml-1 py-1 text-xs">
|
|
||||||
{t("remove_logo")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr className="mt-6" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex items-start">
|
|
||||||
<div className="flex h-5 items-center">
|
|
||||||
<input
|
|
||||||
id="hide-branding"
|
|
||||||
name="hide-branding"
|
|
||||||
type="checkbox"
|
|
||||||
ref={hideBrandingRef}
|
|
||||||
defaultChecked={team?.hideBranding}
|
|
||||||
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
|
||||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
|
||||||
{t("disable_cal_branding")}
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end py-4">
|
|
||||||
<Button type="submit" color="primary">
|
|
||||||
{t("save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { MembershipRole } from "@prisma/client";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { ConfirmationDialogContent, Dialog, DialogTrigger, Icon, showToast } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
|
||||||
import LinkIconButton from "@components/ui/LinkIconButton";
|
|
||||||
|
|
||||||
export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers; role: MembershipRole }) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
|
|
||||||
|
|
||||||
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
|
|
||||||
async onSuccess() {
|
|
||||||
await utils.viewer.teams.get.invalidate();
|
|
||||||
router.push(`/settings/teams`);
|
|
||||||
showToast(t("your_team_updated_successfully"), "success");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.viewer.teams.list.invalidate();
|
|
||||||
router.push(`/settings/teams`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function deleteTeam() {
|
|
||||||
if (props.team?.id) deleteTeamMutation.mutate({ teamId: props.team.id });
|
|
||||||
}
|
|
||||||
function leaveTeam() {
|
|
||||||
if (props.team?.id)
|
|
||||||
acceptOrLeaveMutation.mutate({
|
|
||||||
teamId: props.team.id,
|
|
||||||
accept: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 px-2">
|
|
||||||
{(props.role === MembershipRole.OWNER || props.role === MembershipRole.ADMIN) && (
|
|
||||||
<CreateEventTypeButton
|
|
||||||
isIndividualTeam
|
|
||||||
canAddEvents={true}
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
teamId: props.team?.id,
|
|
||||||
name: props.team?.name,
|
|
||||||
slug: props.team?.slug,
|
|
||||||
image: props.team?.logo,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Link href={permalink} passHref={true}>
|
|
||||||
<a target="_blank">
|
|
||||||
<LinkIconButton Icon={Icon.FiExternalLink}>{t("preview")}</LinkIconButton>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
<LinkIconButton
|
|
||||||
Icon={Icon.FiLink}
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(permalink);
|
|
||||||
showToast("Copied to clipboard", "success");
|
|
||||||
}}>
|
|
||||||
{t("copy_link_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
{props.role === "OWNER" ? (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<LinkIconButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
Icon={Icon.FiTrash}>
|
|
||||||
{t("disband_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title={t("disband_team")}
|
|
||||||
confirmBtnText={t("confirm_disband_team")}
|
|
||||||
onConfirm={deleteTeam}>
|
|
||||||
{t("disband_team_confirmation_message")}
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
) : (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<LinkIconButton
|
|
||||||
Icon={Icon.FiLogOut}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}>
|
|
||||||
{t("leave_team")}
|
|
||||||
</LinkIconButton>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ConfirmationDialogContent
|
|
||||||
variety="danger"
|
|
||||||
title={t("leave_team")}
|
|
||||||
confirmBtnText={t("confirm_leave_team")}
|
|
||||||
onConfirm={leaveTeam}>
|
|
||||||
{t("leave_team_confirmation_message")}
|
|
||||||
</ConfirmationDialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{props.team?.id && props.role !== MembershipRole.MEMBER && (
|
|
||||||
<Link href={`/settings/teams/${props.team.id}/availability`}>
|
|
||||||
<div className="mt-5 hidden space-y-1 sm:block">
|
|
||||||
<LinkIconButton Icon={Icon.FiClock}>View Availability</LinkIconButton>
|
|
||||||
<p className="mt-2 text-sm text-gray-500">See your team members availability at a glance.</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { LOGO } from "@calcom/lib/constants";
|
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
@@ -21,7 +21,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
<HeadSeo title={props.title} description={props.description} />
|
<HeadSeo title={props.title} description={props.description} />
|
||||||
{props.showLogo && (
|
{props.showLogo && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" />
|
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
|
||||||
)}
|
)}
|
||||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||||
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { debounce, noop } from "lodash";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import { RefCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -24,9 +24,7 @@ import {
|
|||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
NORMAL = "NORMAL",
|
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
DOWNGRADE = "DOWNGRADE",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
@@ -37,43 +35,21 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<
|
user: Pick<User, "username" | "metadata">;
|
||||||
User,
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "bio"
|
|
||||||
| "avatar"
|
|
||||||
| "timeZone"
|
|
||||||
| "weekStart"
|
|
||||||
| "hideBranding"
|
|
||||||
| "theme"
|
|
||||||
| "plan"
|
|
||||||
| "brandColor"
|
|
||||||
| "darkBrandColor"
|
|
||||||
| "timeFormat"
|
|
||||||
| "metadata"
|
|
||||||
>;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obtainNewUsernameChangeCondition = ({
|
const obtainNewUsernameChangeCondition = ({
|
||||||
userIsPremium,
|
userIsPremium,
|
||||||
isNewUsernamePremium,
|
isNewUsernamePremium,
|
||||||
stripeCustomer,
|
|
||||||
}: {
|
}: {
|
||||||
userIsPremium: boolean;
|
userIsPremium: boolean;
|
||||||
isNewUsernamePremium: boolean;
|
isNewUsernamePremium: boolean;
|
||||||
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) {
|
if (!userIsPremium && isNewUsernamePremium) {
|
||||||
return UsernameChangeStatusEnum.UPGRADE;
|
return UsernameChangeStatusEnum.UPGRADE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") {
|
|
||||||
return UsernameChangeStatusEnum.DOWNGRADE;
|
|
||||||
}
|
|
||||||
return UsernameChangeStatusEnum.NORMAL;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||||
@@ -106,7 +82,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
setIsInputUsernamePremium(data.premium);
|
setIsInputUsernamePremium(data.premium);
|
||||||
setUsernameIsAvailable(data.available);
|
setUsernameIsAvailable(data.available);
|
||||||
}, 150),
|
}, 150),
|
||||||
[]
|
[currentUsername]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -137,7 +113,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
|
||||||
const paymentRequired = !currentUsername && stripeCustomer?.isPremium && !stripeCustomer?.paidForPremium;
|
const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
|
||||||
|
|
||||||
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
const usernameChangeCondition = obtainNewUsernameChangeCondition({
|
||||||
userIsPremium: isCurrentUsernamePremium,
|
userIsPremium: isCurrentUsernamePremium,
|
||||||
@@ -194,7 +170,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveUsername = () => {
|
const saveUsername = () => {
|
||||||
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) {
|
if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
|
||||||
updateUsername.mutate({
|
updateUsername.mutate({
|
||||||
username: inputUsernameValue,
|
username: inputUsernameValue,
|
||||||
});
|
});
|
||||||
@@ -279,14 +255,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{paymentMsg}
|
{paymentMsg}
|
||||||
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
||||||
|
|
||||||
{usernameIsAvailable && (
|
|
||||||
<p className={classNames("mt-1 text-xs text-gray-900")}>
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
|
|
||||||
<>{t("premium_to_standard_username_description")}</>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
@@ -295,13 +263,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mb-4 w-full px-4 pt-1">
|
<div className="mb-4 w-full px-4 pt-1">
|
||||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||||
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<p className="-mt-4 mb-4 text-sm text-gray-800">
|
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
|
|
||||||
t("change_username_standard_to_premium")}
|
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
|
|
||||||
t("change_username_premium_to_standard")}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
|
||||||
@@ -323,8 +286,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
<div className="mt-4 flex flex-row-reverse gap-x-2">
|
||||||
{/* redirect to checkout */}
|
{/* redirect to checkout */}
|
||||||
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE ||
|
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
@@ -336,7 +298,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Normal save */}
|
{/* Normal save */}
|
||||||
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && (
|
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isLoading}
|
||||||
@@ -347,10 +309,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<DialogClose asChild>
|
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
||||||
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
{t("cancel")}
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<Input
|
<Input
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
|
value={inputUsernameValue}
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
@@ -133,7 +134,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||||
: ""
|
: ""
|
||||||
)}
|
)}
|
||||||
defaultValue={currentUsername}
|
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setInputUsernameValue(event.target.value);
|
setInputUsernameValue(event.target.value);
|
||||||
@@ -194,10 +194,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DialogClose asChild>
|
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
||||||
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
|
{t("cancel")}
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
|
import { User } from "@calcom/prisma/client";
|
||||||
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
import { PremiumTextfield } from "./PremiumTextfield";
|
import { PremiumTextfield } from "./PremiumTextfield";
|
||||||
import { UsernameTextfield } from "./UsernameTextfield";
|
import { UsernameTextfield } from "./UsernameTextfield";
|
||||||
|
|
||||||
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
|
||||||
|
|
||||||
|
interface UsernameAvailabilityFieldProps {
|
||||||
|
onSuccessMutation?: () => void;
|
||||||
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
|
user: Pick<User, "username" | "metadata">;
|
||||||
|
}
|
||||||
|
export const UsernameAvailabilityField = ({
|
||||||
|
onSuccessMutation,
|
||||||
|
onErrorMutation,
|
||||||
|
user,
|
||||||
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
|
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
|
||||||
|
const formMethods = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
username: currentUsername,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={formMethods.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field: { ref, onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<UsernameAvailability
|
||||||
|
currentUsername={currentUsername}
|
||||||
|
setCurrentUsername={setCurrentUsername}
|
||||||
|
inputUsernameValue={value}
|
||||||
|
usernameRef={ref}
|
||||||
|
setInputUsernameValue={onChange}
|
||||||
|
onSuccessMutation={onSuccessMutation}
|
||||||
|
onErrorMutation={onErrorMutation}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DefaultSeoProps, NextSeoProps } from "next-seo";
|
import { DefaultSeoProps, NextSeoProps } from "next-seo";
|
||||||
|
|
||||||
import { SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||||
|
|
||||||
export type HeadSeoProps = {
|
export type HeadSeoProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -27,7 +27,7 @@ export const seoConfig: {
|
|||||||
defaultNextSeo: DefaultSeoProps;
|
defaultNextSeo: DefaultSeoProps;
|
||||||
} = {
|
} = {
|
||||||
headSeo: {
|
headSeo: {
|
||||||
siteName: "Cal.com",
|
siteName: APP_NAME,
|
||||||
},
|
},
|
||||||
defaultNextSeo: {
|
defaultNextSeo: {
|
||||||
twitter: {
|
twitter: {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { UserPlan } from "@prisma/client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: It should be exposed at a single place.
|
|
||||||
*/
|
|
||||||
export function useExposePlanGlobally(plan: UserPlan) {
|
|
||||||
// Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// This variable is used by embed-iframe to determine if we should allow UI configuration
|
|
||||||
window.CalComPlan = plan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
export default function usePublicPage() {
|
export default function usePublicPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isPublicPage = ["/[user]", "/success", "/cancel", "/reschedule"].find((route) =>
|
const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
|
||||||
router.pathname.startsWith(route)
|
router.pathname.startsWith(route)
|
||||||
);
|
);
|
||||||
return isPublicPage;
|
return isPublicPage;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
export default function useRouterQuery<T extends string>(name: T) {
|
||||||
|
const router = useRouter();
|
||||||
|
const existingQueryParams = router.asPath.split("?")[1];
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(existingQueryParams);
|
||||||
|
const query = Object.fromEntries(urlParams);
|
||||||
|
|
||||||
|
const setQuery = (newValue: string | number | null | undefined) => {
|
||||||
|
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
|
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return { [name]: query[name], setQuery } as {
|
||||||
|
[K in T]: string | undefined;
|
||||||
|
} & { setQuery: typeof setQuery };
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { User } from "@prisma/client";
|
export function isBrandingHidden(hideBrandingSetting: boolean, belongsToActiveTeam: boolean) {
|
||||||
|
return belongsToActiveTeam && hideBrandingSetting;
|
||||||
export function isBrandingHidden<TUser extends Pick<User, "hideBranding" | "plan">>(user: TUser) {
|
|
||||||
return user.hideBranding && user.plan !== "FREE";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ export type EmbedProps = {
|
|||||||
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||||
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||||
const ssrResponse = await getServerSideProps(context);
|
const ssrResponse = await getServerSideProps(context);
|
||||||
|
const embed = context.query.embed;
|
||||||
|
|
||||||
|
if ("redirect" in ssrResponse) {
|
||||||
|
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
||||||
|
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
|
||||||
|
|
||||||
|
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||||
|
const newDestinationUrl =
|
||||||
|
destinationUrlObj.pathname +
|
||||||
|
"/embed?" +
|
||||||
|
destinationUrlObj.searchParams.toString() +
|
||||||
|
"&embed=" +
|
||||||
|
embed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ssrResponse,
|
||||||
|
redirect: {
|
||||||
|
...ssrResponse.redirect,
|
||||||
|
destination: newDestinationUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!("props" in ssrResponse)) {
|
if (!("props" in ssrResponse)) {
|
||||||
return ssrResponse;
|
return ssrResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,21 @@ const nextConfig = {
|
|||||||
source: "/router",
|
source: "/router",
|
||||||
destination: "/apps/routing-forms/router",
|
destination: "/apps/routing-forms/router",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/success/:path*",
|
||||||
|
has: [
|
||||||
|
{
|
||||||
|
type: "query",
|
||||||
|
key: "uid",
|
||||||
|
value: "(?<uid>.*)",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
destination: "/booking/:uid/:path*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/cancel/:path*",
|
||||||
|
destination: "/booking/:path*",
|
||||||
|
},
|
||||||
/* TODO: have these files being served from another deployment or CDN {
|
/* TODO: have these files being served from another deployment or CDN {
|
||||||
source: "/embed/embed.js",
|
source: "/embed/embed.js",
|
||||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.3.2",
|
"version": "2.3.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@calcom/tsconfig": "*",
|
"@calcom/tsconfig": "*",
|
||||||
"@calcom/ui": "*",
|
"@calcom/ui": "*",
|
||||||
"@daily-co/daily-js": "^0.26.0",
|
"@daily-co/daily-js": "^0.26.0",
|
||||||
"@formkit/auto-animate": "^1.0.0-beta.1",
|
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||||
"@glidejs/glide": "^3.5.2",
|
"@glidejs/glide": "^3.5.2",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
"@hookform/error-message": "^2.0.0",
|
"@hookform/error-message": "^2.0.0",
|
||||||
@@ -107,12 +107,14 @@
|
|||||||
"react-live-chat-loader": "^2.7.3",
|
"react-live-chat-loader": "^2.7.3",
|
||||||
"react-multi-email": "^0.5.3",
|
"react-multi-email": "^0.5.3",
|
||||||
"react-phone-number-input": "^3.2.7",
|
"react-phone-number-input": "^3.2.7",
|
||||||
|
"react-schemaorg": "^2.0.0",
|
||||||
"react-select": "^5.4.0",
|
"react-select": "^5.4.0",
|
||||||
"react-timezone-select": "^1.3.2",
|
"react-timezone-select": "^1.3.2",
|
||||||
"react-use-intercom": "1.5.1",
|
"react-use-intercom": "1.5.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { GetStaticPropsContext } from "next";
|
import { GetStaticPropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
@@ -38,7 +38,7 @@ export default function Custom404() {
|
|||||||
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
|
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
const isSuccessPage = router.asPath.startsWith("/success");
|
const isSuccessPage = router.asPath.startsWith("/booking");
|
||||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||||
const isSignup = router.asPath.startsWith("/signup");
|
const isSignup = router.asPath.startsWith("/signup");
|
||||||
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
||||||
@@ -64,7 +64,7 @@ export default function Custom404() {
|
|||||||
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
|
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
|
||||||
{t("signup_requires")}
|
{t("signup_requires")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4">{t("signup_requires_description")}</p>
|
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||||||
@@ -191,7 +191,11 @@ export default function Custom404() {
|
|||||||
</span>
|
</span>
|
||||||
) : isCalcom ? (
|
) : isCalcom ? (
|
||||||
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
|
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
|
||||||
{t("the_username")} <strong className="text-blue-500">cal.com{username}</strong>{" "}
|
{t("the_username")}{" "}
|
||||||
|
<strong className="text-blue-500">
|
||||||
|
{new URL(WEBSITE_URL).hostname}
|
||||||
|
{username}
|
||||||
|
</strong>{" "}
|
||||||
{t("is_still_available")} <span className="text-blue-500">{t("register_now")}</span>.
|
{t("is_still_available")} <span className="text-blue-500">{t("register_now")}</span>.
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
export default function Error500() {
|
export default function Error500() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Something unexpected occurred | Cal.com</title>
|
<title>Something unexpected occurred | {APP_NAME}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="m-auto text-center">
|
<div className="m-auto text-center">
|
||||||
@@ -24,7 +27,7 @@ export default function Error500() {
|
|||||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||||
away for you.
|
away for you.
|
||||||
</p>
|
</p>
|
||||||
<Button href={`${WEBSITE_URL}/support`}>Contact support</Button>
|
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||||
Go back
|
Go back
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
|||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
|
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -92,7 +91,6 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const query = { ...router.query };
|
const query = { ...router.query };
|
||||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||||
useExposePlanGlobally("PRO");
|
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
@@ -272,7 +270,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { User } from "@calcom/prisma/client";
|
import { User } from "@calcom/prisma/client";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -71,7 +72,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
away: true,
|
away: true,
|
||||||
plan: true,
|
|
||||||
name: true,
|
name: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
@@ -110,6 +110,11 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
teams: {
|
||||||
|
include: {
|
||||||
|
team: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,10 +126,10 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
"strikethrough",
|
"strikethrough",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!user || !user.eventTypes) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
const [eventType]: (typeof user.eventTypes[number] & {
|
const [eventType]: (typeof user.eventTypes[number] & {
|
||||||
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
|
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
|
||||||
})[] = [
|
})[] = [
|
||||||
{
|
{
|
||||||
...user.eventTypes[0],
|
...user.eventTypes[0],
|
||||||
@@ -133,7 +138,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -150,6 +154,13 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
|
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
|
||||||
});
|
});
|
||||||
|
// Check if the user you are logging into has any active teams
|
||||||
|
const hasActiveTeam =
|
||||||
|
user.teams.filter((m) => {
|
||||||
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
|
if (metadata.success && metadata.data?.subscriptionId) return false;
|
||||||
|
return true;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
@@ -167,6 +178,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
@@ -212,7 +224,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,7 +235,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
|
|
||||||
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
|
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
|
||||||
const eventTypeObject = Object.assign({}, eventType, {
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
metadata: (eventType.metadata || {}) as JSONObject,
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
users: users.map((user) => {
|
users: users.map((user) => {
|
||||||
@@ -232,7 +243,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -262,6 +272,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma";
|
import { bookEventTypeSelect } from "@calcom/prisma";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import getBooking, { GetBookingType } from "@lib/getBooking";
|
import getBooking, { GetBookingType } from "@lib/getBooking";
|
||||||
@@ -113,7 +114,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...eventTypeRaw,
|
...eventTypeRaw,
|
||||||
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,6 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||||
schedulingType: null,
|
schedulingType: null,
|
||||||
|
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
||||||
users: users.map((u) => ({
|
users: users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name: u.name,
|
name: u.name,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import Script from "next/script";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
@@ -36,8 +37,11 @@ function MyApp(props: AppProps) {
|
|||||||
<AppProviders {...props}>
|
<AppProviders {...props}>
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
<I18nLanguageHandler />
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
{getLayout(
|
{getLayout(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
|
||||||
function toRunBeforeReactOnClient() {
|
function toRunBeforeReactOnClient() {
|
||||||
const calEmbedMode =
|
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
|
||||||
location.search.includes("embed=") ||
|
/* Iframe Name */
|
||||||
/* Iframe Name */
|
window.name.includes("cal-embed");
|
||||||
window.name.includes("cal-embed");
|
|
||||||
|
|
||||||
window.isEmbed = () => {
|
window.isEmbed = () => {
|
||||||
// Once an embed mode always an embed mode
|
// Once an embed mode always an embed mode
|
||||||
@@ -70,7 +70,9 @@ class MyDocument extends Document<Props> {
|
|||||||
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
||||||
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
||||||
*/}
|
*/}
|
||||||
<script
|
<Script
|
||||||
|
id="run-before-client"
|
||||||
|
strategy="beforeInteractive"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import path from "path";
|
|||||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||||
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { ErrorCode, verifyPassword, isPasswordValid } from "@calcom/lib/auth";
|
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
import rateLimit from "@calcom/lib/rateLimit";
|
import rateLimit from "@calcom/lib/rateLimit";
|
||||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||||
import { randomString } from "@lib/random";
|
import { randomString } from "@lib/random";
|
||||||
@@ -63,6 +64,11 @@ const providers: Provider[] = [
|
|||||||
password: true,
|
password: true,
|
||||||
twoFactorEnabled: true,
|
twoFactorEnabled: true,
|
||||||
twoFactorSecret: true,
|
twoFactorSecret: true,
|
||||||
|
teams: {
|
||||||
|
include: {
|
||||||
|
team: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,6 +122,13 @@ const providers: Provider[] = [
|
|||||||
intervalInMs: 60 * 1000, // 1 minute
|
intervalInMs: 60 * 1000, // 1 minute
|
||||||
});
|
});
|
||||||
await limiter.check(10, user.email); // 10 requests per minute
|
await limiter.check(10, user.email); // 10 requests per minute
|
||||||
|
// Check if the user you are logging into has any active teams
|
||||||
|
const hasActiveTeams =
|
||||||
|
user.teams.filter((m) => {
|
||||||
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
|
if (metadata.success && metadata.data?.subscriptionId) return false;
|
||||||
|
return true;
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
// authentication success- but does it meet the minimum password requirements?
|
// authentication success- but does it meet the minimum password requirements?
|
||||||
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
||||||
@@ -125,6 +138,7 @@ const providers: Provider[] = [
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: "USER",
|
role: "USER",
|
||||||
|
belongsToActiveTeam: hasActiveTeams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +148,7 @@ const providers: Provider[] = [
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
belongsToActiveTeam: hasActiveTeams,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -204,9 +219,9 @@ if (true) {
|
|||||||
});
|
});
|
||||||
const emailTemplate = Handlebars.compile(emailFile);
|
const emailTemplate = Handlebars.compile(emailFile);
|
||||||
transporter.sendMail({
|
transporter.sendMail({
|
||||||
from: `${process.env.EMAIL_FROM}` || "Cal.com",
|
from: `${process.env.EMAIL_FROM}` || APP_NAME,
|
||||||
to: identifier,
|
to: identifier,
|
||||||
subject: "Your sign-in link for Cal.com",
|
subject: "Your sign-in link for " + APP_NAME,
|
||||||
html: emailTemplate({
|
html: emailTemplate({
|
||||||
base_url: WEBAPP_URL,
|
base_url: WEBAPP_URL,
|
||||||
signin_url: url,
|
signin_url: url,
|
||||||
@@ -272,6 +287,7 @@ export default NextAuth({
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
impersonatedByUID: user?.impersonatedByUID,
|
impersonatedByUID: user?.impersonatedByUID,
|
||||||
|
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +323,7 @@ export default NextAuth({
|
|||||||
email: existingUser.email,
|
email: existingUser.email,
|
||||||
role: existingUser.role,
|
role: existingUser.role,
|
||||||
impersonatedByUID: token.impersonatedByUID as number,
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +341,7 @@ export default NextAuth({
|
|||||||
username: token.username as string,
|
username: token.username as string,
|
||||||
role: token.role as UserPermissionRole,
|
role: token.role as UserPermissionRole,
|
||||||
impersonatedByUID: token.impersonatedByUID as number,
|
impersonatedByUID: token.impersonatedByUID as number,
|
||||||
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return calendsoSession;
|
return calendsoSession;
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ async function handler(req: NextApiRequest) {
|
|||||||
name: parsedQuery.data.full_name,
|
name: parsedQuery.data.full_name,
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
locale: "en", // TODO: We should revisit this
|
locale: "en", // TODO: We should revisit this
|
||||||
plan: "PRO",
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
identityProvider: IdentityProvider.CAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||||
if (process.env.CRON_API_KEY !== apiKey) {
|
if (process.env.CRON_API_KEY !== apiKey) {
|
||||||
@@ -18,35 +13,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* We should add and extra check for non-paying customers in Stripe so we can
|
* Remove this endpoint
|
||||||
* downgrade them here.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
await prisma.user.updateMany({
|
|
||||||
data: {
|
|
||||||
plan: "FREE",
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
plan: "TRIAL",
|
|
||||||
OR: [
|
|
||||||
/**
|
|
||||||
* If the user doesn't have a trial end date,
|
|
||||||
* use the default 14 day trial from creation.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
createdDate: {
|
|
||||||
lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
|
|
||||||
},
|
|
||||||
trialEndsAt: null,
|
|
||||||
},
|
|
||||||
/** If it does, then honor the trial end date. */
|
|
||||||
{
|
|
||||||
trialEndsAt: {
|
|
||||||
lt: dayjs().toDate(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("AttendeeScheduledEmail", {
|
renderEmail("DisabledAppEmail", {
|
||||||
attendee: evt.attendees[0],
|
appName: "Stripe",
|
||||||
calEvent: evt,
|
appType: ["payment"],
|
||||||
|
t,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
select: {
|
select: {
|
||||||
username: true,
|
username: true,
|
||||||
id: true,
|
id: true,
|
||||||
plan: true,
|
|
||||||
createdDate: true,
|
createdDate: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -81,7 +80,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||||
: "No info"
|
: "No info"
|
||||||
}</li>
|
}</li>
|
||||||
<li><b>Plan:</b> ${user.plan}</li>
|
|
||||||
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import { MembershipRole } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
import slugify from "@lib/slugify";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ownerUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
createdDate: true,
|
|
||||||
name: true,
|
|
||||||
plan: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
|
||||||
const slug = slugify(req.body.name);
|
|
||||||
|
|
||||||
const nameCollisions = await prisma.team.count({
|
|
||||||
where: {
|
|
||||||
OR: [{ name: req.body.name }, { slug: slug }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nameCollisions > 0) {
|
|
||||||
return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already taken." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTeam = await prisma.team.create({
|
|
||||||
data: {
|
|
||||||
name: req.body.name,
|
|
||||||
slug: slug,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.membership.create({
|
|
||||||
data: {
|
|
||||||
teamId: createTeam.id,
|
|
||||||
userId: session.user.id,
|
|
||||||
role: MembershipRole.OWNER,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync Services: Close.com
|
|
||||||
closeComUpsertTeamUser(createTeam, ownerUser, MembershipRole.OWNER);
|
|
||||||
|
|
||||||
return res.status(201).json({ message: "Team created" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(404).json({ message: "Team not found" });
|
|
||||||
}
|
|
||||||
@@ -1,86 +1,72 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { defaultAvatarSrc } from "@lib/profile";
|
import { defaultAvatarSrc } from "@lib/profile";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
const querySchema = z
|
||||||
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
|
.object({
|
||||||
const username = req.query.username as string;
|
username: z.string(),
|
||||||
const teamname = req.query.teamname as string;
|
teamname: z.string(),
|
||||||
let identity;
|
})
|
||||||
let linksToThisRoute = false;
|
.partial();
|
||||||
|
|
||||||
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
|
const { username, teamname } = querySchema.parse(req.query);
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: { username },
|
||||||
username: username,
|
select: { avatar: true, email: true },
|
||||||
},
|
|
||||||
select: {
|
|
||||||
avatar: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
identity = {
|
return {
|
||||||
name: username,
|
name: username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
avatar: user?.avatar,
|
avatar: user?.avatar,
|
||||||
};
|
};
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
|
|
||||||
} else if (teamname) {
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: {
|
|
||||||
slug: teamname,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
logo: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
identity = {
|
|
||||||
name: teamname,
|
|
||||||
shouldDefaultBeNameBased: true,
|
|
||||||
avatar: team?.logo,
|
|
||||||
};
|
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
|
|
||||||
}
|
}
|
||||||
|
if (teamname) {
|
||||||
const emailMd5 = crypto
|
const team = await prisma.team.findUnique({
|
||||||
.createHash("md5")
|
where: { slug: teamname },
|
||||||
.update((identity?.email as string) || "guest@example.com")
|
select: { logo: true },
|
||||||
.digest("hex");
|
|
||||||
const img = identity?.avatar;
|
|
||||||
// If image isn't set or links to this route itself, use default avatar
|
|
||||||
if (!img || linksToThisRoute) {
|
|
||||||
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
|
||||||
if (identity?.shouldDefaultBeNameBased) {
|
|
||||||
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
|
||||||
}
|
|
||||||
res.writeHead(302, {
|
|
||||||
Location: defaultSrc,
|
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
res.end();
|
name: teamname,
|
||||||
} else if (!img.includes("data:image")) {
|
email: null,
|
||||||
res.writeHead(302, {
|
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
|
||||||
Location: img,
|
};
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
const decoded = img
|
|
||||||
.toString()
|
|
||||||
.replace("data:image/png;base64,", "")
|
|
||||||
.replace("data:image/jpeg;base64,", "");
|
|
||||||
const imageResp = Buffer.from(decoded, "base64");
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "image/png",
|
|
||||||
"Content-Length": imageResp.length,
|
|
||||||
});
|
|
||||||
res.end(imageResp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const identity = await getIdentityData(req);
|
||||||
|
const img = identity?.avatar;
|
||||||
|
// If image isn't set or links to this route itself, use default avatar
|
||||||
|
if (!img) {
|
||||||
|
res.writeHead(302, {
|
||||||
|
Location: defaultAvatarSrc({
|
||||||
|
md5: crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(identity?.email || "guest@example.com")
|
||||||
|
.digest("hex"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!img.includes("data:image")) {
|
||||||
|
res.writeHead(302, { Location: img });
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||||
|
const imageResp = Buffer.from(decoded, "base64");
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Content-Length": imageResp.length,
|
||||||
|
});
|
||||||
|
res.end(imageResp);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +1,63 @@
|
|||||||
import { InferGetStaticPropsType, NextPageContext } from "next";
|
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||||
|
import { ChangeEventHandler, useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { getSession } from "@calcom/lib/auth";
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { AppCategories } from "@calcom/prisma/client";
|
import type { AppCategories } from "@calcom/prisma/client";
|
||||||
import { AllApps, AppsLayout, AppStoreCategories, TrendingAppsSlider } from "@calcom/ui";
|
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
function AppsSearch({
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0"
|
||||||
|
addOnLeading={<Icon.FiSearch className="h-4 w-4 text-gray-500" />}
|
||||||
|
addOnClassname="!border-gray-100"
|
||||||
|
containerClassName={classNames("focus:!ring-offset-0", className)}
|
||||||
|
type="search"
|
||||||
|
autoComplete="false"
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Apps({
|
||||||
|
categories,
|
||||||
|
appStore,
|
||||||
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppsLayout isPublic heading={t("app_store")} subtitle={t("app_store_description")}>
|
<AppsLayout
|
||||||
<AppStoreCategories categories={categories} />
|
isPublic
|
||||||
<TrendingAppsSlider items={appStore} />
|
heading={t("app_store")}
|
||||||
<AllApps apps={appStore} />
|
subtitle={t("app_store_description")}
|
||||||
|
actions={(className) => (
|
||||||
|
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
||||||
|
)}
|
||||||
|
emptyStore={!appStore.length}>
|
||||||
|
{!searchText && (
|
||||||
|
<>
|
||||||
|
<AppStoreCategories categories={categories} />
|
||||||
|
<TrendingAppsSlider items={appStore} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<AllApps apps={appStore} searchText={searchText} />
|
||||||
</AppsLayout>
|
</AppsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: NextPageContext) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const ssg = await ssgInit(context);
|
const ssg = await ssgInit(context);
|
||||||
|
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
@@ -43,7 +80,6 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: ssg.dehydrate(),
|
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
name: name as AppCategories,
|
name: name as AppCategories,
|
||||||
@@ -53,6 +89,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import z from "zod";
|
|||||||
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { App } from "@calcom/types/App";
|
import { App } from "@calcom/types/App";
|
||||||
@@ -11,7 +12,6 @@ import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProp
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
DisconnectIntegration,
|
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
InstalledAppsLayout,
|
InstalledAppsLayout,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
|
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||||
|
import StepDone from "./steps/StepDone";
|
||||||
|
|
||||||
|
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||||
|
const shouldDisable = props.userCount !== 0;
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: t("administrator_user"),
|
||||||
|
description: t("lets_create_first_administrator_user"),
|
||||||
|
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||||
|
isLoading: isLoadingStep1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("enable_apps"),
|
||||||
|
description: t("enable_apps_description"),
|
||||||
|
content: <AdminAppsList baseURL="/auth/setup" />,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex items-center bg-gray-100 print:h-full">
|
||||||
|
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async () => {
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
userCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
+12
-59
@@ -1,34 +1,14 @@
|
|||||||
import { CheckIcon } from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|
||||||
import { EmailField, Label, PasswordField, TextField, WizardForm } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const StepDone = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
|
||||||
<CheckIcon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -83,13 +63,19 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
router.replace(`/auth/login?email=${data.email_address.toLowerCase()}`);
|
await signIn("credentials", {
|
||||||
|
redirect: false,
|
||||||
|
callbackUrl: "/",
|
||||||
|
email: data.email_address.toLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
router.replace("/auth/setup");
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
const longWebsiteUrl = process.env.NEXT_PUBLIC_WEBSITE_URL.length > 30;
|
const longWebsiteUrl = WEBSITE_URL.length > 30;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
@@ -201,37 +187,4 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
export default SetupFormStep1;
|
||||||
const { t } = useLocale();
|
|
||||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: t("administrator_user"),
|
|
||||||
description: t("lets_create_first_administrator_user"),
|
|
||||||
content: props.userCount !== 0 ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
|
||||||
enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps
|
|
||||||
isLoading: isLoadingStep1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="flex h-screen items-center bg-gray-100 print:h-full">
|
|
||||||
<WizardForm href="/auth/setup" steps={steps} containerClassname="max-w-sm" />
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
userCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
const StepDone = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
|
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-[420px] text-center">
|
||||||
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepDone;
|
||||||
@@ -3,7 +3,7 @@ import { signIn } from "next-auth/react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||||
import {
|
import {
|
||||||
hostedCal,
|
hostedCal,
|
||||||
@@ -161,7 +161,7 @@ const getStripePremiumUsernameUrl = async ({
|
|||||||
customer: customer.id,
|
customer: customer.id,
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
price: getPremiumPlanPrice(),
|
price: getPremiumMonthlyPlanPriceId(),
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
|
||||||
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
schedule: stringOrNumber,
|
schedule: stringOrNumber,
|
||||||
});
|
});
|
||||||
@@ -91,7 +93,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/availability"
|
backPath="/availability"
|
||||||
title={data?.schedule.name && data.schedule.name + " | " + t("availability")}
|
title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
|
||||||
heading={
|
heading={
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -209,14 +211,16 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (ctx) => {
|
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
|
const ssg = await ssgInit(ctx);
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
if (!params.success) return { notFound: true };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
schedule: params.data.schedule,
|
schedule: params.data.schedule,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
revalidate: 10, // seconds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -10,6 +11,8 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
|
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
@@ -45,6 +48,24 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
|
onSuccess: async ({ schedule }) => {
|
||||||
|
await utils.viewer.availability.list.invalidate();
|
||||||
|
showToast(
|
||||||
|
t("availability_updated_successfully", {
|
||||||
|
scheduleName: schedule.name,
|
||||||
|
}),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
if (err instanceof HttpError) {
|
||||||
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
|
showToast(message, "error");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Adds smooth delete button - item fades and old item slides into place
|
// Adds smooth delete button - item fades and old item slides into place
|
||||||
|
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
@@ -71,6 +92,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
}}
|
}}
|
||||||
key={schedule.id}
|
key={schedule.id}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
|
updateDefault={updateMutation.mutate}
|
||||||
deleteFunction={deleteMutation.mutate}
|
deleteFunction={deleteMutation.mutate}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -93,3 +115,13 @@ export default function AvailabilityPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -33,13 +34,14 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
|
|||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma, { baseUserSelect } from "@calcom/prisma";
|
import prisma, { baseUserSelect } from "@calcom/prisma";
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Button, EmailInput, Icon } from "@calcom/ui";
|
import { Button, EmailInput, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
|
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
import { HeadSeo } from "@components/seo/head-seo";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -55,26 +57,41 @@ function redirectToExternalUrl(url: string) {
|
|||||||
function RedirectionToast({ url }: { url: string }) {
|
function RedirectionToast({ url }: { url: string }) {
|
||||||
const [timeRemaining, setTimeRemaining] = useState(10);
|
const [timeRemaining, setTimeRemaining] = useState(10);
|
||||||
const [isToastVisible, setIsToastVisible] = useState(true);
|
const [isToastVisible, setIsToastVisible] = useState(true);
|
||||||
const parsedSuccessUrl = new URL(document.URL);
|
|
||||||
const parsedExternalUrl = new URL(url);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
|
||||||
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
|
||||||
parsedSuccessUrl.searchParams.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlWithSuccessParams =
|
|
||||||
parsedExternalUrl.origin +
|
|
||||||
parsedExternalUrl.pathname +
|
|
||||||
"?" +
|
|
||||||
parsedSuccessUrl.searchParams.toString() +
|
|
||||||
parsedExternalUrl.hash;
|
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const timerRef = useRef<number | null>(null);
|
const timerRef = useRef<number | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const { cancel: isCancellationMode } = querySchema.parse(router.query);
|
||||||
|
const urlWithSuccessParamsRef = useRef<string | null>();
|
||||||
|
if (isCancellationMode && timerRef.current) {
|
||||||
|
setIsToastVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isToastVisible && timerRef.current) {
|
||||||
|
window.clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
}, [isToastVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsedExternalUrl = new URL(url);
|
||||||
|
|
||||||
|
const parsedSuccessUrl = new URL(document.URL);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
||||||
|
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
||||||
|
parsedSuccessUrl.searchParams.set(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlWithSuccessParams =
|
||||||
|
parsedExternalUrl.origin +
|
||||||
|
parsedExternalUrl.pathname +
|
||||||
|
"?" +
|
||||||
|
parsedSuccessUrl.searchParams.toString() +
|
||||||
|
parsedExternalUrl.hash;
|
||||||
|
urlWithSuccessParamsRef.current = urlWithSuccessParams;
|
||||||
|
|
||||||
timerRef.current = window.setInterval(() => {
|
timerRef.current = window.setInterval(() => {
|
||||||
if (timeRemaining > 0) {
|
if (timeRemaining > 0) {
|
||||||
setTimeRemaining((timeRemaining) => {
|
setTimeRemaining((timeRemaining) => {
|
||||||
@@ -88,7 +105,7 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(timerRef.current as number);
|
window.clearInterval(timerRef.current as number);
|
||||||
};
|
};
|
||||||
}, [timeRemaining, urlWithSuccessParams]);
|
}, [timeRemaining, url]);
|
||||||
|
|
||||||
if (!isToastVisible) {
|
if (!isToastVisible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -111,7 +128,9 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
redirectToExternalUrl(urlWithSuccessParams);
|
if (urlWithSuccessParamsRef.current) {
|
||||||
|
redirectToExternalUrl(urlWithSuccessParamsRef.current);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
@@ -122,7 +141,6 @@ function RedirectionToast({ url }: { url: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsToastVisible(false);
|
setIsToastVisible(false);
|
||||||
window.clearInterval(timerRef.current as number);
|
|
||||||
}}
|
}}
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<Icon.FiX className="h-6 w-6 text-white" />
|
<Icon.FiX className="h-6 w-6 text-white" />
|
||||||
@@ -147,6 +165,7 @@ const querySchema = z.object({
|
|||||||
uid: z.string(),
|
uid: z.string(),
|
||||||
allRemainingBookings: stringToBoolean,
|
allRemainingBookings: stringToBoolean,
|
||||||
cancel: stringToBoolean,
|
cancel: stringToBoolean,
|
||||||
|
changes: stringToBoolean,
|
||||||
reschedule: stringToBoolean,
|
reschedule: stringToBoolean,
|
||||||
isSuccessBookingPage: z.string().optional(),
|
isSuccessBookingPage: z.string().optional(),
|
||||||
});
|
});
|
||||||
@@ -159,26 +178,33 @@ export default function Success(props: SuccessProps) {
|
|||||||
allRemainingBookings,
|
allRemainingBookings,
|
||||||
isSuccessBookingPage,
|
isSuccessBookingPage,
|
||||||
cancel: isCancellationMode,
|
cancel: isCancellationMode,
|
||||||
|
changes,
|
||||||
} = querySchema.parse(router.query);
|
} = querySchema.parse(router.query);
|
||||||
|
|
||||||
if (isCancellationMode && typeof window !== "undefined") {
|
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
|
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
|
||||||
? props.bookingInfo.location[0] || ""
|
? props.bookingInfo.location[0]
|
||||||
: props.bookingInfo.location || "";
|
: // If there is no location set then we default to Cal Video
|
||||||
|
"integrations:daily";
|
||||||
|
|
||||||
if (!location) {
|
if (!location) {
|
||||||
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
||||||
console.error(`No location found `);
|
console.error(`No location found `);
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = props.bookingInfo?.user?.name;
|
|
||||||
const email = props.bookingInfo?.user?.email;
|
const email = props.bookingInfo?.user?.email;
|
||||||
const status = props.bookingInfo?.status;
|
const status = props.bookingInfo?.status;
|
||||||
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
||||||
const cancellationReason = props.bookingInfo.cancellationReason;
|
const cancellationReason = props.bookingInfo.cancellationReason;
|
||||||
|
|
||||||
|
const attendeeName =
|
||||||
|
typeof props?.bookingInfo?.attendees?.[0]?.name === "string"
|
||||||
|
? props?.bookingInfo?.attendees?.[0]?.name
|
||||||
|
: "Nameless";
|
||||||
|
|
||||||
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
@@ -189,6 +215,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
|
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
function setIsCancellationMode(value: boolean) {
|
function setIsCancellationMode(value: boolean) {
|
||||||
if (value) router.query.cancel = "true";
|
if (value) router.query.cancel = "true";
|
||||||
@@ -199,8 +226,6 @@ export default function Success(props: SuccessProps) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const attendeeName = typeof name === "string" ? name : "Nameless";
|
|
||||||
|
|
||||||
const eventNameObject = {
|
const eventNameObject = {
|
||||||
attendeeName,
|
attendeeName,
|
||||||
eventType: props.eventType.title,
|
eventType: props.eventType.title,
|
||||||
@@ -220,7 +245,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/success"));
|
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
|
||||||
@@ -231,7 +256,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
sdkActionManager.fire("bookingSuccessful", {
|
sdkActionManager.fire("bookingSuccessful", {
|
||||||
eventType,
|
eventType,
|
||||||
date: date.toString(),
|
date: date.toString(),
|
||||||
duration: eventType.length,
|
duration: calculatedDuration,
|
||||||
organizer: {
|
organizer: {
|
||||||
name: users[0].name || "Nameless",
|
name: users[0].name || "Nameless",
|
||||||
email: users[0].email || "Email-less",
|
email: users[0].email || "Email-less",
|
||||||
@@ -245,6 +270,12 @@ export default function Success(props: SuccessProps) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [eventType, needsConfirmation]);
|
}, [eventType, needsConfirmation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCalculatedDuration(
|
||||||
|
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
function eventLink(): string {
|
function eventLink(): string {
|
||||||
const optional: { location?: string } = {};
|
const optional: { location?: string } = {};
|
||||||
if (location) {
|
if (location) {
|
||||||
@@ -263,7 +294,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
title: eventName,
|
title: eventName,
|
||||||
description: props.eventType.description ? props.eventType.description : undefined,
|
description: props.eventType.description ? props.eventType.description : undefined,
|
||||||
/** formatted to required type of description ^ */
|
/** formatted to required type of description ^ */
|
||||||
duration: { minutes: props.eventType.length },
|
duration: {
|
||||||
|
minutes: calculatedDuration,
|
||||||
|
},
|
||||||
...optional,
|
...optional,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,6 +338,19 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
||||||
|
{!isEmbed && (
|
||||||
|
<EventReservationSchema
|
||||||
|
reservationId={bookingInfo.uid}
|
||||||
|
eventName={eventName}
|
||||||
|
startTime={bookingInfo.startTime}
|
||||||
|
endTime={bookingInfo.endTime}
|
||||||
|
organizer={bookingInfo.user}
|
||||||
|
attendees={bookingInfo.attendees}
|
||||||
|
location={locationToDisplay}
|
||||||
|
description={bookingInfo.description}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{userIsOwner && !isEmbed && (
|
{userIsOwner && !isEmbed && (
|
||||||
<div className="mt-2 ml-4 -mb-4">
|
<div className="mt-2 ml-4 -mb-4">
|
||||||
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
||||||
@@ -319,7 +365,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
||||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||||
{eventType.successRedirectUrl ? <RedirectionToast url={eventType.successRedirectUrl} /> : null}{" "}
|
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
|
||||||
|
<RedirectionToast url={eventType.successRedirectUrl} />
|
||||||
|
) : null}{" "}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "text-center" : "",
|
shouldAlignCentrally ? "text-center" : "",
|
||||||
@@ -389,6 +437,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
<div className="col-span-2 mb-6 last:mb-0">
|
<div className="col-span-2 mb-6 last:mb-0">
|
||||||
<RecurringBookings
|
<RecurringBookings
|
||||||
eventType={props.eventType}
|
eventType={props.eventType}
|
||||||
|
duration={calculatedDuration}
|
||||||
recurringBookings={props.recurringBookings}
|
recurringBookings={props.recurringBookings}
|
||||||
allRemainingBookings={allRemainingBookings}
|
allRemainingBookings={allRemainingBookings}
|
||||||
date={date}
|
date={date}
|
||||||
@@ -399,7 +448,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
{(bookingInfo?.user || bookingInfo?.attendees) && (
|
{(bookingInfo?.user || bookingInfo?.attendees) && (
|
||||||
<>
|
<>
|
||||||
<div className="font-medium">{t("who")}</div>
|
<div className="font-medium">{t("who")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">
|
<div className="col-span-2 last:mb-0">
|
||||||
<>
|
<>
|
||||||
{bookingInfo?.user && (
|
{bookingInfo?.user && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
@@ -441,13 +490,40 @@ export default function Success(props: SuccessProps) {
|
|||||||
)}
|
)}
|
||||||
{customInputs &&
|
{customInputs &&
|
||||||
Object.keys(customInputs).map((key) => {
|
Object.keys(customInputs).map((key) => {
|
||||||
|
// This breaks if you have two label that are the same.
|
||||||
|
// TODO: Fix this in another PR
|
||||||
const customInput = customInputs[key as keyof typeof customInputs];
|
const customInput = customInputs[key as keyof typeof customInputs];
|
||||||
|
const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{customInput !== "" && (
|
{eventTypeCustomFound?.type === "RADIO" && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-2 pr-3 font-medium">{key}</div>
|
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
||||||
<div className="col-span-2 mt-2 mb-2">
|
{eventTypeCustomFound.label}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 mt-1 mb-2">
|
||||||
|
{eventTypeCustomFound.options &&
|
||||||
|
eventTypeCustomFound.options.map((option) => {
|
||||||
|
const selected = option.label == customInput;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.label}
|
||||||
|
className={classNames(
|
||||||
|
"flex space-x-1",
|
||||||
|
!selected && "text-gray-500"
|
||||||
|
)}>
|
||||||
|
<p>{option.label}</p>
|
||||||
|
<span>{option.label === customInput && "✅"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
|
||||||
|
<div className="col-span-3 mt-2 mb-2">
|
||||||
{typeof customInput === "boolean" ? (
|
{typeof customInput === "boolean" ? (
|
||||||
<p>{customInput ? "true" : "false"}</p>
|
<p>{customInput ? "true" : "false"}</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -469,7 +545,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!needsConfirmation &&
|
{(!needsConfirmation || !userIsOwner) &&
|
||||||
!isCancelled &&
|
!isCancelled &&
|
||||||
(!isCancellationMode ? (
|
(!isCancellationMode ? (
|
||||||
<>
|
<>
|
||||||
@@ -513,119 +589,125 @@ export default function Success(props: SuccessProps) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
|
{userIsOwner &&
|
||||||
<>
|
!needsConfirmation &&
|
||||||
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
!isCancellationMode &&
|
||||||
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
|
!isCancelled &&
|
||||||
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
calculatedDuration && (
|
||||||
{t("add_to_calendar")}
|
<>
|
||||||
</span>
|
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
||||||
<div className="justify-left mt-1 flex text-left sm:mt-0">
|
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
|
||||||
<Link
|
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||||
href={
|
{t("add_to_calendar")}
|
||||||
`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
</span>
|
||||||
.utc()
|
<div className="justify-left mt-1 flex text-left sm:mt-0">
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
<Link
|
||||||
.add(props.eventType.length, "minute")
|
href={
|
||||||
.utc()
|
`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
.utc()
|
||||||
props.eventType.description
|
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
||||||
}` +
|
.add(calculatedDuration, "minute")
|
||||||
(typeof location === "string"
|
.utc()
|
||||||
? "&location=" + encodeURIComponent(location)
|
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
||||||
: "") +
|
props.eventType.description
|
||||||
(props.eventType.recurringEvent
|
}` +
|
||||||
? "&recur=" +
|
(typeof location === "string"
|
||||||
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
|
? "&location=" + encodeURIComponent(location)
|
||||||
: "")
|
: "") +
|
||||||
}>
|
(props.eventType.recurringEvent
|
||||||
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
|
? "&recur=" +
|
||||||
<svg
|
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
|
||||||
className="-mt-1.5 inline-block h-4 w-4"
|
: "")
|
||||||
fill="currentColor"
|
}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
|
||||||
viewBox="0 0 24 24">
|
<svg
|
||||||
<title>Google</title>
|
className="-mt-1.5 inline-block h-4 w-4"
|
||||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
fill="currentColor"
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</a>
|
viewBox="0 0 24 24">
|
||||||
</Link>
|
<title>Google</title>
|
||||||
<Link
|
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||||
href={
|
</svg>
|
||||||
encodeURI(
|
</a>
|
||||||
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
|
</Link>
|
||||||
props.eventType.description +
|
<Link
|
||||||
"&enddt=" +
|
href={
|
||||||
date.add(props.eventType.length, "minute").utc().format() +
|
encodeURI(
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
|
||||||
date.utc().format() +
|
props.eventType.description +
|
||||||
"&subject=" +
|
"&enddt=" +
|
||||||
eventName
|
date.add(calculatedDuration, "minute").utc().format() +
|
||||||
) + (location ? "&location=" + location : "")
|
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
||||||
}>
|
date.utc().format() +
|
||||||
<a
|
"&subject=" +
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
eventName
|
||||||
target="_blank">
|
) + (location ? "&location=" + location : "")
|
||||||
<svg
|
}>
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
<a
|
||||||
fill="currentColor"
|
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
target="_blank">
|
||||||
viewBox="0 0 24 24">
|
<svg
|
||||||
<title>Microsoft Outlook</title>
|
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||||
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
fill="currentColor"
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</a>
|
viewBox="0 0 24 24">
|
||||||
</Link>
|
<title>Microsoft Outlook</title>
|
||||||
<Link
|
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
||||||
href={
|
</svg>
|
||||||
encodeURI(
|
</a>
|
||||||
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
|
</Link>
|
||||||
props.eventType.description +
|
<Link
|
||||||
"&enddt=" +
|
href={
|
||||||
date.add(props.eventType.length, "minute").utc().format() +
|
encodeURI(
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
|
||||||
date.utc().format() +
|
props.eventType.description +
|
||||||
"&subject=" +
|
"&enddt=" +
|
||||||
eventName
|
date.add(calculatedDuration, "minute").utc().format() +
|
||||||
) + (location ? "&location=" + location : "")
|
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
||||||
}>
|
date.utc().format() +
|
||||||
<a
|
"&subject=" +
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
eventName
|
||||||
target="_blank">
|
) + (location ? "&location=" + location : "")
|
||||||
<svg
|
}>
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
<a
|
||||||
fill="currentColor"
|
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
target="_blank">
|
||||||
viewBox="0 0 24 24">
|
<svg
|
||||||
<title>Microsoft Office</title>
|
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||||
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
fill="currentColor"
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</a>
|
viewBox="0 0 24 24">
|
||||||
</Link>
|
<title>Microsoft Office</title>
|
||||||
<Link href={"data:text/calendar," + eventLink()}>
|
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
||||||
<a
|
</svg>
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
</a>
|
||||||
download={props.eventType.title + ".ics"}>
|
</Link>
|
||||||
<svg
|
<Link href={"data:text/calendar," + eventLink()}>
|
||||||
version="1.1"
|
<a
|
||||||
fill="currentColor"
|
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
download={props.eventType.title + ".ics"}>
|
||||||
viewBox="0 0 1000 1000"
|
<svg
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
version="1.1"
|
||||||
<title>{t("other")}</title>
|
fill="currentColor"
|
||||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
viewBox="0 0 1000 1000"
|
||||||
</a>
|
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
||||||
</Link>
|
<title>{t("other")}</title>
|
||||||
|
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
{session === null && !(userIsOwner || props.hideBranding) && (
|
{session === null && !(userIsOwner || props.hideBranding) && (
|
||||||
<>
|
<>
|
||||||
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
|
||||||
<div className="border-bookinglightest text-booking-lighter dark:border-darkgray-300 pt-8 text-center text-xs dark:text-white">
|
<div className="border-bookinglightest text-booking-lighter dark:border-darkgray-300 pt-8 text-center text-xs dark:text-white">
|
||||||
<a href="https://cal.com/signup">{t("create_booking_link_with_calcom")}</a>
|
<a href="https://cal.com/signup">
|
||||||
|
{t("create_booking_link_with_calcom", { appName: APP_NAME })}
|
||||||
|
</a>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -669,6 +751,7 @@ type RecurringBookingsProps = {
|
|||||||
eventType: SuccessProps["eventType"];
|
eventType: SuccessProps["eventType"];
|
||||||
recurringBookings: SuccessProps["recurringBookings"];
|
recurringBookings: SuccessProps["recurringBookings"];
|
||||||
date: dayjs.Dayjs;
|
date: dayjs.Dayjs;
|
||||||
|
duration: number | undefined;
|
||||||
is24h: boolean;
|
is24h: boolean;
|
||||||
allRemainingBookings: boolean;
|
allRemainingBookings: boolean;
|
||||||
isCancelled: boolean;
|
isCancelled: boolean;
|
||||||
@@ -677,6 +760,7 @@ type RecurringBookingsProps = {
|
|||||||
export function RecurringBookings({
|
export function RecurringBookings({
|
||||||
eventType,
|
eventType,
|
||||||
recurringBookings,
|
recurringBookings,
|
||||||
|
duration,
|
||||||
date,
|
date,
|
||||||
allRemainingBookings,
|
allRemainingBookings,
|
||||||
is24h,
|
is24h,
|
||||||
@@ -689,6 +773,8 @@ export function RecurringBookings({
|
|||||||
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
|
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (!duration) return null;
|
||||||
|
|
||||||
if (recurringBookingsSorted && allRemainingBookings) {
|
if (recurringBookingsSorted && allRemainingBookings) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -707,7 +793,7 @@ export function RecurringBookings({
|
|||||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
||||||
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -725,7 +811,7 @@ export function RecurringBookings({
|
|||||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
||||||
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -741,7 +827,7 @@ export function RecurringBookings({
|
|||||||
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
|
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
|
||||||
<br />
|
<br />
|
||||||
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
|
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
|
||||||
{formatTime(dayjs(date).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||||
<span className="text-bookinglight">({timeZone()})</span>
|
<span className="text-bookinglight">({timeZone()})</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -762,6 +848,7 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
requiresConfirmation: true,
|
requiresConfirmation: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
|
customInputs: true,
|
||||||
locations: true,
|
locations: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
@@ -771,7 +858,6 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
name: true,
|
name: true,
|
||||||
username: true,
|
username: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
plan: true,
|
|
||||||
theme: true,
|
theme: true,
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
@@ -828,22 +914,25 @@ const handleSeatsEventTypeOnBooking = (
|
|||||||
seatsShowAttendees: boolean | null;
|
seatsShowAttendees: boolean | null;
|
||||||
[x: string | number | symbol]: unknown;
|
[x: string | number | symbol]: unknown;
|
||||||
},
|
},
|
||||||
booking: Partial<
|
bookingInfo: Partial<
|
||||||
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
|
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
|
||||||
>,
|
>,
|
||||||
email: string
|
email: string
|
||||||
) => {
|
) => {
|
||||||
if (eventType?.seatsPerTimeSlot !== null) {
|
if (eventType?.seatsPerTimeSlot !== null) {
|
||||||
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
|
||||||
delete booking.description;
|
delete bookingInfo.description;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!eventType.seatsShowAttendees) {
|
if (!eventType.seatsShowAttendees) {
|
||||||
const attendee = booking?.attendees?.find((a) => a.email === email);
|
const attendee = bookingInfo?.attendees?.find((a) => {
|
||||||
booking["attendees"] = attendee ? [attendee] : [];
|
return a.email === email;
|
||||||
|
});
|
||||||
|
|
||||||
|
bookingInfo["attendees"] = attendee ? [attendee] : [];
|
||||||
}
|
}
|
||||||
return;
|
return bookingInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
@@ -865,6 +954,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
smsReminderNumber: true,
|
smsReminderNumber: true,
|
||||||
recurringEventId: true,
|
recurringEventId: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
location: true,
|
location: true,
|
||||||
status: true,
|
status: true,
|
||||||
cancellationReason: true,
|
cancellationReason: true,
|
||||||
@@ -901,6 +991,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
|
||||||
|
|
||||||
const eventTypeRaw = !bookingInfo.eventTypeId
|
const eventTypeRaw = !bookingInfo.eventTypeId
|
||||||
? getDefaultEvent(eventTypeSlug || "")
|
? getDefaultEvent(eventTypeSlug || "")
|
||||||
@@ -920,7 +1011,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
select: baseUserSelect,
|
select: baseUserSelect,
|
||||||
});
|
});
|
||||||
if (user) {
|
if (user) {
|
||||||
eventTypeRaw.users.push(user);
|
eventTypeRaw.users.push({
|
||||||
|
...user,
|
||||||
|
avatar: "",
|
||||||
|
allowDynamicBooking: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,6 +1031,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
};
|
};
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
@@ -947,7 +1043,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
slug: eventType.team?.slug || eventType.users[0]?.username || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (bookingInfo !== null && email) {
|
if (bookingInfo !== null && email && eventType.seatsPerTimeSlot) {
|
||||||
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
|
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,9 +14,18 @@ import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
|||||||
import BookingListItem from "@components/booking/BookingListItem";
|
import BookingListItem from "@components/booking/BookingListItem";
|
||||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||||
|
|
||||||
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
||||||
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
|
||||||
|
|
||||||
|
type RecurringInfo = {
|
||||||
|
recurringEventId: string | null;
|
||||||
|
count: number;
|
||||||
|
firstDate: Date | null;
|
||||||
|
bookings: { [key: string]: Date[] };
|
||||||
|
};
|
||||||
|
|
||||||
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
|
||||||
|
|
||||||
const descriptionByStatus: Record<BookingListingStatus, string> = {
|
const descriptionByStatus: Record<BookingListingStatus, string> = {
|
||||||
@@ -73,6 +82,18 @@ export default function Bookings() {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let recurringInfoToday: RecurringInfo | undefined;
|
||||||
|
|
||||||
|
const bookingsToday =
|
||||||
|
query.data?.pages.map((page) =>
|
||||||
|
page.bookings.filter((booking: BookingOutput) => {
|
||||||
|
recurringInfoToday = page.recurringInfo.find(
|
||||||
|
(info) => info.recurringEventId === booking.recurringEventId
|
||||||
|
);
|
||||||
|
return new Date(booking.startTime).toDateString() === new Date().toDateString();
|
||||||
|
})
|
||||||
|
)[0] || [];
|
||||||
|
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -83,45 +104,65 @@ export default function Bookings() {
|
|||||||
)}
|
)}
|
||||||
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
||||||
{query.status === "success" && !isEmpty && (
|
{query.status === "success" && !isEmpty && (
|
||||||
<div className="pt-2 xl:pt-0">
|
<>
|
||||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
{!!bookingsToday.length && status === "upcoming" && (
|
||||||
{/* TODO: add today only for the current day
|
<div className="mb-6 pt-2 xl:pt-0">
|
||||||
<p className="pb-3 text-xs font-medium leading-4 text-gray-500 uppercase">{t("today")}</p>
|
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||||
*/}
|
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
|
||||||
|
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||||
<div className="overflow-hidden rounded-md border border-gray-200">
|
<table className="w-full max-w-full table-fixed">
|
||||||
<table className="w-full max-w-full table-fixed">
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
<Fragment>
|
||||||
{query.data.pages.map((page, index) => (
|
{bookingsToday.map((booking: BookingOutput) => (
|
||||||
<Fragment key={index}>
|
|
||||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
|
||||||
const recurringInfo = page.recurringInfo.find(
|
|
||||||
(info) => info.recurringEventId === booking.recurringEventId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<BookingListItem
|
<BookingListItem
|
||||||
key={booking.id}
|
key={booking.id}
|
||||||
listingStatus={status}
|
listingStatus={status}
|
||||||
recurringInfo={recurringInfo}
|
recurringInfo={recurringInfoToday}
|
||||||
{...booking}
|
{...booking}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
</Fragment>
|
||||||
</Fragment>
|
</tbody>
|
||||||
))}
|
</table>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="pt-2 xl:pt-0">
|
||||||
|
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p>
|
||||||
|
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||||
|
<table className="w-full max-w-full table-fixed">
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
||||||
|
{query.data.pages.map((page, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
|
||||||
|
const recurringInfo = page.recurringInfo.find(
|
||||||
|
(info) => info.recurringEventId === booking.recurringEventId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<BookingListItem
|
||||||
|
key={booking.id}
|
||||||
|
listingStatus={status}
|
||||||
|
recurringInfo={recurringInfo}
|
||||||
|
{...booking}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 text-center" ref={buttonInView.ref}>
|
||||||
|
<Button
|
||||||
|
color="minimal"
|
||||||
|
loading={query.isFetchingNextPage}
|
||||||
|
disabled={!query.hasNextPage}
|
||||||
|
onClick={() => query.fetchNextPage()}>
|
||||||
|
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center" ref={buttonInView.ref}>
|
</>
|
||||||
<Button
|
|
||||||
color="minimal"
|
|
||||||
loading={query.isFetchingNextPage}
|
|
||||||
disabled={!query.hasNextPage}
|
|
||||||
onClick={() => query.fetchNextPage()}>
|
|
||||||
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{query.status === "success" && isEmpty && (
|
{query.status === "success" && isEmpty && (
|
||||||
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
|
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
|
||||||
@@ -140,14 +181,16 @@ export default function Bookings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (ctx) => {
|
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
|
const ssg = await ssgInit(ctx);
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
if (!params.success) return { notFound: true };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
status: params.data.status,
|
status: params.data.status,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { GetServerSidePropsContext } from "next";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
uid: z.string(),
|
|
||||||
allRemainingBookings: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.transform((val) => (val ? JSON.parse(val) : false)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Type() {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const { allRemainingBookings, uid } = querySchema.parse(context.query);
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: `/success?uid=${uid}&allRemainingBookings=${allRemainingBookings}&cancel=true`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -90,7 +90,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +116,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
name: u.name,
|
name: u.name,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
hideBranding: u.hideBranding,
|
hideBranding: u.hideBranding,
|
||||||
plan: u.plan,
|
|
||||||
timeZone: u.timeZone,
|
timeZone: u.timeZone,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
@@ -158,7 +156,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
away: user.away,
|
away: user.away,
|
||||||
isDynamicGroup: false,
|
isDynamicGroup: false,
|
||||||
profile,
|
profile,
|
||||||
plan: user.plan,
|
|
||||||
date,
|
date,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
workingHours,
|
workingHours,
|
||||||
@@ -166,6 +163,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
previousPage: context.req.headers.referer ?? null,
|
previousPage: context.req.headers.referer ?? null,
|
||||||
booking,
|
booking,
|
||||||
users: [user.username],
|
users: [user.username],
|
||||||
|
isBrandingHidden: user.hideBranding,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
|
|||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
@@ -69,7 +70,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!eventTypeRaw) return { notFound: true };
|
if (!eventTypeRaw) return { notFound: true };
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...eventTypeRaw,
|
...eventTypeRaw,
|
||||||
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
|
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
...e,
|
...e,
|
||||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||||
|
customInputs: customInputSchema.array().parse(e.customInputs || []),
|
||||||
schedulingType: null,
|
schedulingType: null,
|
||||||
users: users.map((u) => ({
|
users: users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -8,23 +8,24 @@ import { useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
||||||
import getApps, { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
||||||
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
||||||
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Form, showToast } from "@calcom/ui";
|
import { Form, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||||
@@ -39,6 +40,7 @@ import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayo
|
|||||||
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
||||||
|
|
||||||
import { getTranslation } from "@server/lib/i18n";
|
import { getTranslation } from "@server/lib/i18n";
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type FormValues = {
|
export type FormValues = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -63,7 +65,7 @@ export type FormValues = {
|
|||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
}[];
|
}[];
|
||||||
customInputs: EventTypeCustomInput[];
|
customInputs: CustomInputParsed[];
|
||||||
users: string[];
|
users: string[];
|
||||||
schedule: number;
|
schedule: number;
|
||||||
periodType: PeriodType;
|
periodType: PeriodType;
|
||||||
@@ -87,6 +89,8 @@ export type FormValues = {
|
|||||||
bookingLimits?: BookingLimit;
|
bookingLimits?: BookingLimit;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
tabName: z
|
tabName: z
|
||||||
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
|
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
|
||||||
@@ -150,6 +154,21 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadata = eventType.metadata;
|
||||||
|
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
|
||||||
|
if (!!team) {
|
||||||
|
metadata.config = {
|
||||||
|
...metadata.config,
|
||||||
|
useHostSchedulesForTeamEvent:
|
||||||
|
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
|
||||||
|
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
|
||||||
|
: !!eventType.schedule,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Make sure non-team events NEVER have this config key;
|
||||||
|
delete metadata.config?.useHostSchedulesForTeamEvent;
|
||||||
|
}
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: eventType.title,
|
title: eventType.title,
|
||||||
@@ -172,17 +191,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
findDurationType(eventType.minimumBookingNotice),
|
findDurationType(eventType.minimumBookingNotice),
|
||||||
eventType.minimumBookingNotice
|
eventType.minimumBookingNotice
|
||||||
),
|
),
|
||||||
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
|
metadata,
|
||||||
metadata: {
|
|
||||||
...eventType.metadata,
|
|
||||||
config: {
|
|
||||||
...eventType.metadata.config,
|
|
||||||
useHostSchedulesForTeamEvent:
|
|
||||||
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
|
|
||||||
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
|
|
||||||
: !!eventType.schedule,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -256,6 +265,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
recurringEvent,
|
recurringEvent,
|
||||||
locations,
|
locations,
|
||||||
metadata,
|
metadata,
|
||||||
|
customInputs,
|
||||||
// We don't need to send send these values to the backend
|
// We don't need to send send these values to the backend
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
seatsPerTimeSlotEnabled,
|
seatsPerTimeSlotEnabled,
|
||||||
@@ -266,7 +276,17 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
|
|
||||||
if (bookingLimits) {
|
if (bookingLimits) {
|
||||||
const isValid = validateBookingLimitOrder(bookingLimits);
|
const isValid = validateBookingLimitOrder(bookingLimits);
|
||||||
if (!isValid) throw new Error("Booking limits must be in accending order. [day,week,month,year]");
|
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.multipleDuration !== undefined) {
|
||||||
|
if (metadata?.multipleDuration.length < 1) {
|
||||||
|
throw new Error(t("event_setup_multiple_duration_error"));
|
||||||
|
} else {
|
||||||
|
if (input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
||||||
|
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
@@ -283,6 +303,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
seatsShowAttendees,
|
seatsShowAttendees,
|
||||||
metadata,
|
metadata,
|
||||||
|
customInputs,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div ref={animationParentRef} className="space-y-6">
|
<div ref={animationParentRef} className="space-y-6">
|
||||||
@@ -297,6 +318,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const { req, query } = context;
|
const { req, query } = context;
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
if (Number.isNaN(typeParam)) {
|
if (Number.isNaN(typeParam)) {
|
||||||
return {
|
return {
|
||||||
@@ -319,7 +341,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
id: true,
|
id: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
email: true,
|
email: true,
|
||||||
plan: true,
|
|
||||||
locale: true,
|
locale: true,
|
||||||
defaultScheduleId: true,
|
defaultScheduleId: true,
|
||||||
});
|
});
|
||||||
@@ -446,6 +467,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const credentials = await prisma.credential.findMany({
|
const credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
|
app: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -453,6 +477,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
appId: true,
|
appId: true,
|
||||||
|
invalid: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -480,6 +505,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
|
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
|
||||||
const parsedMetaData = newMetadata;
|
const parsedMetaData = newMetadata;
|
||||||
|
|
||||||
|
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...restEventType,
|
...restEventType,
|
||||||
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
||||||
@@ -487,6 +514,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
||||||
locations: locations as unknown as LocationObject[],
|
locations: locations as unknown as LocationObject[],
|
||||||
metadata: parsedMetaData,
|
metadata: parsedMetaData,
|
||||||
|
customInputs: parsedCustomInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
// backwards compat
|
// backwards compat
|
||||||
@@ -502,7 +530,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
}
|
}
|
||||||
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
||||||
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
||||||
const integrations = getApps(credentials);
|
const integrations = await getEnabledApps(credentials);
|
||||||
const locationOptions = getLocationOptions(integrations, t);
|
const locationOptions = getLocationOptions(integrations, t);
|
||||||
|
|
||||||
const eventTypeObject = Object.assign({}, eventType, {
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
@@ -531,6 +559,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
team: eventTypeObject.team || null,
|
team: eventTypeObject.team || null,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
currentUserMembership,
|
currentUserMembership,
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import Head from "next/head";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, useEffect, useState } from "react";
|
import React, { Fragment, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
|
||||||
|
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ConfirmationDialogContent,
|
ConfirmationDialogContent,
|
||||||
CreateEventTypeButton,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
@@ -38,6 +38,8 @@ import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
|||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||||
|
|
||||||
@@ -440,7 +442,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
navigator
|
navigator
|
||||||
.share({
|
.share({
|
||||||
title: t("share"),
|
title: t("share"),
|
||||||
text: t("share_event"),
|
text: t("share_event", { appName: APP_NAME }),
|
||||||
url: calLink,
|
url: calLink,
|
||||||
})
|
})
|
||||||
.then(() => showToast(t("link_shared"), "success"))
|
.then(() => showToast(t("link_shared"), "success"))
|
||||||
@@ -531,9 +533,9 @@ const EventTypeListHeading = ({
|
|||||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
||||||
<a className="font-bold">{profile?.name || ""}</a>
|
<a className="font-bold">{profile?.name || ""}</a>
|
||||||
</Link>
|
</Link>
|
||||||
{membershipCount && (
|
{membershipCount && teamId && (
|
||||||
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
|
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
|
||||||
<Link href="/settings/teams">
|
<Link href={`/settings/teams/${teamId}/members`}>
|
||||||
<a>
|
<a>
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
||||||
@@ -579,12 +581,9 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
|
|||||||
|
|
||||||
const EventTypesPage = () => {
|
const EventTypesPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Head>
|
|
||||||
<title>Home | Cal.com</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<Shell
|
<Shell
|
||||||
heading={t("event_types_page_title") as string}
|
heading={t("event_types_page_title") as string}
|
||||||
subtitle={t("event_types_page_subtitle") as string}
|
subtitle={t("event_types_page_subtitle") as string}
|
||||||
@@ -622,4 +621,14 @@ const EventTypesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default EventTypesPage;
|
export default EventTypesPage;
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ import { useRouter } from "next/router";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getSession } from "@calcom/lib/auth";
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
import { User } from "@calcom/prisma/client";
|
||||||
import { Button, StepCard, Steps } from "@calcom/ui";
|
import { Button, StepCard, Steps } from "@calcom/ui";
|
||||||
|
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||||
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
||||||
|
|
||||||
interface IOnboardingPageProps {
|
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INITIAL_STEP = "user-settings";
|
const INITIAL_STEP = "user-settings";
|
||||||
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
|
||||||
@@ -46,7 +46,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{
|
{
|
||||||
title: `${t("welcome_to_cal_header")}`,
|
title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
|
||||||
subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`],
|
subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,7 +86,9 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
|||||||
data-testid="onboarding"
|
data-testid="onboarding"
|
||||||
key={router.asPath}>
|
key={router.asPath}>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Cal.com - {t("getting_started")}</title>
|
<title>
|
||||||
|
{APP_NAME} - {t("getting_started")}
|
||||||
|
</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
@@ -162,7 +164,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
weekStart: true,
|
weekStart: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
if (!booking) {
|
if (!booking) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +51,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
|
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +66,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
||||||
"/" +
|
"/" +
|
||||||
eventType?.slug;
|
eventType?.slug;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
|
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[uid]";
|
||||||
|
|
||||||
|
export { default } from "../[uid]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
|
||||||
|
|
||||||
function AdminAppsView() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Meta title="Apps" description="apps_description" />
|
|
||||||
<h1>App listing</h1>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
|
||||||
|
|
||||||
export default AdminAppsView;
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
function AdminAppsView() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Meta title={t("apps")} description={t("apps_description")} />
|
||||||
|
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminAppsView;
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
|
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminView() {
|
function AdminView() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Admin" description="Impersonation" />
|
<Meta title={t("admin")} description={t("impersonation")} />
|
||||||
<form
|
<form
|
||||||
className="mb-6 w-full sm:w-1/2"
|
className="mb-6 w-full sm:w-1/2"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -21,7 +24,7 @@ function AdminView() {
|
|||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<TextField
|
<TextField
|
||||||
containerClassName="w-full"
|
containerClassName="w-full"
|
||||||
name="Impersonate User"
|
name={t("user_impersonation_heading")}
|
||||||
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
|
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
hint={t("impersonate_user_tip")}
|
hint={t("impersonate_user_tip")}
|
||||||
@@ -36,4 +39,14 @@ function AdminView() {
|
|||||||
|
|
||||||
AdminView.getLayout = getLayout;
|
AdminView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminView;
|
export default AdminView;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminAppsView() {
|
function AdminAppsView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -11,4 +15,14 @@ function AdminAppsView() {
|
|||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminAppsView;
|
export default AdminAppsView;
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function AdminUsersView() {
|
function AdminUsersView() {
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Users" description="users_description" />
|
<Meta title={t("users")} description={t("users_description")} />
|
||||||
<h1>Users listing</h1>
|
<h1>{t("users_listing")}</h1>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminUsersView.getLayout = getLayout;
|
AdminUsersView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AdminUsersView;
|
export default AdminUsersView;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
import { HelpScout, useChat } from "react-live-chat-loader";
|
||||||
@@ -8,6 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
|
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
interface CtaRowProps {
|
interface CtaRowProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -33,7 +36,6 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
|||||||
const BillingView = () => {
|
const BillingView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const isPro = user?.plan === "PRO";
|
|
||||||
const [, loadChat] = useChat();
|
const [, loadChat] = useChat();
|
||||||
const [showChat, setShowChat] = useState(false);
|
const [showChat, setShowChat] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -50,21 +52,16 @@ const BillingView = () => {
|
|||||||
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
||||||
<div className="space-y-6 text-sm sm:space-y-8">
|
<div className="space-y-6 text-sm sm:space-y-8">
|
||||||
<CtaRow
|
<CtaRow
|
||||||
className={classNames(!isPro && "pointer-events-none opacity-30")}
|
|
||||||
title={t("billing_manage_details_title")}
|
title={t("billing_manage_details_title")}
|
||||||
description={t("billing_manage_details_description")}>
|
description={t("billing_manage_details_description")}>
|
||||||
<Button
|
<Button color="primary" href={billingHref} target="_blank" EndIcon={Icon.FiExternalLink}>
|
||||||
color={isPro ? "primary" : "secondary"}
|
|
||||||
href={billingHref}
|
|
||||||
target="_blank"
|
|
||||||
EndIcon={Icon.FiExternalLink}>
|
|
||||||
{t("billing_portal")}
|
{t("billing_portal")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
|
|
||||||
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
||||||
<Button color="secondary" onClick={onContactSupportClick}>
|
<Button color="secondary" onClick={onContactSupportClick}>
|
||||||
{t("billing_help_cta")}
|
{t("contact_support")}
|
||||||
</Button>
|
</Button>
|
||||||
</CtaRow>
|
</CtaRow>
|
||||||
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
||||||
@@ -75,4 +72,14 @@ const BillingView = () => {
|
|||||||
|
|
||||||
BillingView.getLayout = getLayout;
|
BillingView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default BillingView;
|
export default BillingView;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
|
||||||
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
|
||||||
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
|
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
|
||||||
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
|
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +19,8 @@ import {
|
|||||||
SkeletonLoader,
|
SkeletonLoader,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const ApiKeysView = () => {
|
const ApiKeysView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -43,7 +47,10 @@ const ApiKeysView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="API Keys" description="API keys allow other apps to communicate with Cal.com" />
|
<Meta
|
||||||
|
title={t("api_keys")}
|
||||||
|
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||||
|
/>
|
||||||
|
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<>
|
<>
|
||||||
@@ -70,7 +77,7 @@ const ApiKeysView = () => {
|
|||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Icon.FiLink}
|
Icon={Icon.FiLink}
|
||||||
headline={t("create_first_api_key")}
|
headline={t("create_first_api_key")}
|
||||||
description={t("create_first_api_key_description")}
|
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||||
buttonRaw={<NewApiKeyButton />}
|
buttonRaw={<NewApiKeyButton />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -79,7 +86,7 @@ const ApiKeysView = () => {
|
|||||||
</LicenseRequired>
|
</LicenseRequired>
|
||||||
|
|
||||||
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
|
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
|
||||||
<DialogContent type="creation" useOwnActionButtons>
|
<DialogContent type="creation">
|
||||||
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
|
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -89,4 +96,14 @@ const ApiKeysView = () => {
|
|||||||
|
|
||||||
ApiKeysView.getLayout = getLayout;
|
ApiKeysView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ApiKeysView;
|
export default ApiKeysView;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
@@ -16,9 +19,12 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||||
@@ -40,7 +46,7 @@ const SkeletonLoader = () => {
|
|||||||
|
|
||||||
const AppearanceView = () => {
|
const AppearanceView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const session = useSession();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ const AppearanceView = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
@@ -84,7 +90,7 @@ const AppearanceView = () => {
|
|||||||
theme: values.theme || null,
|
theme: values.theme || null,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<Meta title="Appearance" description="Manage settings for your booking appearance" />
|
<Meta title={t("appearance")} description={t("appearance_description")} />
|
||||||
<div className="mb-6 flex items-center text-sm">
|
<div className="mb-6 flex items-center text-sm">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">{t("theme")}</p>
|
<p className="font-semibold">{t("theme")}</p>
|
||||||
@@ -171,14 +177,15 @@ const AppearanceView = () => {
|
|||||||
<div className="flex w-full text-sm">
|
<div className="flex w-full text-sm">
|
||||||
<div className="mr-1 flex-grow">
|
<div className="mr-1 flex-grow">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="mr-2 font-semibold">{t("disable_cal_branding")}</p>
|
<p className="mr-2 font-semibold">{t("disable_cal_branding", { appName: APP_NAME })}</p>
|
||||||
<Badge variant="gray">{t("pro")}</Badge>
|
<Badge variant="gray">{t("pro")}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding")}</p>
|
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none">
|
<div className="flex-none">
|
||||||
<Switch
|
<Switch
|
||||||
id="hideBranding"
|
id="hideBranding"
|
||||||
|
disabled={!session.data?.user.belongsToActiveTeam}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
|
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
|
||||||
}
|
}
|
||||||
@@ -203,6 +210,16 @@ const AppearanceView = () => {
|
|||||||
|
|
||||||
AppearanceView.getLayout = getLayout;
|
AppearanceView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default AppearanceView;
|
export default AppearanceView;
|
||||||
interface ThemeLabelProps {
|
interface ThemeLabelProps {
|
||||||
variant: "light" | "dark" | "system";
|
variant: "light" | "dark" | "system";
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -10,7 +12,6 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
DisconnectIntegration,
|
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
getSettingsLayout as getLayout,
|
getSettingsLayout as getLayout,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -22,12 +23,15 @@ import {
|
|||||||
SkeletonButton,
|
SkeletonButton,
|
||||||
SkeletonContainer,
|
SkeletonContainer,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -66,15 +70,17 @@ const CalendarsView = () => {
|
|||||||
async onSettled() {
|
async onSettled() {
|
||||||
await utils.viewer.connectedCalendars.invalidate();
|
await utils.viewer.connectedCalendars.invalidate();
|
||||||
},
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
showToast(t("calendar_updated_successfully"), "success");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showToast(t("unexpected_error_try_again"), "error");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta
|
<Meta title={t("calendars")} description={t("calendars_description")} CTA={<AddCalendarButton />} />
|
||||||
title="Calendars"
|
|
||||||
description="Configure how your event types interact with your calendars"
|
|
||||||
CTA={<AddCalendarButton />}
|
|
||||||
/>
|
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader />}
|
||||||
@@ -110,7 +116,7 @@ const CalendarsView = () => {
|
|||||||
{t("check_for_conflicts")}
|
{t("check_for_conflicts")}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
|
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
|
||||||
<List>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
<Fragment key={item.credentialId}>
|
<Fragment key={item.credentialId}>
|
||||||
{item.error && item.error.message && (
|
{item.error && item.error.message && (
|
||||||
@@ -137,8 +143,8 @@ const CalendarsView = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{item?.error === undefined && item.calendars && (
|
{item?.error === undefined && item.calendars && (
|
||||||
<ListItem expanded className="flex-col">
|
<ListItem className="flex-col rounded-md">
|
||||||
<div className="flex w-full flex-1 items-center space-x-3 pb-5 pl-1 pt-1 rtl:space-x-reverse">
|
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
item.integration.logo && (
|
item.integration.logo && (
|
||||||
@@ -164,7 +170,7 @@ const CalendarsView = () => {
|
|||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
trashIcon
|
trashIcon
|
||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
buttonProps={{ size: "icon", color: "secondary" }}
|
buttonProps={{ className: "border border-gray-300" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +178,7 @@ const CalendarsView = () => {
|
|||||||
<p className="px-2 pt-4 text-sm text-neutral-500">
|
<p className="px-2 pt-4 text-sm text-neutral-500">
|
||||||
{t("toggle_calendars_conflict")}
|
{t("toggle_calendars_conflict")}
|
||||||
</p>
|
</p>
|
||||||
<ul className="space-y-2 px-2 pt-4">
|
<ul className="space-y-2 p-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
key={cal.externalId}
|
key={cal.externalId}
|
||||||
@@ -224,4 +230,14 @@ const CalendarsView = () => {
|
|||||||
|
|
||||||
CalendarsView.getLayout = getLayout;
|
CalendarsView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default CalendarsView;
|
export default CalendarsView;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -5,7 +6,9 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -22,9 +25,12 @@ import {
|
|||||||
SkeletonText,
|
SkeletonText,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
@@ -58,18 +64,19 @@ const ConferencingLayout = () => {
|
|||||||
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
const [deleteAppModal, setDeleteAppModal] = useState(false);
|
||||||
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
|
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
|
||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading)
|
||||||
|
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
||||||
<Meta title="Conferencing" description="Add your favourite video conferencing apps for your meetings" />
|
<Meta title={t("conferencing")} description={t("conferencing_description")} />
|
||||||
<List>
|
<List>
|
||||||
{apps?.items &&
|
{apps?.items &&
|
||||||
apps.items
|
apps.items
|
||||||
.map((app) => ({ ...app, title: app.title || app.name }))
|
.map((app) => ({ ...app, title: app.title || app.name }))
|
||||||
.map((app) => (
|
.map((app) => (
|
||||||
<ListItem className="flex-col border-0" key={app.title}>
|
<ListItem className="flex-col border-0" key={app.title}>
|
||||||
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse">
|
<div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse">
|
||||||
{
|
{
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
|
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
|
||||||
@@ -111,10 +118,14 @@ const ConferencingLayout = () => {
|
|||||||
title={t("Remove app")}
|
title={t("Remove app")}
|
||||||
description={t("are_you_sure_you_want_to_remove_this_app")}
|
description={t("are_you_sure_you_want_to_remove_this_app")}
|
||||||
type="confirmation"
|
type="confirmation"
|
||||||
actionText={t("yes_remove_app")}
|
Icon={Icon.FiAlertCircle}>
|
||||||
Icon={Icon.FiAlertCircle}
|
<DialogFooter>
|
||||||
actionOnClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}
|
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
|
||||||
/>
|
{t("yes_remove_app")}
|
||||||
|
</Button>
|
||||||
|
<DialogClose />
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -122,4 +133,14 @@ const ConferencingLayout = () => {
|
|||||||
|
|
||||||
ConferencingLayout.getLayout = getLayout;
|
ConferencingLayout.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ConferencingLayout;
|
export default ConferencingLayout;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
@@ -21,9 +22,12 @@ import {
|
|||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
import { nameOfDay } from "@lib/core/i18n/weekday";
|
import { nameOfDay } from "@lib/core/i18n/weekday";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
<SkeletonText className="h-8 w-full" />
|
<SkeletonText className="h-8 w-full" />
|
||||||
@@ -47,14 +51,14 @@ const GeneralQueryView = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("something_went_wrong"));
|
throw new Error(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<WithQuery
|
<WithQuery
|
||||||
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -127,7 +131,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
weekStart: values.weekStart.value,
|
weekStart: values.weekStart.value,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<Meta title="General" description="Manage settings for your language and timezone" />
|
<Meta title={t("general")} description={t("general_description")} />
|
||||||
<Controller
|
<Controller
|
||||||
name="locale"
|
name="locale"
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
@@ -210,4 +214,14 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
|
|
||||||
GeneralQueryView.getLayout = getLayout;
|
GeneralQueryView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default GeneralQueryView;
|
export default GeneralQueryView;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
|
import { BaseSyntheticEvent, useRef, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -14,7 +16,9 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
Form,
|
Form,
|
||||||
getSettingsLayout as getLayout,
|
getSettingsLayout as getLayout,
|
||||||
@@ -32,11 +36,14 @@ import {
|
|||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<SkeletonAvatar className=" h-12 w-12 px-4" />
|
<SkeletonAvatar className=" h-12 w-12 px-4" />
|
||||||
@@ -56,14 +63,25 @@ interface DeleteAccountValues {
|
|||||||
totpCode: string;
|
totpCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
username: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
bio: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ProfileView = () => {
|
const ProfileView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||||
|
const { data: avatar, isLoading: isLoadingAvatar } = trpc.viewer.avatar.useQuery();
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
|
utils.viewer.me.invalidate();
|
||||||
|
utils.viewer.avatar.invalidate();
|
||||||
|
setTempFormValues(null);
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showToast(t("error_updating_settings"), "error");
|
showToast(t("error_updating_settings"), "error");
|
||||||
@@ -71,18 +89,14 @@ const ProfileView = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
||||||
|
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
|
||||||
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
||||||
|
|
||||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||||
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
||||||
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
||||||
|
|
||||||
const form = useForm<DeleteAccountValues>();
|
const form = useForm<DeleteAccountValues>();
|
||||||
|
|
||||||
const emailMd5 = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(user?.email || "example@example.com")
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const onDeleteMeSuccessMutation = async () => {
|
const onDeleteMeSuccessMutation = async () => {
|
||||||
await utils.viewer.me.invalidate();
|
await utils.viewer.me.invalidate();
|
||||||
showToast(t("Your account was deleted"), "success");
|
showToast(t("Your account was deleted"), "success");
|
||||||
@@ -97,7 +111,7 @@ const ProfileView = () => {
|
|||||||
|
|
||||||
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
|
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
mutation.mutate(formMethods.getValues());
|
if (tempFormValues) mutation.mutate(tempFormValues);
|
||||||
setConfirmPasswordOpen(false);
|
setConfirmPasswordOpen(false);
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
@@ -143,6 +157,7 @@ const ProfileView = () => {
|
|||||||
deleteMeWithoutPasswordMutation.mutate();
|
deleteMeWithoutPasswordMutation.mutate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
|
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
if (isCALIdentityProviver) {
|
if (isCALIdentityProviver) {
|
||||||
@@ -153,38 +168,6 @@ const ProfileView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formMethods = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
avatar: "",
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
bio: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting, isDirty },
|
|
||||||
} = formMethods;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
reset(
|
|
||||||
{
|
|
||||||
avatar: user?.avatar || "",
|
|
||||||
username: user?.username || "",
|
|
||||||
name: user?.name || "",
|
|
||||||
email: user?.email || "",
|
|
||||||
bio: user?.bio || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keepDirtyValues: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [reset, user]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const passwordRef = useRef<HTMLInputElement>(null!);
|
const passwordRef = useRef<HTMLInputElement>(null!);
|
||||||
|
|
||||||
@@ -196,140 +179,100 @@ const ProfileView = () => {
|
|||||||
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
||||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||||
};
|
};
|
||||||
const onSuccessfulUsernameUpdate = async () => {
|
|
||||||
showToast(t("settings_updated_successfully"), "success");
|
|
||||||
await utils.viewer.me.invalidate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onErrorInUsernameUpdate = () => {
|
if (isLoading || !user || isLoadingAvatar || !avatar)
|
||||||
showToast(t("error_updating_settings"), "error");
|
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading || !user) return <SkeletonLoader />;
|
const defaultValues = {
|
||||||
const isDisabled = isSubmitting || !isDirty;
|
username: user.username || "",
|
||||||
|
avatar: avatar.avatar || "",
|
||||||
|
name: user.name || "",
|
||||||
|
email: user.email || "",
|
||||||
|
bio: user.bio || "",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form
|
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||||
form={formMethods}
|
<ProfileForm
|
||||||
handleSubmit={(values) => {
|
key={JSON.stringify(defaultValues)}
|
||||||
if (values.email !== user?.email && isCALIdentityProviver) {
|
defaultValues={defaultValues}
|
||||||
|
onSubmit={(values) => {
|
||||||
|
if (values.email !== user.email && isCALIdentityProviver) {
|
||||||
|
setTempFormValues(values);
|
||||||
setConfirmPasswordOpen(true);
|
setConfirmPasswordOpen(true);
|
||||||
} else {
|
} else {
|
||||||
mutation.mutate(values);
|
mutation.mutate(values);
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
<Meta title="Profile" description="Manage settings for your cal profile" />
|
extraField={
|
||||||
<div className="flex items-center">
|
<div className="mt-8">
|
||||||
<Controller
|
<UsernameAvailabilityField
|
||||||
control={formMethods.control}
|
user={user}
|
||||||
name="avatar"
|
onSuccessMutation={async () => {
|
||||||
render={({ field: { value } }) => (
|
showToast(t("settings_updated_successfully"), "success");
|
||||||
<>
|
await utils.viewer.me.invalidate();
|
||||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
}}
|
||||||
<div className="ml-4">
|
onErrorMutation={() => {
|
||||||
<ImageUploader
|
showToast(t("error_updating_settings"), "error");
|
||||||
target="avatar"
|
}}
|
||||||
id="avatar-upload"
|
/>
|
||||||
buttonMsg={t("change_avatar")}
|
</div>
|
||||||
handleAvatarChange={(newAvatar) => {
|
}
|
||||||
formMethods.setValue("avatar", newAvatar);
|
/>
|
||||||
}}
|
|
||||||
imageSrc={value}
|
<hr className="my-6 border-neutral-200" />
|
||||||
/>
|
|
||||||
</div>
|
<Label>{t("danger_zone")}</Label>
|
||||||
</>
|
{/* Delete account Dialog */}
|
||||||
|
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
data-testid="delete-account"
|
||||||
|
color="destructive"
|
||||||
|
className="mt-1 border-2"
|
||||||
|
StartIcon={Icon.FiTrash2}>
|
||||||
|
{t("delete_account")}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
title={t("delete_account_modal_title")}
|
||||||
|
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||||
|
type="creation"
|
||||||
|
Icon={Icon.FiAlertTriangle}>
|
||||||
|
<>
|
||||||
|
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
|
||||||
|
{isCALIdentityProviver && (
|
||||||
|
<PasswordField
|
||||||
|
data-testid="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
label="Password"
|
||||||
|
ref={passwordRef}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<Controller
|
|
||||||
control={formMethods.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field: { ref, onChange, value } }) => {
|
|
||||||
return (
|
|
||||||
<UsernameAvailability
|
|
||||||
currentUsername={user?.username || ""}
|
|
||||||
inputUsernameValue={value}
|
|
||||||
usernameRef={ref}
|
|
||||||
setInputUsernameValue={onChange}
|
|
||||||
onSuccessMutation={onSuccessfulUsernameUpdate}
|
|
||||||
onErrorMutation={onErrorInUsernameUpdate}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
||||||
disabled={isDisabled}
|
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
||||||
color="primary"
|
<TwoFactor center={false} />
|
||||||
className="mt-8"
|
</Form>
|
||||||
type="submit"
|
)}
|
||||||
loading={mutation.isLoading}>
|
|
||||||
{t("update")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<hr className="my-6 border-neutral-200" />
|
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
||||||
|
<DialogFooter>
|
||||||
<Label>{t("danger_zone")}</Label>
|
<Button
|
||||||
{/* Delete account Dialog */}
|
color="primary"
|
||||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
data-testid="delete-account-confirm"
|
||||||
<DialogTrigger asChild>
|
onClick={(e) => onConfirmButton(e)}>
|
||||||
<Button
|
{t("delete_my_account")}
|
||||||
data-testid="delete-account"
|
</Button>
|
||||||
color="destructive"
|
<DialogClose />
|
||||||
className="mt-1 border-2"
|
</DialogFooter>
|
||||||
StartIcon={Icon.FiTrash2}>
|
</>
|
||||||
{t("delete_account")}
|
</DialogContent>
|
||||||
</Button>
|
</Dialog>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
title={t("delete_account_modal_title")}
|
|
||||||
description={t("confirm_delete_account_modal")}
|
|
||||||
type="creation"
|
|
||||||
actionText={t("delete_my_account")}
|
|
||||||
actionProps={{
|
|
||||||
// @ts-expect-error data attributes aren't typed
|
|
||||||
"data-testid": "delete-account-confirm",
|
|
||||||
}}
|
|
||||||
Icon={Icon.FiAlertTriangle}
|
|
||||||
actionOnClick={(e) => e && onConfirmButton(e)}>
|
|
||||||
<>
|
|
||||||
<p className="mb-7">{t("delete_account_confirmation_message")}</p>
|
|
||||||
{isCALIdentityProviver && (
|
|
||||||
<PasswordField
|
|
||||||
data-testid="password"
|
|
||||||
name="password"
|
|
||||||
id="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
label="Password"
|
|
||||||
ref={passwordRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
|
||||||
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
|
||||||
<TwoFactor center={false} />
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
|
||||||
</>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* If changing email, confirm password */}
|
{/* If changing email, confirm password */}
|
||||||
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
||||||
@@ -337,9 +280,7 @@ const ProfileView = () => {
|
|||||||
title={t("confirm_password")}
|
title={t("confirm_password")}
|
||||||
description={t("confirm_password_change_email")}
|
description={t("confirm_password_change_email")}
|
||||||
type="creation"
|
type="creation"
|
||||||
actionText={t("confirm")}
|
Icon={Icon.FiAlertTriangle}>
|
||||||
Icon={Icon.FiAlertTriangle}
|
|
||||||
actionOnClick={(e) => e && onConfirmPassword(e)}>
|
|
||||||
<>
|
<>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
data-testid="password"
|
data-testid="password"
|
||||||
@@ -352,6 +293,12 @@ const ProfileView = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
|
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
|
||||||
|
{t("confirm")}
|
||||||
|
</Button>
|
||||||
|
<DialogClose />
|
||||||
|
</DialogFooter>
|
||||||
</>
|
</>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -359,6 +306,83 @@ const ProfileView = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ProfileForm = ({
|
||||||
|
defaultValues,
|
||||||
|
onSubmit,
|
||||||
|
extraField,
|
||||||
|
}: {
|
||||||
|
defaultValues: FormValues;
|
||||||
|
onSubmit: (values: FormValues) => void;
|
||||||
|
extraField?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const emailMd5 = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(defaultValues.email || "example@example.com")
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const formMethods = useForm<FormValues>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isSubmitting, isDirty },
|
||||||
|
} = formMethods;
|
||||||
|
|
||||||
|
const isDisabled = isSubmitting || !isDirty;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Controller
|
||||||
|
control={formMethods.control}
|
||||||
|
name="avatar"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<>
|
||||||
|
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<ImageUploader
|
||||||
|
target="avatar"
|
||||||
|
id="avatar-upload"
|
||||||
|
buttonMsg={t("change_avatar")}
|
||||||
|
handleAvatarChange={(newAvatar) => {
|
||||||
|
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
imageSrc={value || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{extraField}
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-8">
|
||||||
|
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
||||||
|
{t("update")}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ProfileView.getLayout = getLayout;
|
ProfileView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileView;
|
export default ProfileView;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -13,6 +14,8 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const ProfileImpersonationView = () => {
|
const ProfileImpersonationView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
@@ -39,7 +42,7 @@ const ProfileImpersonationView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Impersonation Settings" description="" />
|
<Meta title={t("impersonation")} description={t("impersonation_description")} />
|
||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={({ disableImpersonation }) => {
|
handleSubmit={({ disableImpersonation }) => {
|
||||||
@@ -74,4 +77,14 @@ const ProfileImpersonationView = () => {
|
|||||||
|
|
||||||
ProfileImpersonationView.getLayout = getLayout;
|
ProfileImpersonationView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default ProfileImpersonationView;
|
export default ProfileImpersonationView;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
|
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
type ChangePasswordFormValues = {
|
type ChangePasswordFormValues = {
|
||||||
oldPassword: string;
|
oldPassword: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
@@ -92,4 +95,14 @@ const PasswordView = () => {
|
|||||||
|
|
||||||
PasswordView.getLayout = getLayout;
|
PasswordView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default PasswordView;
|
export default PasswordView;
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,37 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, getSettingsLayout as getLayout, Loader, Meta, Switch } from "@calcom/ui";
|
import {
|
||||||
|
Badge,
|
||||||
|
getSettingsLayout as getLayout,
|
||||||
|
Meta,
|
||||||
|
Switch,
|
||||||
|
SkeletonButton,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||||
|
return (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<Meta title={title} description={description} />
|
||||||
|
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TwoFactorAuthView = () => {
|
const TwoFactorAuthView = () => {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
@@ -16,11 +41,11 @@ const TwoFactorAuthView = () => {
|
|||||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||||
|
|
||||||
if (isLoading) return <Loader />;
|
if (isLoading) return <SkeletonLoader title={t("2fa")} description={t("2fa_description")} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Two-Factor Authentication" description="Manage settings for your account passwords" />
|
<Meta title={t("2fa")} description={t("2fa_description")} />
|
||||||
<div className="mt-6 flex items-start space-x-4">
|
<div className="mt-6 flex items-start space-x-4">
|
||||||
<Switch
|
<Switch
|
||||||
checked={user?.twoFactorEnabled}
|
checked={user?.twoFactorEnabled}
|
||||||
@@ -35,7 +60,7 @@ const TwoFactorAuthView = () => {
|
|||||||
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-600">Add an extra layer of security to your account.</p>
|
<p className="text-sm text-gray-600">{t("add_an_extra_layer_of_security")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,4 +93,14 @@ const TwoFactorAuthView = () => {
|
|||||||
|
|
||||||
TwoFactorAuthView.getLayout = getLayout;
|
TwoFactorAuthView.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default TwoFactorAuthView;
|
export default TwoFactorAuthView;
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getWizardLayout as getLayout } from "@calcom/ui";
|
import { getWizardLayout as getLayout } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const CreateNewTeamPage = () => {
|
const CreateNewTeamPage = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create a new Team</title>
|
<title>{t("create_new_team")}</title>
|
||||||
<meta name="description" content="Create a new team to ease your organisational booking" />
|
<meta name="description" content={t("create_new_team_description")} />
|
||||||
</Head>
|
</Head>
|
||||||
<CreateANewTeamForm />
|
<CreateANewTeamForm />
|
||||||
</>
|
</>
|
||||||
@@ -17,4 +22,14 @@ const CreateNewTeamPage = () => {
|
|||||||
|
|
||||||
CreateNewTeamPage.getLayout = getLayout;
|
CreateNewTeamPage.getLayout = getLayout;
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default CreateNewTeamPage;
|
export default CreateNewTeamPage;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user