Merge remote-tracking branch 'origin/main' into feature/app-store-cli
This commit is contained in:
commit
bcfd22614b
|
@ -15,6 +15,8 @@
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||||
|
# To enable enterprise-only features, fill your license key in here
|
||||||
|
CALCOM_LICENSE_KEY=
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
|
@ -25,6 +27,7 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
|
||||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||||
|
NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
||||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||||
|
|
||||||
# To enable SAML login, set both these variables
|
# To enable SAML login, set both these variables
|
||||||
|
|
|
@ -16,7 +16,8 @@ jobs:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Use Node ${{ matrix.node }}
|
- name: Use Node ${{ matrix.node }}
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
name: E2E test - embed
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ tests/ci-embed ]
|
||||||
|
pull_request_target: # So we can test on forks
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
||||||
|
paths-ignore:
|
||||||
|
- apps/api/**
|
||||||
|
- apps/console/**
|
||||||
|
- apps/docs/**
|
||||||
|
- apps/swagger/**
|
||||||
|
- apps/website/**
|
||||||
|
- apps/web/public/**
|
||||||
|
- tests/**
|
||||||
|
- playwright/**
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 20
|
||||||
|
name: Testing Embeds
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node: ["14.x"]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||||
|
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET: secret
|
||||||
|
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||||
|
GOOGLE_LOGIN_ENABLED: true
|
||||||
|
# CRON_API_KEY: xxx
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||||
|
PAYMENT_FEE_FIXED: 10
|
||||||
|
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
SAML_ADMINS: pro@example.com
|
||||||
|
NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||||
|
NEXT_PUBLIC_IS_E2E: 1
|
||||||
|
# EMAIL_FROM: e2e@cal.com
|
||||||
|
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||||
|
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||||
|
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||||
|
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||||
|
# MS_GRAPH_CLIENT_ID: xxx
|
||||||
|
# MS_GRAPH_CLIENT_SECRET: xxx
|
||||||
|
# ZOOM_CLIENT_ID: xxx
|
||||||
|
# ZOOM_CLIENT_SECRET: xxx
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:12.1
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: calendso
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Use Node ${{ matrix.node }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: playwright-cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/Library/Caches/ms-playwright
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
${{ github.workspace }}/node_modules/playwright
|
||||||
|
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: cache-playwright-
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- name: Install playwright deps
|
||||||
|
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- run: yarn embed-tests-prepare
|
||||||
|
- run: yarn workspace @calcom/embed-core embed-tests-update-snapshots:ci
|
||||||
|
- run: yarn workspace @calcom/embed-react embed-tests-update-snapshots:ci
|
||||||
|
|
||||||
|
- name: Upload embed-core results
|
||||||
|
if: ${{ always() }}
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: test-results-core
|
||||||
|
path: packages/embeds/embed-core/playwright/results
|
||||||
|
|
||||||
|
- name: Upload embed-react results
|
||||||
|
if: ${{ always() }}
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: test-results-react
|
||||||
|
path: packages/embeds/embed-react/playwright/results
|
|
@ -4,7 +4,7 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- public/static/locales/**
|
- apps/web/public/static/locales/**
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
"Website(3001)",
|
"Website(3001)",
|
||||||
"Embed Core(3100)",
|
"Embed Core(3100)",
|
||||||
"Embed React(3101)",
|
"Embed React(3101)",
|
||||||
"Prisma Studio(5555)"
|
"Prisma Studio(5555)",
|
||||||
|
"Maildev(587)",
|
||||||
|
"AppStoreCli:Watch"
|
||||||
],
|
],
|
||||||
// Mark as the default build task so cmd/ctrl+shift+b will create them
|
// Mark as the default build task so cmd/ctrl+shift+b will create them
|
||||||
"group": {
|
"group": {
|
||||||
|
@ -65,6 +67,20 @@
|
||||||
"command": "yarn db-studio",
|
"command": "yarn db-studio",
|
||||||
"isBackground": false,
|
"isBackground": false,
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maildev(587)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "maildev -s 587",
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "AppStoreCli:Watch",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "cd packages/app-store-cli && yarn build:watch",
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,26 @@ Contributions are what make the open source community such an amazing place to b
|
||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||||
|
|
||||||
|
## Areas of expertise
|
||||||
|
|
||||||
|
### Legend
|
||||||
|
|
||||||
|
✅ = has knowledge
|
||||||
|
|
||||||
|
🥇 = is their main priority
|
||||||
|
|
||||||
|
⚠️ = is the only one with knowledge
|
||||||
|
|
||||||
|
👀 = has no knowledge but wants to be onboarded
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://dynamic-svgs.vercel.app/image.svg?dark">
|
||||||
|
<img alt="Areas of expertise table" src="https://dynamic-svgs.vercel.app/image.svg">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
The development branch is `main`. This is the branch that all pull
|
The development branch is `main`. This is the branch that all pull
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
<a href="https://cal.com">Website</a>
|
<a href="https://cal.com">Website</a>
|
||||||
·
|
·
|
||||||
<a href="https://github.com/calcom/cal.com/issues">Issues</a>
|
<a href="https://github.com/calcom/cal.com/issues">Issues</a>
|
||||||
|
·
|
||||||
|
<a href="https://cal.com/roadmap">Roadmap</a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -30,8 +32,10 @@
|
||||||
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
|
||||||
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
|
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
|
||||||
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
|
||||||
|
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
|
||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=social"></a>
|
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||||
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 8474e2baa27ef665b69491fa000ccb5d94257363
|
|
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
||||||
Subproject commit 14aca7ef2b81421bdf4c95020bf54255abe34dcb
|
Subproject commit ed2f42fb0195b1afa0bf2edbab1df2126038b273
|
|
@ -1 +1 @@
|
||||||
Subproject commit dfa050650c1507f24ec81d972c0c697ec07934d9
|
Subproject commit b6b26f47922a5404086bf34635338dc6afa9c1d3
|
|
@ -1,23 +1,21 @@
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
export default function AppsShell({ children }: { children: React.ReactNode }) {
|
const tabs = [
|
||||||
const { t } = useLocale();
|
|
||||||
const { status } = useSession();
|
|
||||||
const tabs = [
|
|
||||||
{
|
{
|
||||||
name: t("app_store"),
|
name: "app_store",
|
||||||
href: "/apps",
|
href: "/apps",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("installed_apps"),
|
name: "installed_apps",
|
||||||
href: "/apps/installed",
|
href: "/apps/installed",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default function AppsShell({ children }: { children: React.ReactNode }) {
|
||||||
|
const { status } = useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
export default function BookingsShell({ children }: { children: React.ReactNode }) {
|
const tabs = [
|
||||||
const { t } = useLocale();
|
|
||||||
const tabs = [
|
|
||||||
{
|
{
|
||||||
name: t("upcoming"),
|
name: "upcoming",
|
||||||
href: "/bookings/upcoming",
|
href: "/bookings/upcoming",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("recurring"),
|
name: "recurring",
|
||||||
href: "/bookings/recurring",
|
href: "/bookings/recurring",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("past"),
|
name: "past",
|
||||||
href: "/bookings/past",
|
href: "/bookings/past",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("cancelled"),
|
name: "cancelled",
|
||||||
href: "/bookings/cancelled",
|
href: "/bookings/cancelled",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default function BookingsShell({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
|
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useBrandColors } from "@calcom/embed-core";
|
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
|
||||||
|
|
||||||
const brandColor = "#292929";
|
const brandColor = "#292929";
|
||||||
const brandTextColor = "#ffffff";
|
const brandTextColor = "#ffffff";
|
||||||
|
|
|
@ -9,17 +9,17 @@ export default function EmptyScreen({
|
||||||
}: {
|
}: {
|
||||||
Icon: SVGComponent;
|
Icon: SVGComponent;
|
||||||
headline: string;
|
headline: string;
|
||||||
description: string;
|
description: string | React.ReactElement;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
|
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-white">
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
<Icon className="inline-block h-10 w-10 bg-white" />
|
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[420px] text-center">
|
<div className="max-w-[420px] text-center">
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium">{headline}</h2>
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
|
||||||
<p className="text-sm leading-6 text-gray-600">{description}</p>
|
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import Link, { LinkProps } from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, Fragment, MouseEventHandler } from "react";
|
import { FC, Fragment, MouseEventHandler } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
|
@ -22,6 +24,7 @@ export interface NavTabProps {
|
||||||
|
|
||||||
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
|
@ -77,7 +80,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>{tab.name}</span>
|
<span>{t(tab.name)}</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</Component>
|
</Component>
|
||||||
|
|
|
@ -1,48 +1,54 @@
|
||||||
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
|
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
|
||||||
import React from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import ErrorBoundary from "@lib/ErrorBoundary";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
import Shell from "./Shell";
|
||||||
|
|
||||||
export default function SettingsShell({ children }: { children: React.ReactNode }) {
|
const tabs = [
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
{
|
||||||
name: t("profile"),
|
name: "profile",
|
||||||
href: "/settings/profile",
|
href: "/settings/profile",
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("security"),
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: KeyIcon,
|
icon: KeyIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("teams"),
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: UserGroupIcon,
|
icon: UserGroupIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("billing"),
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: CreditCardIcon,
|
icon: CreditCardIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("admin"),
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: LockClosedIcon,
|
icon: LockClosedIcon,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default function SettingsShell({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Shell {...rest}>
|
||||||
<div className="sm:mx-auto">
|
<div className="sm:mx-auto">
|
||||||
<NavTabs tabs={tabs} />
|
<NavTabs tabs={tabs} />
|
||||||
</div>
|
</div>
|
||||||
<main className="max-w-4xl">{children}</main>
|
<main className="max-w-4xl">
|
||||||
|
<>
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</>
|
</>
|
||||||
|
</main>
|
||||||
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { useRouter } from "next/router";
|
||||||
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import Dropdown, {
|
import Dropdown, {
|
||||||
|
@ -32,6 +32,7 @@ import LicenseBanner from "@ee/components/LicenseBanner";
|
||||||
import TrialBanner from "@ee/components/TrialBanner";
|
import TrialBanner from "@ee/components/TrialBanner";
|
||||||
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
|
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
|
||||||
|
|
||||||
|
import ErrorBoundary from "@lib/ErrorBoundary";
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { WEBAPP_URL } from "@lib/config/constants";
|
import { WEBAPP_URL } from "@lib/config/constants";
|
||||||
import { shouldShowOnboarding } from "@lib/getting-started";
|
import { shouldShowOnboarding } from "@lib/getting-started";
|
||||||
|
@ -349,7 +350,7 @@ const Layout = ({
|
||||||
"px-4 sm:px-6 md:px-8",
|
"px-4 sm:px-6 md:px-8",
|
||||||
props.flexChildrenContainer && "flex flex-1 flex-col"
|
props.flexChildrenContainer && "flex flex-1 flex-col"
|
||||||
)}>
|
)}>
|
||||||
{!props.isLoading ? props.children : props.customLoader}
|
<ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
{/* show bottom navigation for md and smaller (tablet and phones) */}
|
{/* show bottom navigation for md and smaller (tablet and phones) */}
|
||||||
{status === "authenticated" && (
|
{status === "authenticated" && (
|
||||||
|
@ -477,7 +478,7 @@ function UserDropdown({ small }: { small?: boolean }) {
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
className="rounded-full"
|
className="rounded-full"
|
||||||
src={process.env.NEXT_PUBLIC_WEBSITE_URL + "/" + user?.username + "/avatar.png"}
|
src={WEBAPP_URL + "/" + user?.username + "/avatar.png"}
|
||||||
alt={user?.username || "Nameless User"}
|
alt={user?.username || "Nameless User"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,20 +46,17 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="mb-4">
|
<div className="mb-8">
|
||||||
<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("add_new_schedule")}
|
{t("add_new_schedule")}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
handleSubmit={(values) => {
|
handleSubmit={(values) => {
|
||||||
createMutation.mutate(values);
|
createMutation.mutate(values);
|
||||||
}}>
|
}}>
|
||||||
<div className="mt-3 space-y-4">
|
<div className="mt-3 space-y-2">
|
||||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||||
{t("name")}
|
{t("name")}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
BanIcon,
|
BanIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
|
LocationMarkerIcon,
|
||||||
PaperAirplaneIcon,
|
PaperAirplaneIcon,
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
@ -16,6 +17,7 @@ import { Frequency as RRuleFrequency } from "rrule";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import showToast from "@calcom/lib/notification";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
|
@ -23,9 +25,11 @@ import { TextArea } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
import { LocationType } from "@lib/location";
|
||||||
import { parseRecurringDates } from "@lib/parseDate";
|
import { parseRecurringDates } from "@lib/parseDate";
|
||||||
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
|
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||||
|
|
||||||
|
@ -72,6 +76,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new HttpError({ statusCode: res.status });
|
throw new HttpError({ statusCode: res.status });
|
||||||
}
|
}
|
||||||
|
setRejectionDialogIsOpen(false);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
async onSettled() {
|
async onSettled() {
|
||||||
|
@ -89,8 +94,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||||
? t("reject_all")
|
? t("reject_all")
|
||||||
: t("reject"),
|
: t("reject"),
|
||||||
onClick: (e) => {
|
onClick: () => {
|
||||||
e.stopPropagation();
|
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: BanIcon,
|
icon: BanIcon,
|
||||||
|
@ -102,8 +106,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||||
? t("confirm_all")
|
? t("confirm_all")
|
||||||
: t("confirm"),
|
: t("confirm"),
|
||||||
onClick: (e) => {
|
onClick: () => {
|
||||||
e.stopPropagation();
|
|
||||||
mutation.mutate(true);
|
mutation.mutate(true);
|
||||||
},
|
},
|
||||||
icon: CheckIcon,
|
icon: CheckIcon,
|
||||||
|
@ -120,25 +123,33 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
icon: XIcon,
|
icon: XIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "edit_booking",
|
||||||
label: t("reschedule"),
|
label: t("edit_booking"),
|
||||||
icon: ClockIcon,
|
icon: PencilAltIcon,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "edit",
|
id: "reschedule",
|
||||||
icon: PencilAltIcon,
|
icon: ClockIcon,
|
||||||
label: t("edit_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}`,
|
href: `/reschedule/${booking.uid}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: ClockIcon,
|
icon: PaperAirplaneIcon,
|
||||||
|
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
|
||||||
label: t("send_reschedule_request"),
|
label: t("send_reschedule_request"),
|
||||||
onClick: (e) => {
|
onClick: () => {
|
||||||
e.stopPropagation();
|
|
||||||
setIsOpenRescheduleDialog(true);
|
setIsOpenRescheduleDialog(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "change_location",
|
||||||
|
label: t("edit_location"),
|
||||||
|
onClick: () => {
|
||||||
|
setIsOpenLocationDialog(true);
|
||||||
|
},
|
||||||
|
icon: LocationMarkerIcon,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -154,6 +165,26 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
|
|
||||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||||
|
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||||
|
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("location_updated"), "success");
|
||||||
|
setIsOpenLocationDialog(false);
|
||||||
|
utils.invalidateQueries("viewer.bookings");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => {
|
||||||
|
let newLocation = newLocationType as string;
|
||||||
|
if (
|
||||||
|
newLocationType === LocationType.InPerson ||
|
||||||
|
newLocationType === LocationType.Link ||
|
||||||
|
newLocationType === LocationType.UserPhone
|
||||||
|
) {
|
||||||
|
newLocation = details[Object.keys(details)[0]];
|
||||||
|
}
|
||||||
|
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [];
|
let recurringStrings: string[] = [];
|
||||||
|
@ -168,6 +199,30 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
router.push({
|
||||||
|
pathname: "/success",
|
||||||
|
query: {
|
||||||
|
date: booking.startTime,
|
||||||
|
type: booking.eventType.id,
|
||||||
|
eventSlug: booking.eventType.slug,
|
||||||
|
user: user?.username || "",
|
||||||
|
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
|
||||||
|
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||||
|
location: booking.location
|
||||||
|
? booking.location.includes("integration")
|
||||||
|
? (t("web_conferencing_details_to_follow") as string)
|
||||||
|
: booking.location
|
||||||
|
: "",
|
||||||
|
eventName: booking.eventType.eventName || "",
|
||||||
|
bookingId: booking.id,
|
||||||
|
recur: booking.recurringEventId,
|
||||||
|
reschedule: booking.confirmed,
|
||||||
|
listingStatus: booking.listingStatus,
|
||||||
|
status: booking.status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
|
@ -175,6 +230,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||||
bookingUId={booking.uid}
|
bookingUId={booking.uid}
|
||||||
/>
|
/>
|
||||||
|
<EditLocationDialog
|
||||||
|
booking={booking}
|
||||||
|
saveLocation={saveLocation}
|
||||||
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||||
|
@ -209,32 +270,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr
|
<tr className="flex hover:bg-neutral-50">
|
||||||
className="flex cursor-pointer hover:bg-neutral-50"
|
<td
|
||||||
onClick={() =>
|
className="hidden whitespace-nowrap align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"
|
||||||
router.push({
|
onClick={onClick}>
|
||||||
pathname: "/success",
|
<div className="cursor-pointer py-4">
|
||||||
query: {
|
|
||||||
date: booking.startTime,
|
|
||||||
type: booking.eventType.id,
|
|
||||||
eventSlug: booking.eventType.slug,
|
|
||||||
user: user?.username || "",
|
|
||||||
name: booking.attendees[0].name,
|
|
||||||
email: booking.attendees[0].email,
|
|
||||||
location: booking.location
|
|
||||||
? booking.location.includes("integration")
|
|
||||||
? (t("web_conferencing_details_to_follow") as string)
|
|
||||||
: booking.location
|
|
||||||
: "",
|
|
||||||
eventName: booking.eventType.eventName || "",
|
|
||||||
bookingId: booking.id,
|
|
||||||
recur: booking.recurringEventId,
|
|
||||||
reschedule: booking.confirmed,
|
|
||||||
status: booking.listingStatus,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56">
|
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||||
|
@ -259,7 +299,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
.toLowerCase()}`
|
.toLowerCase()}`
|
||||||
),
|
),
|
||||||
})} ${booking.recurringCount} ${t(
|
})} ${booking.recurringCount} ${t(
|
||||||
`${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
|
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()}`,
|
||||||
{ count: booking.recurringCount }
|
{ count: booking.recurringCount }
|
||||||
)}`}
|
)}`}
|
||||||
</p>
|
</p>
|
||||||
|
@ -268,8 +310,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}>
|
<td
|
||||||
|
className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
|
||||||
|
onClick={onClick}>
|
||||||
|
<div className="cursor-pointer py-4">
|
||||||
<div className="sm:hidden">
|
<div className="sm:hidden">
|
||||||
{!booking.confirmed && !booking.rejected && (
|
{!booking.confirmed && !booking.rejected && (
|
||||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
|
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
|
||||||
|
@ -300,7 +346,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div className="max-w-52 md:max-w-96 truncate text-sm text-gray-500" title={booking.description}>
|
<div
|
||||||
|
className="max-w-52 md:max-w-96 truncate text-sm text-gray-500"
|
||||||
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -318,6 +366,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
||||||
|
|
|
@ -7,7 +7,7 @@ import utc from "dayjs/plugin/utc";
|
||||||
import { memoize } from "lodash";
|
import { memoize } from "lodash";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useEmbedStyles } from "@calcom/embed-core";
|
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
|
@ -30,9 +30,9 @@ import {
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
sdkActionManager,
|
sdkActionManager,
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
} from "@calcom/embed-core";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
|
|
||||||
|
@ -68,6 +68,8 @@ export const locationKeyToString = (location: LocationObject, t: TFunction) => {
|
||||||
case LocationType.Link:
|
case LocationType.Link:
|
||||||
return location.link || "Link"; // If disabled link won't exist on the object
|
return location.link || "Link"; // If disabled link won't exist on the object
|
||||||
case LocationType.Phone:
|
case LocationType.Phone:
|
||||||
|
return t("your_number");
|
||||||
|
case LocationType.UserPhone:
|
||||||
return t("phone_call");
|
return t("phone_call");
|
||||||
case LocationType.GoogleMeet:
|
case LocationType.GoogleMeet:
|
||||||
return "Google Meet";
|
return "Google Meet";
|
||||||
|
@ -234,7 +236,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||||
.filter((user) => user.name !== profile.name)
|
.filter((user) => user.name !== profile.name)
|
||||||
.map((user) => ({
|
.map((user) => ({
|
||||||
title: user.name,
|
title: user.name,
|
||||||
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
|
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||||
alt: user.name || undefined,
|
alt: user.name || undefined,
|
||||||
})),
|
})),
|
||||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||||
|
@ -328,7 +330,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||||
.map((user) => ({
|
.map((user) => ({
|
||||||
title: user.name,
|
title: user.name,
|
||||||
alt: user.name,
|
alt: user.name,
|
||||||
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
|
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||||
})),
|
})),
|
||||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||||
}
|
}
|
||||||
|
@ -370,7 +372,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||||
return (
|
return (
|
||||||
<span key={el.type}>
|
<span key={el.type}>
|
||||||
{locationKeyToString(el, t)}{" "}
|
{locationKeyToString(el, t)}{" "}
|
||||||
{arr.length - 1 !== i && <span className="font-light"> or </span>}
|
{arr.length - 1 !== i && (
|
||||||
|
<span className="font-light"> {t("or_lowercase")} </span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -24,7 +24,11 @@ import { Frequency as RRuleFrequency } from "rrule";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useEmbedNonStylesConfig, useIsBackgroundTransparent, useIsEmbed } from "@calcom/embed-core";
|
import {
|
||||||
|
useEmbedNonStylesConfig,
|
||||||
|
useIsBackgroundTransparent,
|
||||||
|
useIsEmbed,
|
||||||
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
@ -79,6 +83,7 @@ type BookingFormValues = {
|
||||||
customInputs?: {
|
customInputs?: {
|
||||||
[key: string]: string | boolean;
|
[key: string]: string | boolean;
|
||||||
};
|
};
|
||||||
|
rescheduleReason?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BookingPage = ({
|
const BookingPage = ({
|
||||||
|
@ -251,6 +256,7 @@ const BookingPage = ({
|
||||||
email: primaryAttendee.email || "",
|
email: primaryAttendee.email || "",
|
||||||
guests: guestListEmails,
|
guests: guestListEmails,
|
||||||
notes: booking.description || "",
|
notes: booking.description || "",
|
||||||
|
rescheduleReason: "",
|
||||||
customInputs: eventType.customInputs.reduce(
|
customInputs: eventType.customInputs.reduce(
|
||||||
(customInputs, input) => ({
|
(customInputs, input) => ({
|
||||||
...customInputs,
|
...customInputs,
|
||||||
|
@ -782,8 +788,19 @@ const BookingPage = ({
|
||||||
<label
|
<label
|
||||||
htmlFor="notes"
|
htmlFor="notes"
|
||||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
{t("additional_notes")}
|
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
|
||||||
</label>
|
</label>
|
||||||
|
{rescheduleUid ? (
|
||||||
|
<textarea
|
||||||
|
{...bookingForm.register("rescheduleReason")}
|
||||||
|
id="rescheduleReason"
|
||||||
|
name="rescheduleReason"
|
||||||
|
rows={3}
|
||||||
|
className={inputClassName}
|
||||||
|
placeholder={t("reschedule_placeholder")}
|
||||||
|
disabled={disabledExceptForOwner}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
{...bookingForm.register("notes")}
|
{...bookingForm.register("notes")}
|
||||||
id="notes"
|
id="notes"
|
||||||
|
@ -793,7 +810,9 @@ const BookingPage = ({
|
||||||
placeholder={t("share_additional_notes")}
|
placeholder={t("share_additional_notes")}
|
||||||
disabled={disabledExceptForOwner}
|
disabled={disabledExceptForOwner}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -0,0 +1,336 @@
|
||||||
|
import { LocationMarkerIcon } from "@heroicons/react/solid";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Button } from "@calcom/ui";
|
||||||
|
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||||
|
import { Form } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
import { linkValueToString } from "@lib/linkValueToString";
|
||||||
|
import { LocationType } from "@lib/location";
|
||||||
|
import { LocationOptionsToString } from "@lib/locationOptions";
|
||||||
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
|
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
||||||
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
|
const PhoneInput = dynamic(
|
||||||
|
() => import("@components/ui/form/PhoneInput")
|
||||||
|
) as unknown as typeof PhoneInputType;
|
||||||
|
|
||||||
|
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||||
|
|
||||||
|
type OptionTypeBase = {
|
||||||
|
label: string;
|
||||||
|
value: LocationType;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LocationFormValues = {
|
||||||
|
locationType: LocationType;
|
||||||
|
locationAddress?: string;
|
||||||
|
locationLink?: string;
|
||||||
|
locationPhoneNumber?: string;
|
||||||
|
displayLocationPublicly?: boolean;
|
||||||
|
};
|
||||||
|
interface ISetLocationDialog {
|
||||||
|
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
|
||||||
|
selection?: OptionTypeBase;
|
||||||
|
booking?: BookingItem;
|
||||||
|
defaultValues?: {
|
||||||
|
type: LocationType;
|
||||||
|
address?: string | undefined;
|
||||||
|
link?: string | undefined;
|
||||||
|
hostPhoneNumber?: string | undefined;
|
||||||
|
displayLocationPublicly?: boolean | undefined;
|
||||||
|
}[];
|
||||||
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
isOpenDialog: boolean;
|
||||||
|
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
|
const {
|
||||||
|
saveLocation,
|
||||||
|
selection,
|
||||||
|
booking,
|
||||||
|
setShowLocationModal,
|
||||||
|
isOpenDialog,
|
||||||
|
defaultValues,
|
||||||
|
setSelectedLocation,
|
||||||
|
} = props;
|
||||||
|
const { t } = useLocale();
|
||||||
|
const locationsQuery = trpc.useQuery(["viewer.locationOptions"]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selection) {
|
||||||
|
locationFormMethods.setValue("locationType", selection?.value);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selection]);
|
||||||
|
|
||||||
|
const locationFormSchema = z.object({
|
||||||
|
locationType: z.string(),
|
||||||
|
locationAddress: z.string().optional(),
|
||||||
|
locationLink: z.string().url().optional(),
|
||||||
|
displayLocationPublicly: z.boolean().optional(),
|
||||||
|
locationPhoneNumber: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationFormMethods = useForm<LocationFormValues>({
|
||||||
|
mode: "onSubmit",
|
||||||
|
resolver: zodResolver(locationFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedLocation = useWatch({
|
||||||
|
control: locationFormMethods.control,
|
||||||
|
name: "locationType",
|
||||||
|
});
|
||||||
|
|
||||||
|
const LocationOptions =
|
||||||
|
selectedLocation === LocationType.InPerson ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||||
|
{t("set_address_place")}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...locationFormMethods.register("locationAddress")}
|
||||||
|
id="address"
|
||||||
|
required
|
||||||
|
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||||
|
defaultValue={
|
||||||
|
defaultValues
|
||||||
|
? defaultValues.find(
|
||||||
|
(location: { type: LocationType }) => location.type === LocationType.InPerson
|
||||||
|
)?.address
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!booking && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Controller
|
||||||
|
name="displayLocationPublicly"
|
||||||
|
control={locationFormMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<CheckboxField
|
||||||
|
defaultChecked={
|
||||||
|
defaultValues
|
||||||
|
? defaultValues.find((location) => location.type === LocationType.InPerson)
|
||||||
|
?.displayLocationPublicly
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
description={t("display_location_label")}
|
||||||
|
onChange={(e) =>
|
||||||
|
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||||
|
}
|
||||||
|
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : selectedLocation === LocationType.Link ? (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
|
||||||
|
{t("set_link_meeting")}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...locationFormMethods.register("locationLink")}
|
||||||
|
id="link"
|
||||||
|
required
|
||||||
|
className="block w-full rounded-sm border-gray-300 sm:text-sm"
|
||||||
|
defaultValue={
|
||||||
|
defaultValues
|
||||||
|
? defaultValues.find(
|
||||||
|
(location: { type: LocationType }) => location.type === LocationType.Link
|
||||||
|
)?.link
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{locationFormMethods.formState.errors.locationLink && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!booking && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<Controller
|
||||||
|
name="displayLocationPublicly"
|
||||||
|
control={locationFormMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<CheckboxField
|
||||||
|
description={t("display_location_label")}
|
||||||
|
defaultChecked={
|
||||||
|
defaultValues
|
||||||
|
? defaultValues.find((location) => location.type === LocationType.Link)
|
||||||
|
?.displayLocationPublicly
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
|
||||||
|
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : selectedLocation === LocationType.UserPhone ? (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
|
||||||
|
{t("set_your_phone_number")}
|
||||||
|
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<PhoneInput<LocationFormValues>
|
||||||
|
control={locationFormMethods.control}
|
||||||
|
name="locationPhoneNumber"
|
||||||
|
required
|
||||||
|
id="locationPhoneNumber"
|
||||||
|
placeholder={t("host_phone_number")}
|
||||||
|
defaultValue={
|
||||||
|
defaultValues
|
||||||
|
? defaultValues.find(
|
||||||
|
(location: { type: LocationType }) => location.type === LocationType.UserPhone
|
||||||
|
)?.hostPhoneNumber
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{locationFormMethods.formState.errors.locationPhoneNumber && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">Invalid input</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpenDialog}>
|
||||||
|
<DialogContent asChild>
|
||||||
|
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||||
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
|
<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">
|
||||||
|
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
{t("edit_location")}
|
||||||
|
</h3>
|
||||||
|
{!booking && (
|
||||||
|
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"></div>
|
||||||
|
</div>
|
||||||
|
{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">{linkValueToString(booking.location, t)}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Form
|
||||||
|
form={locationFormMethods}
|
||||||
|
handleSubmit={async (values) => {
|
||||||
|
const { locationType: newLocation, displayLocationPublicly } = values;
|
||||||
|
|
||||||
|
let details = {};
|
||||||
|
if (newLocation === LocationType.InPerson) {
|
||||||
|
details = {
|
||||||
|
address: values.locationAddress,
|
||||||
|
displayLocationPublicly,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (newLocation === LocationType.Link) {
|
||||||
|
details = { link: values.locationLink, displayLocationPublicly };
|
||||||
|
}
|
||||||
|
if (newLocation === LocationType.UserPhone) {
|
||||||
|
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLocation(newLocation, details);
|
||||||
|
setShowLocationModal(false);
|
||||||
|
setSelectedLocation?.(undefined);
|
||||||
|
locationFormMethods.unregister([
|
||||||
|
"locationType",
|
||||||
|
"locationLink",
|
||||||
|
"locationAddress",
|
||||||
|
"locationPhoneNumber",
|
||||||
|
]);
|
||||||
|
}}>
|
||||||
|
<QueryCell
|
||||||
|
query={locationsQuery}
|
||||||
|
success={({ data: locationOptions }) => {
|
||||||
|
if (!locationOptions.length) return null;
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name="locationType"
|
||||||
|
control={locationFormMethods.control}
|
||||||
|
render={() => (
|
||||||
|
<Select
|
||||||
|
maxMenuHeight={150}
|
||||||
|
name="location"
|
||||||
|
defaultValue={selection}
|
||||||
|
options={
|
||||||
|
booking
|
||||||
|
? locationOptions.filter((location) => location.value !== "phone")
|
||||||
|
: locationOptions
|
||||||
|
}
|
||||||
|
isSearchable={false}
|
||||||
|
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
|
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-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>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,10 +1,10 @@
|
||||||
import { UserIcon } from "@heroicons/react/outline";
|
import { UserIcon } from "@heroicons/react/outline";
|
||||||
import { InformationCircleIcon } from "@heroicons/react/solid";
|
import { InformationCircleIcon } from "@heroicons/react/solid";
|
||||||
import { MembershipRole } from "@prisma/client";
|
import { MembershipRole } from "@prisma/client";
|
||||||
import React, { useState, useEffect, SyntheticEvent, useMemo } from "react";
|
import React, { useState, SyntheticEvent, useMemo } from "react";
|
||||||
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
|
||||||
import { TextField } from "@calcom/ui/form/fields";
|
import { TextField } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { MembershipRole } from "@prisma/client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
@ -74,7 +74,7 @@ export default function MemberListItem(props: Props) {
|
||||||
<div className="flex w-full flex-col justify-between sm:flex-row">
|
<div className="flex w-full flex-col justify-between sm:flex-row">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Avatar
|
<Avatar
|
||||||
imageSrc={WEBSITE_URL + "/" + props.member.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + props.member.username + "/avatar.png"}
|
||||||
alt={name || ""}
|
alt={name || ""}
|
||||||
className="h-9 w-9 rounded-full"
|
className="h-9 w-9 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,13 +5,12 @@ import Link from "next/link";
|
||||||
import { TeamPageProps } from "pages/team/[slug]";
|
import { TeamPageProps } from "pages/team/[slug]";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import Text from "@components/ui/Text";
|
|
||||||
|
|
||||||
type TeamType = TeamPageProps["team"];
|
type TeamType = TeamPageProps["team"];
|
||||||
type MembersType = TeamType["members"];
|
type MembersType = TeamType["members"];
|
||||||
|
@ -52,14 +51,14 @@ const Team = ({ team }: TeamPageProps) => {
|
||||||
<div>
|
<div>
|
||||||
<Avatar
|
<Avatar
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||||
className="-mt-4 h-12 w-12"
|
className="-mt-4 h-12 w-12"
|
||||||
/>
|
/>
|
||||||
<section className="mt-2 w-full space-y-1">
|
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||||
<Text variant="title">{member.name}</Text>
|
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
|
||||||
<Text variant="subtitle" className="">
|
<p className="text-sm font-normal text-neutral-500 dark:text-white">
|
||||||
{member.bio || t("user_from_team", { user: member.name, team: team.name })}
|
{member.bio || t("user_from_team", { user: member.name, team: team.name })}
|
||||||
</Text>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"inline-block transform bg-white text-left align-bottom transition-all sm:align-middle",
|
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
|
||||||
{
|
{
|
||||||
"sm:w-full sm:max-w-lg ": !props.wide,
|
"sm:w-full sm:max-w-lg ": !props.wide,
|
||||||
"sm:w-4xl sm:max-w-4xl": props.wide,
|
"sm:w-4xl sm:max-w-4xl": props.wide,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
export type ActionType = {
|
export type ActionType = {
|
||||||
id: string;
|
id: string;
|
||||||
icon?: SVGComponent;
|
icon?: SVGComponent;
|
||||||
|
iconClassName?: string;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
color?: "primary" | "secondary";
|
color?: "primary" | "secondary";
|
||||||
|
@ -52,6 +53,7 @@ const DropdownActions = ({
|
||||||
className="w-full rounded-none font-normal"
|
className="w-full rounded-none font-normal"
|
||||||
href={action.href}
|
href={action.href}
|
||||||
StartIcon={action.icon}
|
StartIcon={action.icon}
|
||||||
|
startIconClassName={action.iconClassName}
|
||||||
onClick={action.onClick || defaultAction}
|
onClick={action.onClick || defaultAction}
|
||||||
data-testid={action.id}>
|
data-testid={action.id}>
|
||||||
{action.label}
|
{action.label}
|
||||||
|
@ -81,6 +83,7 @@ const TableActions: FC<Props> = ({ actions }) => {
|
||||||
href={action.href}
|
href={action.href}
|
||||||
onClick={action.onClick || defaultAction}
|
onClick={action.onClick || defaultAction}
|
||||||
StartIcon={action.icon}
|
StartIcon={action.icon}
|
||||||
|
startIconClassName={action.iconClassName}
|
||||||
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
|
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
color={action.color || "secondary"}>
|
color={action.color || "secondary"}>
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Body: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-gray-900 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Body;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Body from "./Body";
|
|
||||||
|
|
||||||
export default Body;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Caption: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-sm text-gray-500 dark:text-white leading-tight", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Caption;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Caption from "./Caption";
|
|
||||||
|
|
||||||
export default Caption;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Caption2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-xs italic text-gray-500 dark:text-white leading-tight", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Caption2;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Caption2 from "./Caption2";
|
|
||||||
|
|
||||||
export default Caption2;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Footnote: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-xs font-medium text-gray-500 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footnote;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Footnote from "./Footnote";
|
|
||||||
|
|
||||||
export default Footnote;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Headline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("font-cal text-xl text-gray-900 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Headline;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Headline from "./Headline";
|
|
||||||
|
|
||||||
export default Headline;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Largetitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("font-cal tracking-wider text-3xl mb-2", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Largetitle;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Largetitle from "./Largetitle";
|
|
||||||
|
|
||||||
export default Largetitle;
|
|
|
@ -1,15 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Overline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames(
|
|
||||||
"text-sm capitalize font-medium text-gray-900 dark:text-white",
|
|
||||||
props?.className
|
|
||||||
);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Overline;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Overline from "./Overline";
|
|
||||||
|
|
||||||
export default Overline;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Subheadline: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-xl text-gray-500 dark:text-white leading-relaxed", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Subheadline;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Subheadline from "./Subheadline";
|
|
||||||
|
|
||||||
export default Subheadline;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Subtitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-sm font-normal text-neutral-500 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Subtitle;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Subtitle from "./Subtitle";
|
|
||||||
|
|
||||||
export default Subtitle;
|
|
|
@ -1,164 +0,0 @@
|
||||||
/**
|
|
||||||
* @deprecated create new a new set of components, waiting for designs
|
|
||||||
*/
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import Body from "./Body";
|
|
||||||
import Caption from "./Caption";
|
|
||||||
import Caption2 from "./Caption2";
|
|
||||||
import Footnote from "./Footnote";
|
|
||||||
import Headline from "./Headline";
|
|
||||||
import Largetitle from "./Largetitle";
|
|
||||||
import Overline from "./Overline";
|
|
||||||
import Subheadline from "./Subheadline";
|
|
||||||
import Subtitle from "./Subtitle";
|
|
||||||
import Title from "./Title";
|
|
||||||
import Title2 from "./Title2";
|
|
||||||
import Title3 from "./Title3";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
variant?:
|
|
||||||
| "overline"
|
|
||||||
| "caption"
|
|
||||||
| "body"
|
|
||||||
| "caption2"
|
|
||||||
| "footnote"
|
|
||||||
| "headline"
|
|
||||||
| "largetitle"
|
|
||||||
| "subheadline"
|
|
||||||
| "subtitle"
|
|
||||||
| "title"
|
|
||||||
| "title2"
|
|
||||||
| "title3";
|
|
||||||
children: any;
|
|
||||||
text?: string;
|
|
||||||
tx?: string;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TextProps = {
|
|
||||||
children: any;
|
|
||||||
text?: string;
|
|
||||||
tx?: string;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* static let largeTitle: Font
|
|
||||||
* A font with the large title text style.
|
|
||||||
*
|
|
||||||
* static let title: Font
|
|
||||||
* A font with the title text style.
|
|
||||||
*
|
|
||||||
* static let title2: Font
|
|
||||||
* Create a font for second level hierarchical headings.
|
|
||||||
*
|
|
||||||
* static let title3: Font
|
|
||||||
* Create a font for third level hierarchical headings.
|
|
||||||
*
|
|
||||||
* static let headline: Font
|
|
||||||
* A font with the headline text style.
|
|
||||||
*
|
|
||||||
* static let subheadline: Font
|
|
||||||
* A font with the subheadline text style.
|
|
||||||
*
|
|
||||||
* static let body: Font
|
|
||||||
* A font with the body text style.
|
|
||||||
*
|
|
||||||
* static let callout: Font
|
|
||||||
* A font with the callout text style.
|
|
||||||
*
|
|
||||||
* static let caption: Font
|
|
||||||
* A font with the caption text style.
|
|
||||||
*
|
|
||||||
* static let caption2: Font
|
|
||||||
* Create a font with the alternate caption text style.
|
|
||||||
*
|
|
||||||
* static let footnote: Font
|
|
||||||
* A font with the footnote text style.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Text: React.FunctionComponent<Props> = (props: Props) => {
|
|
||||||
switch (props?.variant) {
|
|
||||||
case "overline":
|
|
||||||
return (
|
|
||||||
<Overline text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Overline>
|
|
||||||
);
|
|
||||||
case "body":
|
|
||||||
return (
|
|
||||||
<Body text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Body>
|
|
||||||
);
|
|
||||||
case "caption":
|
|
||||||
return (
|
|
||||||
<Caption text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Caption>
|
|
||||||
);
|
|
||||||
case "caption2":
|
|
||||||
return (
|
|
||||||
<Caption2 text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Caption2>
|
|
||||||
);
|
|
||||||
case "footnote":
|
|
||||||
return (
|
|
||||||
<Footnote text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Footnote>
|
|
||||||
);
|
|
||||||
case "headline":
|
|
||||||
return (
|
|
||||||
<Headline text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
case "largetitle":
|
|
||||||
return (
|
|
||||||
<Largetitle text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Largetitle>
|
|
||||||
);
|
|
||||||
case "subheadline":
|
|
||||||
return (
|
|
||||||
<Subheadline text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Subheadline>
|
|
||||||
);
|
|
||||||
case "subtitle":
|
|
||||||
return (
|
|
||||||
<Subtitle text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Subtitle>
|
|
||||||
);
|
|
||||||
case "title":
|
|
||||||
return (
|
|
||||||
<Title text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Title>
|
|
||||||
);
|
|
||||||
case "title2":
|
|
||||||
return (
|
|
||||||
<Title2 text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Title2>
|
|
||||||
);
|
|
||||||
case "title3":
|
|
||||||
return (
|
|
||||||
<Title3 text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Title3>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Body text={props?.text} tx={props?.tx} className={props?.className}>
|
|
||||||
{props.children}
|
|
||||||
</Body>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Text;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Title: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("font-medium text-neutral-900 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Title;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Title from "./Title";
|
|
||||||
|
|
||||||
export default Title;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Title2: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames("text-base font-normal text-gray-900 dark:text-white", props?.className);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Title2;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Title2 from "./Title2";
|
|
||||||
|
|
||||||
export default Title2;
|
|
|
@ -1,15 +0,0 @@
|
||||||
import classnames from "classnames";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { TextProps } from "../Text";
|
|
||||||
|
|
||||||
const Title3: React.FunctionComponent<TextProps> = (props: TextProps) => {
|
|
||||||
const classes = classnames(
|
|
||||||
"text-xs font-semibold leading-tight text-gray-900 dark:text-white",
|
|
||||||
props?.className
|
|
||||||
);
|
|
||||||
|
|
||||||
return <p className={classes}>{props?.text || props.children}</p>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Title3;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import Title3 from "./Title3";
|
|
||||||
|
|
||||||
export default Title3;
|
|
|
@ -1,40 +0,0 @@
|
||||||
import Body from "./Body";
|
|
||||||
import Caption from "./Caption";
|
|
||||||
import Caption2 from "./Caption2";
|
|
||||||
import Footnote from "./Footnote";
|
|
||||||
import Headline from "./Headline";
|
|
||||||
import Largetitle from "./Largetitle";
|
|
||||||
import Overline from "./Overline";
|
|
||||||
import Subheadline from "./Subheadline";
|
|
||||||
import Subtitle from "./Subtitle";
|
|
||||||
import Text from "./Text";
|
|
||||||
import Title from "./Title";
|
|
||||||
import Title2 from "./Title2";
|
|
||||||
import Title3 from "./Title3";
|
|
||||||
|
|
||||||
export { Text };
|
|
||||||
export default Text;
|
|
||||||
|
|
||||||
export { Title };
|
|
||||||
|
|
||||||
export { Title2 };
|
|
||||||
|
|
||||||
export { Title3 };
|
|
||||||
|
|
||||||
export { Largetitle };
|
|
||||||
|
|
||||||
export { Subtitle };
|
|
||||||
|
|
||||||
export { Headline };
|
|
||||||
|
|
||||||
export { Subheadline };
|
|
||||||
|
|
||||||
export { Caption };
|
|
||||||
|
|
||||||
export { Caption2 };
|
|
||||||
|
|
||||||
export { Footnote };
|
|
||||||
|
|
||||||
export { Overline };
|
|
||||||
|
|
||||||
export { Body };
|
|
|
@ -1,51 +1,60 @@
|
||||||
import React, { forwardRef, InputHTMLAttributes } from "react";
|
import React, { forwardRef, InputHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
import InfoBadge from "@components/ui/InfoBadge";
|
import InfoBadge from "@components/ui/InfoBadge";
|
||||||
|
|
||||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
description: string;
|
description: string;
|
||||||
descriptionAsLabel?: boolean;
|
descriptionAsLabel?: boolean;
|
||||||
infomationIconText?: string;
|
informationIconText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||||
({ label, description, infomationIconText, descriptionAsLabel, ...rest }, ref) => {
|
({ label, description, informationIconText, ...rest }, ref) => {
|
||||||
|
const descriptionAsLabel = !label || rest.descriptionAsLabel;
|
||||||
return (
|
return (
|
||||||
<div className="block items-center sm:flex">
|
<div className="block items-center sm:flex">
|
||||||
{label && !descriptionAsLabel && (
|
{label && (
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700">
|
{React.createElement(
|
||||||
{label}
|
descriptionAsLabel ? "div" : "label",
|
||||||
</label>
|
{
|
||||||
</div>
|
className: "flex text-sm font-medium text-neutral-700",
|
||||||
|
...(!descriptionAsLabel
|
||||||
|
? {
|
||||||
|
htmlFor: rest.id,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
label
|
||||||
)}
|
)}
|
||||||
{label && descriptionAsLabel && (
|
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
|
||||||
<span className="flex text-sm font-medium text-neutral-700">{label}</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
|
{React.createElement(
|
||||||
|
descriptionAsLabel ? "label" : "div",
|
||||||
|
{
|
||||||
|
className: classNames(
|
||||||
|
"relative flex items-start",
|
||||||
|
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
<>
|
||||||
<div className="flex h-5 items-center">
|
<div className="flex h-5 items-center">
|
||||||
<input
|
<input
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled={rest.disabled}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm ltr:ml-3 rtl:mr-3">
|
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
|
||||||
{!label || descriptionAsLabel ? (
|
</>
|
||||||
<label htmlFor={rest.id} className="text-neutral-700">
|
|
||||||
{description}
|
|
||||||
</label>
|
|
||||||
) : (
|
|
||||||
<p className="text-neutral-900">{description}</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{informationIconText && <InfoBadge content={informationIconText}></InfoBadge>}
|
||||||
{infomationIconText && <InfoBadge content={infomationIconText}></InfoBadge>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import React, { AriaRole, ComponentType, FC, Fragment } from "react";
|
||||||
|
|
||||||
|
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import EmptyScreen from "@components/EmptyScreen";
|
||||||
|
|
||||||
|
type LicenseRequiredProps = {
|
||||||
|
as?: keyof JSX.IntrinsicElements | "";
|
||||||
|
className?: string;
|
||||||
|
role?: AriaRole | undefined;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component will only render it's children if the installation has a valid
|
||||||
|
* license.
|
||||||
|
*/
|
||||||
|
const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest }) => {
|
||||||
|
const session = useSession();
|
||||||
|
const Component = as || Fragment;
|
||||||
|
return (
|
||||||
|
<Component {...rest}>
|
||||||
|
{session.data?.hasValidLicense ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={ExclamationIcon}
|
||||||
|
headline="This is an enterprise feature"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
To enable this feature, get a deployment key at{" "}
|
||||||
|
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
|
||||||
|
Cal.com console
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function withLicenseRequired<T>(Component: ComponentType<T>) {
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
|
return (hocProps: T) => {
|
||||||
|
return (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...(hocProps as T)} />;
|
||||||
|
</LicenseRequired>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LicenseRequired;
|
|
@ -15,11 +15,12 @@ import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||||
|
|
||||||
import { TApiKeys } from "./ApiKeyListItem";
|
import LicenseRequired from "../LicenseRequired";
|
||||||
|
import type { TApiKeys } from "./ApiKeyListItem";
|
||||||
|
|
||||||
export default function ApiKeyDialogForm(props: {
|
export default function ApiKeyDialogForm(props: {
|
||||||
title: string;
|
title: string;
|
||||||
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean };
|
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: {
|
||||||
const watchNeverExpires = form.watch("neverExpires");
|
const watchNeverExpires = form.watch("neverExpires");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<LicenseRequired>
|
||||||
{successfulNewApiKeyModal ? (
|
{successfulNewApiKeyModal ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
|
@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }>
|
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean }>
|
||||||
form={form}
|
form={form}
|
||||||
handleSubmit={async (event) => {
|
handleSubmit={async (event) => {
|
||||||
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
|
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
|
||||||
setApiKey(apiKey);
|
setApiKey(apiKey);
|
||||||
setApiKeyDetails({ ...event });
|
setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires });
|
||||||
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
await utils.invalidateQueries(["viewer.apiKeys.list"]);
|
||||||
setSuccessfulNewApiKeyModal(true);
|
setSuccessfulNewApiKeyModal(true);
|
||||||
}}
|
}}
|
||||||
|
@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</>
|
</LicenseRequired>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,17 +12,16 @@ import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import { List } from "@components/List";
|
import { List } from "@components/List";
|
||||||
|
|
||||||
export default function ApiKeyListContainer() {
|
import LicenseRequired from "../LicenseRequired";
|
||||||
|
|
||||||
|
function ApiKeyListContainer() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.useQuery(["viewer.apiKeys.list"]);
|
const query = trpc.useQuery(["viewer.apiKeys.list"]);
|
||||||
|
|
||||||
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
|
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null);
|
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null);
|
||||||
return (
|
return (
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => (
|
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
|
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
|
||||||
<div className="mt-9">
|
<div className="mt-9">
|
||||||
|
@ -35,10 +34,14 @@ export default function ApiKeyListContainer() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<LicenseRequired>
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => (
|
||||||
|
<>
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<List className="pb-6">
|
<List className="pb-6">
|
||||||
{data.map((item: any) => (
|
{data.map((item) => (
|
||||||
<ApiKeyListItem
|
<ApiKeyListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
apiKey={item}
|
apiKey={item}
|
||||||
|
@ -54,7 +57,10 @@ export default function ApiKeyListContainer() {
|
||||||
{/* New api key dialog */}
|
{/* New api key dialog */}
|
||||||
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
|
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<ApiKeyDialogForm title={t("create_api_key")} handleClose={() => setNewApiKeyModal(false)} />
|
<ApiKeyDialogForm
|
||||||
|
title={t("create_api_key")}
|
||||||
|
handleClose={() => setNewApiKeyModal(false)}
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
{/* Edit api key dialog */}
|
{/* Edit api key dialog */}
|
||||||
|
@ -73,5 +79,9 @@ export default function ApiKeyListContainer() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</LicenseRequired>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ApiKeyListContainer;
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
|
|
||||||
|
import LicenseRequired from "../LicenseRequired";
|
||||||
|
|
||||||
export default function SAMLConfiguration({
|
export default function SAMLConfiguration({
|
||||||
teamsView,
|
teamsView,
|
||||||
teamId,
|
teamId,
|
||||||
|
@ -92,7 +94,7 @@ export default function SAMLConfiguration({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isSAMLLoginEnabled ? (
|
{isSAMLLoginEnabled ? (
|
||||||
<>
|
<LicenseRequired>
|
||||||
<hr className="mt-8" />
|
<hr className="mt-8" />
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
|
||||||
|
@ -157,7 +159,7 @@ export default function SAMLConfiguration({
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<hr className="mt-4" />
|
||||||
</form>
|
</form>
|
||||||
</>
|
</LicenseRequired>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,10 +6,10 @@ import timezone from "dayjs/plugin/timezone";
|
||||||
import toArray from "dayjs/plugin/toArray";
|
import toArray from "dayjs/plugin/toArray";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
|
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import getStripe from "@calcom/stripe/client";
|
import getStripe from "@calcom/stripe/client";
|
||||||
import PaymentComponent from "@ee/components/stripe/Payment";
|
import PaymentComponent from "@ee/components/stripe/Payment";
|
||||||
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
|
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
|
||||||
|
|
|
@ -13,7 +13,6 @@ import ContactMenuItem from "./ContactMenuItem";
|
||||||
export default function HelpMenuItem() {
|
export default function HelpMenuItem() {
|
||||||
const [rating, setRating] = useState<null | string>(null);
|
const [rating, setRating] = useState<null | string>(null);
|
||||||
const [comment, setComment] = useState("");
|
const [comment, setComment] = useState("");
|
||||||
// const [errorMessage, setErrorMessage] = useState(false);
|
|
||||||
const [disableSubmit, setDisableSubmit] = useState(true);
|
const [disableSubmit, setDisableSubmit] = useState(true);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ import dayjs from "dayjs";
|
||||||
import utc from "dayjs/plugin/utc";
|
import utc from "dayjs/plugin/utc";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||||
|
|
||||||
import { trpc, inferQueryOutput } from "@lib/trpc";
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||||
|
@ -33,11 +34,12 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
}, [utils, selectedTimeZone, selectedDate]);
|
}, [utils, selectedTimeZone, selectedDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<LicenseRequired>
|
||||||
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
|
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
|
||||||
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
|
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Avatar
|
<Avatar
|
||||||
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + props.member?.username + "/avatar.png"}
|
||||||
alt={props.member?.name || ""}
|
alt={props.member?.name || ""}
|
||||||
className="h-14 w-14 rounded-full"
|
className="h-14 w-14 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
@ -61,7 +63,8 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
|
classNamePrefix="react-select"
|
||||||
|
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -73,7 +76,8 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
{ value: 60, label: "60 minutes" },
|
{ value: 60, label: "60 minutes" },
|
||||||
]}
|
]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
classNamePrefix="react-select"
|
||||||
|
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||||
/>
|
/>
|
||||||
|
@ -81,7 +85,7 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
{props.team && props.member && (
|
{props.team && props.member && (
|
||||||
<TeamAvailabilityTimes
|
<TeamAvailabilityTimes
|
||||||
className="overflow-auto"
|
className="overflow-scroll"
|
||||||
teamId={props.team.id}
|
teamId={props.team.id}
|
||||||
memberId={props.member.id}
|
memberId={props.member.id}
|
||||||
frequency={frequency}
|
frequency={frequency}
|
||||||
|
@ -90,5 +94,6 @@ export default function TeamAvailabilityModal(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</LicenseRequired>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState, useEffect, CSSProperties } from "react";
|
||||||
import AutoSizer from "react-virtualized-auto-sizer";
|
import AutoSizer from "react-virtualized-auto-sizer";
|
||||||
import { FixedSizeList as List } from "react-window";
|
import { FixedSizeList as List } from "react-window";
|
||||||
|
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
||||||
HeaderComponent={
|
HeaderComponent={
|
||||||
<div className="mb-6 flex items-center">
|
<div className="mb-6 flex items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
|
imageSrc={CAL_URL + "/" + member.username + "/avatar.png"}
|
||||||
alt={member?.name || ""}
|
alt={member?.name || ""}
|
||||||
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
|
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { Button } from "@calcom/ui/Button";
|
||||||
import { useContracts } from "../../../contexts/contractsContext";
|
import { useContracts } from "../../../contexts/contractsContext";
|
||||||
import genericAbi from "../../../web3/abis/abiWithGetBalance.json";
|
import genericAbi from "../../../web3/abis/abiWithGetBalance.json";
|
||||||
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
|
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
|
||||||
|
import { withLicenseRequired } from "../LicenseRequired";
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
ethereum: AbstractProvider & { selectedAddress: string };
|
ethereum: AbstractProvider & { selectedAddress: string };
|
||||||
|
@ -150,4 +151,4 @@ const CryptoSection = (props: CryptoSectionProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CryptoSection;
|
export default withLicenseRequired(CryptoSection);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { PaymentType, Prisma } from "@prisma/client";
|
import { PaymentType, Prisma } from "@prisma/client";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
|
@ -17,8 +18,15 @@ export type PaymentInfo = {
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
let paymentFeePercentage: number | undefined;
|
const stripeKeysSchema = z.object({
|
||||||
let paymentFeeFixed: number | undefined;
|
payment_fee_fixed: z.number(),
|
||||||
|
payment_fee_percentage: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const stripeCredentialSchema = z.object({
|
||||||
|
stripe_user_id: z.string(),
|
||||||
|
stripe_publishable_key: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export async function handlePayment(
|
export async function handlePayment(
|
||||||
evt: CalendarEvent,
|
evt: CalendarEvent,
|
||||||
|
@ -35,13 +43,10 @@ export async function handlePayment(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const appKeys = await getAppKeysFromSlug("stripe");
|
const appKeys = await getAppKeysFromSlug("stripe");
|
||||||
if (typeof appKeys.payment_fee_fixed === "number") paymentFeePercentage = appKeys.payment_fee_fixed;
|
const { payment_fee_fixed, payment_fee_percentage } = stripeKeysSchema.parse(appKeys);
|
||||||
if (typeof appKeys.payment_fee_percentage === "number") paymentFeeFixed = appKeys.payment_fee_percentage;
|
|
||||||
|
|
||||||
const paymentFee = Math.round(
|
const paymentFee = Math.round(selectedEventType.price * payment_fee_percentage + payment_fee_fixed);
|
||||||
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
|
const { stripe_user_id, stripe_publishable_key } = stripeCredentialSchema.parse(stripeCredential.key);
|
||||||
);
|
|
||||||
const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;
|
|
||||||
|
|
||||||
const params: Stripe.PaymentIntentCreateParams = {
|
const params: Stripe.PaymentIntentCreateParams = {
|
||||||
amount: selectedEventType.price,
|
amount: selectedEventType.price,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { useRouter } from "next/router";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
|
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||||
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
|
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
|
||||||
|
|
||||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||||
|
@ -49,6 +50,7 @@ export function TeamAvailabilityPage() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
|
<LicenseRequired>
|
||||||
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
|
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
|
||||||
{isLoading && <Loader />}
|
{isLoading && <Loader />}
|
||||||
{isFreeUser ? (
|
{isFreeUser ? (
|
||||||
|
@ -60,6 +62,7 @@ export function TeamAvailabilityPage() {
|
||||||
) : (
|
) : (
|
||||||
TeamAvailability
|
TeamAvailability
|
||||||
)}
|
)}
|
||||||
|
</LicenseRequired>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { ErrorInfo } from "react";
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component<
|
||||||
|
{ children: React.ReactNode },
|
||||||
|
{ error: Error | null; errorInfo: ErrorInfo | null }
|
||||||
|
> {
|
||||||
|
constructor(props: { children: React.ReactNode } | Readonly<{ children: React.ReactNode }>) {
|
||||||
|
super(props);
|
||||||
|
this.state = { error: null, errorInfo: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch?(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// Catch errors in any components below and re-render with error message
|
||||||
|
this.setState({ error, errorInfo });
|
||||||
|
// You can also log error messages to an error reporting service here
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.errorInfo) {
|
||||||
|
// Error path
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Something went wrong.</h2>
|
||||||
|
<details style={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{this.state.error && this.state.error.toString()}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Normally, just render children
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
|
@ -16,7 +16,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.Rea
|
||||||
));
|
));
|
||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = NextAppProps & {
|
export type AppProps = Omit<NextAppProps, "Component"> & {
|
||||||
|
Component: NextAppProps["Component"] & { requiresLicense?: boolean };
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
err?: Error;
|
err?: Error;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calend
|
||||||
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
|
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
|
||||||
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
|
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
|
||||||
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
|
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
|
||||||
|
import AttendeeLocationChangeEmail from "@lib/emails/templates/attendee-location-change-email";
|
||||||
import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email";
|
import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email";
|
||||||
import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email";
|
import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email";
|
||||||
import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
|
import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
|
||||||
|
@ -10,6 +11,7 @@ import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-ema
|
||||||
import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email";
|
import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email";
|
||||||
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
|
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
|
||||||
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email";
|
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email";
|
||||||
|
import OrganizerLocationChangeEmail from "@lib/emails/templates/organizer-location-change-email";
|
||||||
import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email";
|
import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email";
|
||||||
import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email";
|
import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email";
|
||||||
import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email";
|
import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email";
|
||||||
|
@ -268,6 +270,38 @@ export const sendRequestRescheduleEmail = async (
|
||||||
await Promise.all(emailsToSend);
|
await Promise.all(emailsToSend);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendLocationChangeEmails = async (
|
||||||
|
calEvent: CalendarEvent,
|
||||||
|
recurringEvent: RecurringEvent = {}
|
||||||
|
) => {
|
||||||
|
const emailsToSend: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
emailsToSend.push(
|
||||||
|
...calEvent.attendees.map((attendee) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee, recurringEvent);
|
||||||
|
resolve(scheduledEmail.sendEmail());
|
||||||
|
} catch (e) {
|
||||||
|
reject(console.error("AttendeeLocationChangeEmail.sendEmail failed", e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
emailsToSend.push(
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const scheduledEmail = new OrganizerLocationChangeEmail(calEvent, recurringEvent);
|
||||||
|
resolve(scheduledEmail.sendEmail());
|
||||||
|
} catch (e) {
|
||||||
|
reject(console.error("OrganizerLocationChangeEmail.sendEmail failed", e));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(emailsToSend);
|
||||||
|
};
|
||||||
export const sendFeedbackEmail = async (feedback: Feedback) => {
|
export const sendFeedbackEmail = async (feedback: Feedback) => {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import toArray from "dayjs/plugin/toArray";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
|
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||||
|
|
||||||
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
import {
|
||||||
|
emailHead,
|
||||||
|
emailSchedulingBodyHeader,
|
||||||
|
emailBodyLogo,
|
||||||
|
emailScheduledBodyHeaderContent,
|
||||||
|
emailSchedulingBodyDivider,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
dayjs.extend(toArray);
|
||||||
|
|
||||||
|
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
||||||
|
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
icalEvent: {
|
||||||
|
filename: "event.ics",
|
||||||
|
content: this.getiCalEventAsString(),
|
||||||
|
},
|
||||||
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
replyTo: this.calEvent.organizer.email,
|
||||||
|
subject: `${this.attendee.language.translate("location_changed_event_type_subject", {
|
||||||
|
eventType: this.calEvent.type,
|
||||||
|
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||||
|
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
|
||||||
|
"h:mma"
|
||||||
|
)}, ${this.attendee.language.translate(
|
||||||
|
this.getInviteeStart().format("dddd").toLowerCase()
|
||||||
|
)}, ${this.attendee.language.translate(
|
||||||
|
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||||
|
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||||
|
})}`,
|
||||||
|
html: this.getHtmlBody(),
|
||||||
|
text: this.getTextBody(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
protected getTextBody(): string {
|
||||||
|
// Only the original attendee can make changes to the event
|
||||||
|
// Guests cannot
|
||||||
|
if (this.attendee === this.calEvent.attendees[0]) {
|
||||||
|
return `
|
||||||
|
${this.attendee.language.translate("event_location_changed")}
|
||||||
|
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||||
|
${this.getWhat()}
|
||||||
|
${this.getWhen()}
|
||||||
|
${this.getLocation()}
|
||||||
|
${this.getDescription()}
|
||||||
|
${this.getAdditionalNotes()}
|
||||||
|
${this.attendee.language.translate("need_to_reschedule_or_cancel")}
|
||||||
|
${getCancelLink(this.calEvent)}
|
||||||
|
`.replace(/(<([^>]+)>)/gi, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
${this.attendee.language.translate("event_location_changed")}
|
||||||
|
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||||
|
${this.getWhat()}
|
||||||
|
${this.getWhen()}
|
||||||
|
${this.getLocation()}
|
||||||
|
${this.getAdditionalNotes()}
|
||||||
|
`.replace(/(<([^>]+)>)/gi, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getHtmlBody(): string {
|
||||||
|
const headerContent = this.attendee.language.translate("location_changed_event_type_subject", {
|
||||||
|
eventType: this.calEvent.type,
|
||||||
|
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||||
|
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
|
||||||
|
"h:mma"
|
||||||
|
)}, ${this.attendee.language.translate(
|
||||||
|
this.getInviteeStart().format("dddd").toLowerCase()
|
||||||
|
)}, ${this.attendee.language.translate(
|
||||||
|
this.getInviteeStart().format("MMMM").toLowerCase()
|
||||||
|
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
${emailHead(headerContent)}
|
||||||
|
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||||
|
<div style="background-color:#F5F5F5;">
|
||||||
|
${emailSchedulingBodyHeader("calendarCircle")}
|
||||||
|
${emailScheduledBodyHeaderContent(
|
||||||
|
this.attendee.language.translate("event_location_changed"),
|
||||||
|
this.attendee.language.translate("emailed_you_and_any_other_attendees")
|
||||||
|
)}
|
||||||
|
${emailSchedulingBodyDivider()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.getWhat()}
|
||||||
|
${this.getWhen()}
|
||||||
|
${this.getWho()}
|
||||||
|
${this.getLocation()}
|
||||||
|
${this.getDescription()}
|
||||||
|
${this.getAdditionalNotes()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${emailSchedulingBodyDivider()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.getManageLink()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${emailBodyLogo()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,6 +102,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_attendee"
|
||||||
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
|
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
|
||||||
organizer: this.calEvent.organizer.name,
|
organizer: this.calEvent.organizer.name,
|
||||||
})},
|
})},
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getAdditionalNotes()}
|
${this.getAdditionalNotes()}
|
||||||
|
@ -151,6 +152,7 @@ ${getCancelLink(this.calEvent)}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getWho()}
|
${this.getWho()}
|
||||||
|
|
|
@ -52,6 +52,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||||
return `
|
return `
|
||||||
${this.attendee.language.translate("event_has_been_rescheduled")}
|
${this.attendee.language.translate("event_has_been_rescheduled")}
|
||||||
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getLocation()}
|
${this.getLocation()}
|
||||||
|
@ -112,6 +113,7 @@ ${this.getCustomInputs()}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getWho()}
|
${this.getWho()}
|
||||||
|
|
|
@ -452,4 +452,12 @@ ${getRichDescription(this.calEvent)}
|
||||||
protected getInviteeEnd(): Dayjs {
|
protected getInviteeEnd(): Dayjs {
|
||||||
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getReason(): string {
|
||||||
|
return `
|
||||||
|
<div style="line-height: 6px; margin-bottom: 24px;">
|
||||||
|
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
|
||||||
|
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
|
import timezone from "dayjs/plugin/timezone";
|
||||||
|
import toArray from "dayjs/plugin/toArray";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
|
||||||
|
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||||
|
|
||||||
|
import {
|
||||||
|
emailHead,
|
||||||
|
emailSchedulingBodyHeader,
|
||||||
|
emailBodyLogo,
|
||||||
|
emailScheduledBodyHeaderContent,
|
||||||
|
emailSchedulingBodyDivider,
|
||||||
|
} from "./common";
|
||||||
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(localizedFormat);
|
||||||
|
dayjs.extend(toArray);
|
||||||
|
|
||||||
|
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
||||||
|
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||||
|
const toAddresses = [this.calEvent.organizer.email];
|
||||||
|
if (this.calEvent.team) {
|
||||||
|
this.calEvent.team.members.forEach((member) => {
|
||||||
|
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
|
||||||
|
if (memberAttendee) {
|
||||||
|
toAddresses.push(memberAttendee.email);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icalEvent: {
|
||||||
|
filename: "event.ics",
|
||||||
|
content: this.getiCalEventAsString(),
|
||||||
|
},
|
||||||
|
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||||
|
to: toAddresses.join(","),
|
||||||
|
subject: `${this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
|
||||||
|
eventType: this.calEvent.type,
|
||||||
|
name: this.calEvent.attendees[0].name,
|
||||||
|
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
|
||||||
|
"h:mma"
|
||||||
|
)}, ${this.calEvent.organizer.language.translate(
|
||||||
|
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||||
|
)}, ${this.calEvent.organizer.language.translate(
|
||||||
|
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||||
|
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||||
|
})}`,
|
||||||
|
html: this.getHtmlBody(),
|
||||||
|
text: this.getTextBody(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getTextBody(): string {
|
||||||
|
return `
|
||||||
|
${this.calEvent.organizer.language.translate("event_location_changed")}
|
||||||
|
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
|
||||||
|
${this.getWhat()}
|
||||||
|
${this.getWhen()}
|
||||||
|
${this.getLocation()}
|
||||||
|
${this.getDescription()}
|
||||||
|
${this.getAdditionalNotes()}
|
||||||
|
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
|
||||||
|
${getCancelLink(this.calEvent)}
|
||||||
|
`.replace(/(<([^>]+)>)/gi, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getHtmlBody(): string {
|
||||||
|
const headerContent = this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
|
||||||
|
eventType: this.calEvent.type,
|
||||||
|
name: this.calEvent.attendees[0].name,
|
||||||
|
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
|
||||||
|
"h:mma"
|
||||||
|
)}, ${this.calEvent.organizer.language.translate(
|
||||||
|
this.getOrganizerStart().format("dddd").toLowerCase()
|
||||||
|
)}, ${this.calEvent.organizer.language.translate(
|
||||||
|
this.getOrganizerStart().format("MMMM").toLowerCase()
|
||||||
|
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
${emailHead(headerContent)}
|
||||||
|
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||||
|
<div style="background-color:#F5F5F5;">
|
||||||
|
${emailSchedulingBodyHeader("calendarCircle")}
|
||||||
|
${emailScheduledBodyHeaderContent(
|
||||||
|
this.calEvent.organizer.language.translate("event_location_changed"),
|
||||||
|
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
|
||||||
|
)}
|
||||||
|
${emailSchedulingBodyDivider()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.getWhat()}
|
||||||
|
${this.getWhen()}
|
||||||
|
${this.getWho()}
|
||||||
|
${this.getLocation()}
|
||||||
|
${this.getDescription()}
|
||||||
|
${this.getAdditionalNotes()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${emailSchedulingBodyDivider()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.getManageLink()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${emailBodyLogo()}
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,6 +111,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_organizer
|
||||||
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
||||||
attendee: this.calEvent.attendees[0].name,
|
attendee: this.calEvent.attendees[0].name,
|
||||||
})},
|
})},
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getLocation()}
|
${this.getLocation()}
|
||||||
|
@ -163,6 +164,7 @@ ${getCancelLink(this.calEvent)}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getWho()}
|
${this.getWho()}
|
||||||
|
|
|
@ -59,6 +59,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||||
return `
|
return `
|
||||||
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
|
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
|
||||||
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
|
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getLocation()}
|
${this.getLocation()}
|
||||||
|
@ -108,6 +109,7 @@ ${getCancelLink(this.calEvent)}
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||||
|
${this.calEvent.cancellationReason && this.getReason()}
|
||||||
${this.getWhat()}
|
${this.getWhat()}
|
||||||
${this.getWhen()}
|
${this.getWhen()}
|
||||||
${this.getWho()}
|
${this.getWho()}
|
||||||
|
|
|
@ -439,4 +439,12 @@ ${getRichDescription(this.calEvent)}
|
||||||
protected getOrganizerEnd(): Dayjs {
|
protected getOrganizerEnd(): Dayjs {
|
||||||
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getReason(): string {
|
||||||
|
return `
|
||||||
|
<div style="line-height: 6px; margin-bottom: 24px;">
|
||||||
|
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
|
||||||
|
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useEmbedTheme } from "@calcom/embed-core";
|
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
|
||||||
|
|
||||||
import { Maybe } from "@trpc/server";
|
import { Maybe } from "@trpc/server";
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import { LocationType } from "./location";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function to translate booking location value to a readable string
|
||||||
|
* @param linkValue
|
||||||
|
* @param translationFunction
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const linkValueToString = (
|
||||||
|
linkValue: string | undefined | null,
|
||||||
|
translationFunction: TFunction
|
||||||
|
): string => {
|
||||||
|
const t = translationFunction;
|
||||||
|
if (!linkValue) {
|
||||||
|
return translationFunction("no_location");
|
||||||
|
}
|
||||||
|
switch (linkValue) {
|
||||||
|
case LocationType.InPerson:
|
||||||
|
return t("in_person_meeting");
|
||||||
|
case LocationType.UserPhone:
|
||||||
|
return t("user_phone");
|
||||||
|
case LocationType.GoogleMeet:
|
||||||
|
return `Google Meet: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Zoom:
|
||||||
|
return `Zoom: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Daily:
|
||||||
|
return `Cal Video: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Jitsi:
|
||||||
|
return `Jitsi: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Huddle01:
|
||||||
|
return `Huddle01t: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Tandem:
|
||||||
|
return `Tandem: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
case LocationType.Teams:
|
||||||
|
return `Teams: ${t("meeting_url_in_conformation_email")}`;
|
||||||
|
default:
|
||||||
|
return linkValue || "";
|
||||||
|
}
|
||||||
|
};
|
|
@ -2,6 +2,12 @@ import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import { LocationType } from "./location";
|
import { LocationType } from "./location";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this function for translating event location to a readable string
|
||||||
|
* @param location
|
||||||
|
* @param t
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
export const LocationOptionsToString = (location: string, t: TFunction) => {
|
export const LocationOptionsToString = (location: string, t: TFunction) => {
|
||||||
switch (location) {
|
switch (location) {
|
||||||
case LocationType.InPerson:
|
case LocationType.InPerson:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "1.6.1",
|
"version": "1.6.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
@ -76,6 +76,7 @@
|
||||||
"jimp": "^0.16.1",
|
"jimp": "^0.16.1",
|
||||||
"libphonenumber-js": "^1.9.53",
|
"libphonenumber-js": "^1.9.53",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^9.3.4",
|
"micro": "^9.3.4",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^12.1.6",
|
"next": "^12.1.6",
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
"@types/glidejs__glide": "^3.4.2",
|
"@types/glidejs__glide": "^3.4.2",
|
||||||
"@types/jest": "^27.5.1",
|
"@types/jest": "^27.5.1",
|
||||||
"@types/lodash": "^4.14.182",
|
"@types/lodash": "^4.14.182",
|
||||||
|
"@types/memory-cache": "^0.2.2",
|
||||||
"@types/micro": "7.3.6",
|
"@types/micro": "7.3.6",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/module-alias": "^2.0.1",
|
"@types/module-alias": "^2.0.1",
|
||||||
|
|
|
@ -10,7 +10,12 @@ import { useEffect, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
|
import {
|
||||||
|
sdkActionManager,
|
||||||
|
useEmbedNonStylesConfig,
|
||||||
|
useEmbedStyles,
|
||||||
|
useIsEmbed,
|
||||||
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import defaultEvents, {
|
import defaultEvents, {
|
||||||
getDynamicEventDescription,
|
getDynamicEventDescription,
|
||||||
getGroupName,
|
getGroupName,
|
||||||
|
@ -337,11 +342,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
weekStart: "Sunday",
|
weekStart: "Sunday",
|
||||||
brandColor: "",
|
brandColor: "",
|
||||||
darkBrandColor: "",
|
darkBrandColor: "",
|
||||||
allowDynamicBooking: users.some((user) => {
|
allowDynamicBooking: !users.some((user) => {
|
||||||
return !user.allowDynamicBooking;
|
return !user.allowDynamicBooking;
|
||||||
})
|
}),
|
||||||
? false
|
|
||||||
: true,
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: user.name || user.username,
|
name: user.name || user.username,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { UserPlan } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
|
import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
|
||||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
|
@ -307,11 +307,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
weekStart: "Sunday",
|
weekStart: "Sunday",
|
||||||
brandColor: "",
|
brandColor: "",
|
||||||
darkBrandColor: "",
|
darkBrandColor: "",
|
||||||
allowDynamicBooking: users.some((user) => {
|
allowDynamicBooking: !users.some((user) => {
|
||||||
return !user.allowDynamicBooking;
|
return !user.allowDynamicBooking;
|
||||||
})
|
}),
|
||||||
? false
|
|
||||||
: true,
|
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: user.name || user.username,
|
name: user.name || user.username,
|
||||||
|
|
|
@ -200,11 +200,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
theme: null,
|
theme: null,
|
||||||
brandColor: "",
|
brandColor: "",
|
||||||
darkBrandColor: "",
|
darkBrandColor: "",
|
||||||
allowDynamicBooking: users.some((user) => {
|
allowDynamicBooking: !users.some((user) => {
|
||||||
return !user.allowDynamicBooking;
|
return !user.allowDynamicBooking;
|
||||||
})
|
}),
|
||||||
? false
|
|
||||||
: true,
|
|
||||||
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
|
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Head from "next/head";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import LicenseRequired from "@ee/components/LicenseRequired";
|
||||||
|
|
||||||
import AppProviders, { AppProps } from "@lib/app-providers";
|
import AppProviders, { AppProps } from "@lib/app-providers";
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
@ -37,7 +38,13 @@ function MyApp(props: AppProps) {
|
||||||
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
|
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
|
||||||
<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>
|
||||||
|
{Component.requiresLicense ? (
|
||||||
|
<LicenseRequired>
|
||||||
<Component {...pageProps} err={err} />
|
<Component {...pageProps} err={err} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
)}
|
||||||
</AppProviders>
|
</AppProviders>
|
||||||
</ContractsProvider>
|
</ContractsProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import nodemailer, { TransportOptions } from "nodemailer";
|
||||||
import { authenticator } from "otplib";
|
import { authenticator } from "otplib";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import checkLicense from "@calcom/ee/server/checkLicense";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_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";
|
||||||
|
@ -276,8 +277,10 @@ export default NextAuth({
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
|
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
|
||||||
const calendsoSession: Session = {
|
const calendsoSession: Session = {
|
||||||
...session,
|
...session,
|
||||||
|
hasValidLicense,
|
||||||
user: {
|
user: {
|
||||||
...session.user,
|
...session.user,
|
||||||
id: token.id as number,
|
id: token.id as number,
|
||||||
|
|
|
@ -201,7 +201,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||||
|
|
||||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||||
|
|
||||||
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
|
type ExtendedBookingCreateBody = BookingCreateBody & {
|
||||||
|
noEmail?: boolean;
|
||||||
|
recurringCount?: number;
|
||||||
|
rescheduleReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
|
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
|
||||||
|
@ -677,7 +681,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
if (originalRescheduledBooking?.uid) {
|
if (originalRescheduledBooking?.uid) {
|
||||||
// Use EventManager to conditionally use all needed integrations.
|
// Use EventManager to conditionally use all needed integrations.
|
||||||
const updateManager = await eventManager.update(evt, originalRescheduledBooking.uid, booking?.id);
|
const updateManager = await eventManager.update(
|
||||||
|
evt,
|
||||||
|
originalRescheduledBooking.uid,
|
||||||
|
booking?.id,
|
||||||
|
reqBody.rescheduleReason
|
||||||
|
);
|
||||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||||
// to the default description when we are sending the emails.
|
// to the default description when we are sending the emails.
|
||||||
evt.description = eventType.description;
|
evt.description = eventType.description;
|
||||||
|
@ -711,7 +720,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
{
|
{
|
||||||
...evt,
|
...evt,
|
||||||
additionInformation: metadata,
|
additionInformation: metadata,
|
||||||
additionalNotes, // Resets back to the addtionalNote input and not the overriden value
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||||
|
cancellationReason: reqBody.rescheduleReason,
|
||||||
},
|
},
|
||||||
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
|
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { ClipboardIcon } from "@heroicons/react/solid";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getWorkingHours } from "@lib/availability";
|
import { getWorkingHours } from "@lib/availability";
|
||||||
import { GetBookingType } from "@lib/getBooking";
|
import { GetBookingType } from "@lib/getBooking";
|
||||||
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location";
|
import { locationHiddenFilter, LocationObject } from "@lib/location";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { z } from "zod";
|
||||||
|
|
||||||
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
||||||
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
|
||||||
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { StripeData } from "@calcom/stripe/server";
|
import { StripeData } from "@calcom/stripe/server";
|
||||||
|
@ -64,6 +65,7 @@ import Shell from "@components/Shell";
|
||||||
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
||||||
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import RecurringEventController from "@components/eventtype/RecurringEventController";
|
import RecurringEventController from "@components/eventtype/RecurringEventController";
|
||||||
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
|
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
|
@ -72,7 +74,6 @@ import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
||||||
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
||||||
import MinutesField from "@components/ui/form/MinutesField";
|
import MinutesField from "@components/ui/form/MinutesField";
|
||||||
import PhoneInput from "@components/ui/form/PhoneInput";
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
import * as RadioArea from "@components/ui/form/radio-area";
|
import * as RadioArea from "@components/ui/form/radio-area";
|
||||||
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
import WebhookListContainer from "@components/webhook/WebhookListContainer";
|
||||||
|
@ -179,7 +180,7 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
|
||||||
}}
|
}}
|
||||||
readOnly={proUpgradeRequired}
|
readOnly={proUpgradeRequired}
|
||||||
type="url"
|
type="url"
|
||||||
className=" block w-full rounded-sm border-gray-300 sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 sm:text-sm"
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
|
@ -323,10 +324,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
);
|
);
|
||||||
const [tokensList, setTokensList] = useState<Array<Token>>([]);
|
const [tokensList, setTokensList] = useState<Array<Token>>([]);
|
||||||
|
|
||||||
const defaultSeats = 2;
|
const defaultSeatsPro = 6;
|
||||||
const defaultSeatsInput = 6;
|
const minSeats = 2;
|
||||||
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
|
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
|
||||||
const [inputSeatNumber, setInputSeatNumber] = useState(eventType.seatsPerTimeSlot! >= defaultSeatsInput);
|
|
||||||
|
|
||||||
const periodType =
|
const periodType =
|
||||||
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
||||||
|
@ -422,134 +422,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
|
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
setShowLocationModal(false);
|
||||||
|
|
||||||
const LocationOptions = () => {
|
|
||||||
if (!selectedLocation) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (selectedLocation.value) {
|
|
||||||
case LocationType.InPerson:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_address_place")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationAddress")}
|
|
||||||
id="address"
|
|
||||||
required
|
|
||||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
|
||||||
defaultValue={
|
|
||||||
formMethods
|
|
||||||
.getValues("locations")
|
|
||||||
.find((location) => location.type === LocationType.InPerson)?.address
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CheckboxField
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) => onChange(e.target.checked)}
|
|
||||||
infomationIconText={t("display_location_info_badge")}></CheckboxField>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case LocationType.Link:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_link_meeting")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...locationFormMethods.register("locationLink")}
|
|
||||||
id="address"
|
|
||||||
required
|
|
||||||
className=" block w-full rounded-sm border-gray-300 sm:text-sm"
|
|
||||||
defaultValue={
|
|
||||||
formMethods.getValues("locations").find((location) => location.type === LocationType.Link)
|
|
||||||
?.link
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{locationFormMethods.formState.errors.locationLink && (
|
|
||||||
<p className="mt-1 text-red-500">
|
|
||||||
{locationFormMethods.formState.errors.locationLink.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<Controller
|
|
||||||
name="displayLocationPublicly"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CheckboxField
|
|
||||||
description={t("display_location_label")}
|
|
||||||
onChange={(e) => onChange(e.target.checked)}
|
|
||||||
infomationIconText={t("display_location_info_badge")}></CheckboxField>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case LocationType.UserPhone:
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("set_your_phone_number")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<PhoneInput
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
name="locationPhoneNumber"
|
|
||||||
required
|
|
||||||
id="locationPhoneNumber"
|
|
||||||
placeholder={t("host_phone_number")}
|
|
||||||
rules={{}}
|
|
||||||
defaultValue={
|
|
||||||
formMethods
|
|
||||||
.getValues("locations")
|
|
||||||
.find((location) => location.type === LocationType.UserPhone)?.hostPhoneNumber
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{locationFormMethods.formState.errors.locationPhoneNumber && (
|
|
||||||
<p className="mt-1 text-red-500">
|
|
||||||
{locationFormMethods.formState.errors.locationPhoneNumber.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case LocationType.Phone:
|
|
||||||
return <p className="text-sm">{t("cal_invitee_phone_number_scheduling")}</p>;
|
|
||||||
/* TODO: Render this dynamically from App Store */
|
|
||||||
case LocationType.GoogleMeet:
|
|
||||||
return <p className="text-sm">{t("cal_provide_google_meet_location")}</p>;
|
|
||||||
case LocationType.Zoom:
|
|
||||||
return <p className="text-sm">{t("cal_provide_zoom_meeting_url")}</p>;
|
|
||||||
case LocationType.Daily:
|
|
||||||
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
|
|
||||||
case LocationType.Jitsi:
|
|
||||||
return <p className="text-sm">{t("cal_provide_jitsi_meeting_url")}</p>;
|
|
||||||
case LocationType.Huddle01:
|
|
||||||
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
|
||||||
case LocationType.Tandem:
|
|
||||||
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
|
|
||||||
case LocationType.Teams:
|
|
||||||
return <p className="text-sm">{t("cal_provide_teams_meeting_url")}</p>;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
const removeCustom = (index: number) => {
|
||||||
|
@ -580,11 +453,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
team ? `team/${team.slug}` : eventType.users[0].username
|
eventType.slug
|
||||||
}/${eventType.slug}`;
|
}`;
|
||||||
|
|
||||||
const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const mapUserToValue = ({
|
const mapUserToValue = ({
|
||||||
id,
|
id,
|
||||||
|
@ -597,7 +470,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
}) => ({
|
}) => ({
|
||||||
value: `${id || ""}`,
|
value: `${id || ""}`,
|
||||||
label: `${name || ""}`,
|
label: `${name || ""}`,
|
||||||
avatar: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
|
@ -641,7 +514,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<Select
|
<Select
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e?.value) {
|
if (e?.value) {
|
||||||
const newLocationType: LocationType = e.value;
|
const newLocationType: LocationType = e.value;
|
||||||
|
@ -669,7 +542,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="flex flex-grow items-center">
|
<div className="flex flex-grow items-center">
|
||||||
<LocationMarkerIcon className="h-6 w-6" />
|
<LocationMarkerIcon className="h-6 w-6" />
|
||||||
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
|
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
|
||||||
{location.link}
|
{location.address}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1081,7 +954,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex rounded-sm">
|
<div className="flex rounded-sm">
|
||||||
<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">
|
<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?.replace(/^(https?:|)\/\//, "")}/
|
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
||||||
{team ? "team/" + team.slug : eventType.users[0].username}/
|
{team ? "team/" + team.slug : eventType.users[0].username}/
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
|
@ -1089,7 +962,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
id="slug"
|
id="slug"
|
||||||
aria-labelledby="slug-label"
|
aria-labelledby="slug-label"
|
||||||
required
|
required
|
||||||
className=" block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
className="block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
|
||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
{...formMethods.register("slug", {
|
{...formMethods.register("slug", {
|
||||||
setValueAs: (v) => slugify(v),
|
setValueAs: (v) => slugify(v),
|
||||||
|
@ -1155,7 +1028,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<textarea
|
<textarea
|
||||||
id="description"
|
id="description"
|
||||||
className=" block w-full rounded-sm border-gray-300 text-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm "
|
||||||
placeholder={t("quick_video_meeting")}
|
placeholder={t("quick_video_meeting")}
|
||||||
{...formMethods.register("description")}
|
{...formMethods.register("description")}
|
||||||
defaultValue={asStringOrUndefined(eventType.description)}></textarea>
|
defaultValue={asStringOrUndefined(eventType.description)}></textarea>
|
||||||
|
@ -1312,7 +1185,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
<div className="relative mt-1 rounded-sm">
|
<div className="relative mt-1 rounded-sm">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className=" block w-full rounded-sm border-gray-300 text-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm "
|
||||||
placeholder={t("meeting_with_user")}
|
placeholder={t("meeting_with_user")}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
|
@ -1334,7 +1207,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
{
|
{
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className=" block w-full rounded-sm border-gray-300 text-sm"
|
className="block w-full rounded-sm border-gray-300 text-sm "
|
||||||
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
|
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
|
||||||
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
|
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
|
||||||
{...formMethods.register("smartContractAddress")}
|
{...formMethods.register("smartContractAddress")}
|
||||||
|
@ -1439,9 +1312,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="requiresConfirmation"
|
name="requiresConfirmation"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.requiresConfirmation}
|
defaultValue={eventType.requiresConfirmation}
|
||||||
render={() => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
id="requiresConfirmation"
|
id="requiresConfirmation"
|
||||||
descriptionAsLabel
|
descriptionAsLabel
|
||||||
|
@ -1450,10 +1322,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
description={t("opt_in_booking_description")}
|
description={t("opt_in_booking_description")}
|
||||||
defaultChecked={eventType.requiresConfirmation}
|
defaultChecked={eventType.requiresConfirmation}
|
||||||
disabled={enableSeats}
|
disabled={enableSeats}
|
||||||
checked={formMethods.watch("disableGuests")}
|
checked={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => onChange(e?.target.checked)}
|
||||||
formMethods.setValue("requiresConfirmation", e?.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -1518,7 +1388,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
name="hashedLink"
|
name="hashedLink"
|
||||||
data-testid="generated-hash-url"
|
data-testid="generated-hash-url"
|
||||||
type="text"
|
type="text"
|
||||||
className=" grow select-none border-gray-300 bg-gray-50 text-sm text-gray-500 ltr:rounded-l-sm rtl:rounded-r-sm"
|
className="grow select-none border-gray-300 bg-gray-50 text-sm text-gray-500 ltr:rounded-l-sm rtl:rounded-r-sm"
|
||||||
defaultValue={placeholderHashedLink}
|
defaultValue={placeholderHashedLink}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -1654,7 +1524,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
id=""
|
id=""
|
||||||
className=" block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
{...formMethods.register("periodCountCalendarDays")}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||||
<option value="1">{t("calendar_days")}</option>
|
<option value="1">{t("calendar_days")}</option>
|
||||||
|
@ -1728,7 +1598,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
|
@ -1766,7 +1636,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
|
@ -1802,7 +1672,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
if (e?.target.checked) {
|
if (e?.target.checked) {
|
||||||
setEnableSeats(true);
|
setEnableSeats(true);
|
||||||
// Want to disable individuals from taking multiple seats
|
// Want to disable individuals from taking multiple seats
|
||||||
formMethods.setValue("seatsPerTimeSlot", defaultSeats);
|
formMethods.setValue("seatsPerTimeSlot", defaultSeatsPro);
|
||||||
formMethods.setValue("disableGuests", true);
|
formMethods.setValue("disableGuests", true);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1860,14 +1730,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="focus:border-primary-500 focus:ring-primary-500 py- block w-20 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
className="focus:border-primary-500 focus:ring-primary-500 py- block w-20 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
||||||
placeholder={`${defaultSeatsInput}`}
|
placeholder={`${defaultSeatsPro}`}
|
||||||
|
min={minSeats}
|
||||||
{...formMethods.register("seatsPerTimeSlot", {
|
{...formMethods.register("seatsPerTimeSlot", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
min: defaultSeatsInput,
|
|
||||||
})}
|
})}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
eventType.seatsPerTimeSlot || defaultSeatsInput
|
eventType.seatsPerTimeSlot || defaultSeatsPro
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1883,25 +1753,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
classNamePrefix="react-select"
|
classNamePrefix="react-select"
|
||||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-auto rounded-sm border border-gray-300 sm:text-sm "
|
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-auto rounded-sm border border-gray-300 sm:text-sm "
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val!.value === -1) {
|
if (!val) {
|
||||||
formMethods.setValue(
|
return;
|
||||||
"seatsPerTimeSlot",
|
}
|
||||||
defaultSeatsInput
|
if (val.value === -1) {
|
||||||
);
|
formMethods.setValue("seatsPerTimeSlot", minSeats);
|
||||||
setInputSeatNumber(true);
|
|
||||||
} else {
|
} else {
|
||||||
setInputSeatNumber(false);
|
formMethods.setValue("seatsPerTimeSlot", val.value);
|
||||||
formMethods.setValue(
|
|
||||||
"seatsPerTimeSlot",
|
|
||||||
val!.value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
defaultValue={{
|
defaultValue={{
|
||||||
value: eventType.seatsPerTimeSlot || defaultSeats,
|
value: eventType.seatsPerTimeSlot || minSeats,
|
||||||
label: `${
|
label: `${eventType.seatsPerTimeSlot || minSeats}`,
|
||||||
eventType.seatsPerTimeSlot || defaultSeats
|
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
options={selectSeatsPerTimeSlotOptions}
|
options={selectSeatsPerTimeSlotOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -1992,12 +1855,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
min="0.5"
|
min="0.5"
|
||||||
type="number"
|
type="number"
|
||||||
required
|
required
|
||||||
className=" block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm"
|
className="block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm"
|
||||||
placeholder="Price"
|
placeholder="Price"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e.target.valueAsNumber * 100);
|
field.onChange(e.target.valueAsNumber * 100);
|
||||||
}}
|
}}
|
||||||
value={field.value > 0 ? field.value / 100 : 0}
|
value={field.value > 0 ? field.value / 100 : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -2145,78 +2008,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={showLocationModal} onOpenChange={setShowLocationModal}>
|
<EditLocationDialog
|
||||||
<DialogContent asChild>
|
isOpenDialog={showLocationModal}
|
||||||
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
setShowLocationModal={setShowLocationModal}
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
saveLocation={addLocation}
|
||||||
<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">
|
defaultValues={formMethods.getValues("locations")}
|
||||||
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
|
selection={
|
||||||
</div>
|
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
|
||||||
{t("edit_location")}
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Form
|
|
||||||
form={locationFormMethods}
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
|
||||||
let details = {};
|
|
||||||
if (newLocation === LocationType.InPerson) {
|
|
||||||
details = {
|
|
||||||
address: values.locationAddress,
|
|
||||||
displayLocationPublicly,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (newLocation === LocationType.Link) {
|
setSelectedLocation={setSelectedLocation}
|
||||||
details = { link: values.locationLink, displayLocationPublicly };
|
|
||||||
}
|
|
||||||
if (newLocation === LocationType.UserPhone) {
|
|
||||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
|
||||||
}
|
|
||||||
addLocation(newLocation, details);
|
|
||||||
setShowLocationModal(false);
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="locationType"
|
|
||||||
control={locationFormMethods.control}
|
|
||||||
render={() => (
|
|
||||||
<Select
|
|
||||||
maxMenuHeight={100}
|
|
||||||
name="location"
|
|
||||||
defaultValue={selectedLocation}
|
|
||||||
options={locationOptions}
|
|
||||||
isSearchable={false}
|
|
||||||
className=" my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) {
|
|
||||||
locationFormMethods.setValue("locationType", val.value);
|
|
||||||
locationFormMethods.unregister("locationLink");
|
|
||||||
locationFormMethods.unregister("locationAddress");
|
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
|
||||||
setSelectedLocation(val);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<LocationOptions />
|
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
|
||||||
<Button onClick={() => setShowLocationModal(false)} type="button" color="secondary">
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">{t("update")}</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="customInputs"
|
name="customInputs"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
|
@ -2294,6 +2095,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||||
|
|
||||||
|
if (Number.isNaN(typeParam)) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
@ -2406,7 +2213,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!rawEventType) throw Error("Event type not found");
|
if (!rawEventType) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await prisma.credential.findMany({
|
const credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -2478,7 +2289,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
const teamMembers = eventTypeObject.team
|
const teamMembers = eventTypeObject.team
|
||||||
? eventTypeObject.team.members.map((member) => {
|
? eventTypeObject.team.members.map((member) => {
|
||||||
const user = member.user;
|
const user = member.user;
|
||||||
user.avatar = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`;
|
user.avatar = `${CAL_URL}/${user.username}/avatar.png`;
|
||||||
return user;
|
return user;
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
|
@ -19,12 +19,12 @@ 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 { WEBAPP_URL } from "@calcom/lib/constants";
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
import { Dialog } from "@calcom/ui/Dialog";
|
||||||
import Dropdown, {
|
import Dropdown, {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
@ -246,7 +246,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
items={type.users.map((organizer) => ({
|
items={type.users.map((organizer) => ({
|
||||||
alt: organizer.name || "",
|
alt: organizer.name || "",
|
||||||
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${organizer.username}/avatar.png`,
|
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -257,7 +257,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
)}>
|
)}>
|
||||||
<Tooltip content={t("preview") as string}>
|
<Tooltip content={t("preview") as string}>
|
||||||
<a
|
<a
|
||||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`}
|
href={`${CAL_URL}/${group.profile.slug}/${type.slug}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={classNames("btn-icon appearance-none", type.$disabled && " opacity-30")}>
|
className={classNames("btn-icon appearance-none", type.$disabled && " opacity-30")}>
|
||||||
|
@ -271,9 +271,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(`${CAL_URL}/${group.profile.slug}/${type.slug}`);
|
||||||
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
className={classNames("btn-icon", type.$disabled && " opacity-30")}>
|
className={classNames("btn-icon", type.$disabled && " opacity-30")}>
|
||||||
<LinkIcon
|
<LinkIcon
|
||||||
|
@ -354,8 +352,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent portalled>
|
<DropdownMenuContent portalled>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link
|
<Link href={`${CAL_URL}/${group.profile.slug}/${type.slug}`}>
|
||||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`}>
|
|
||||||
<a target="_blank">
|
<a target="_blank">
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
@ -376,9 +373,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
StartIcon={ClipboardCopyIcon}
|
StartIcon={ClipboardCopyIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(`${CAL_URL}/${group.profile.slug}/${type.slug}`);
|
||||||
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
|
|
||||||
);
|
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}>
|
}}>
|
||||||
{t("copy_link") as string}
|
{t("copy_link") as string}
|
||||||
|
@ -398,7 +393,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
.share({
|
.share({
|
||||||
title: t("share"),
|
title: t("share"),
|
||||||
text: t("share_event"),
|
text: t("share_event"),
|
||||||
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`,
|
url: `${CAL_URL}/${group.profile.slug}/${type.slug}`,
|
||||||
})
|
})
|
||||||
.then(() => showToast(t("link_shared"), "success"))
|
.then(() => showToast(t("link_shared"), "success"))
|
||||||
.catch(() => showToast(t("failed"), "error"));
|
.catch(() => showToast(t("failed"), "error"));
|
||||||
|
@ -500,11 +495,10 @@ const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeading
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{profile?.slug && (
|
{profile?.slug && (
|
||||||
<Link href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${profile.slug}`}>
|
<Link href={`${CAL_URL}/${profile.slug}`}>
|
||||||
<a className="block text-xs text-neutral-500">{`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(
|
<a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${
|
||||||
"https://",
|
profile.slug
|
||||||
""
|
}`}</a>
|
||||||
)}/${profile.slug}`}</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { ClientSuspense } from "@components/ClientSuspense";
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import Schedule from "@components/availability/Schedule";
|
import Schedule from "@components/availability/Schedule";
|
||||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||||
import Text from "@components/ui/Text";
|
|
||||||
import TimezoneSelect from "@components/ui/form/TimezoneSelect";
|
import TimezoneSelect from "@components/ui/form/TimezoneSelect";
|
||||||
|
|
||||||
import getEventTypes from "../lib/queries/event-types/get-event-types";
|
import getEventTypes from "../lib/queries/event-types/get-event-types";
|
||||||
|
@ -398,10 +397,10 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
{t("timezone")}
|
{t("timezone")}
|
||||||
</label>
|
</label>
|
||||||
<Text variant="caption">
|
<p className="text-sm leading-tight text-gray-500 dark:text-white">
|
||||||
{t("current_time")}:
|
{t("current_time")}:
|
||||||
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
|
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
|
||||||
</Text>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
@ -529,9 +528,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
||||||
defaultValue={props.user.bio || undefined}
|
defaultValue={props.user.bio || undefined}
|
||||||
/>
|
/>
|
||||||
<Text variant="caption" className="mt-2">
|
<p className="mt-2 text-sm leading-tight text-gray-500 dark:text-white">
|
||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
</Text>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
|
@ -582,17 +581,13 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
||||||
<article className="relative">
|
<article className="relative">
|
||||||
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
|
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
|
||||||
<header>
|
<header>
|
||||||
<Text className="text-white" variant="largetitle">
|
<p className="font-cal mb-2 text-3xl tracking-wider text-white">{steps[currentStep].title}</p>
|
||||||
{steps[currentStep].title}
|
<p className="text-sm font-normal text-white">{steps[currentStep].description}</p>
|
||||||
</Text>
|
|
||||||
<Text className="text-white" variant="subtitle">
|
|
||||||
{steps[currentStep].description}
|
|
||||||
</Text>
|
|
||||||
</header>
|
</header>
|
||||||
<section className="space-y-2 pt-4">
|
<section className="space-y-2 pt-4">
|
||||||
<Text variant="footnote">
|
<p className="text-xs font-medium text-gray-500 dark:text-white">
|
||||||
Step {currentStep + 1} of {steps.length}
|
Step {currentStep + 1} of {steps.length}
|
||||||
</Text>
|
</p>
|
||||||
|
|
||||||
{error && <Alert severity="error" message={error?.message} />}
|
{error && <Alert severity="error" message={error?.message} />}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { useIntercom } from "@ee/lib/intercom/useIntercom";
|
import { useIntercom } from "@ee/lib/intercom/useIntercom";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
|
||||||
|
|
||||||
type CardProps = { title: string; description: string; className?: string; children: ReactNode };
|
type CardProps = { title: string; description: string; className?: string; children: ReactNode };
|
||||||
const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => (
|
const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => (
|
||||||
|
@ -30,8 +29,8 @@ export default function Billing() {
|
||||||
const { boot, show } = useIntercom();
|
const { boot, show } = useIntercom();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell heading={t("billing")} subtitle={t("manage_your_billing_info")}>
|
<SettingsShell heading={t("billing")} subtitle={t("manage_your_billing_info")}>
|
||||||
<SettingsShell>
|
<>
|
||||||
<div className="py-6 lg:col-span-9 lg:pb-8">
|
<div className="py-6 lg:col-span-9 lg:pb-8">
|
||||||
{data?.plan && ["FREE", "TRIAL"].includes(data.plan) && (
|
{data?.plan && ["FREE", "TRIAL"].includes(data.plan) && (
|
||||||
<Card
|
<Card
|
||||||
|
@ -72,7 +71,7 @@ export default function Billing() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { signOut } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
|
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
import showToast from "@calcom/lib/notification";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
@ -15,7 +16,6 @@ import { withQuery } from "@lib/QueryCell";
|
||||||
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { nameOfDay } from "@lib/core/i18n/weekday";
|
import { nameOfDay } from "@lib/core/i18n/weekday";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
@ -23,7 +23,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import ImageUploader from "@components/ImageUploader";
|
import ImageUploader from "@components/ImageUploader";
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import Badge from "@components/ui/Badge";
|
import Badge from "@components/ui/Badge";
|
||||||
|
@ -488,11 +487,9 @@ export default function Settings(props: Props) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
|
<SettingsShell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
|
||||||
<SettingsShell>
|
|
||||||
<WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} />
|
<WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} />
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { identityProviderNameMap } from "@lib/auth";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
|
||||||
import ChangePasswordSection from "@components/security/ChangePasswordSection";
|
import ChangePasswordSection from "@components/security/ChangePasswordSection";
|
||||||
import DisableUserImpersonation from "@components/security/DisableUserImpersonation";
|
import DisableUserImpersonation from "@components/security/DisableUserImpersonation";
|
||||||
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
|
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
|
||||||
|
@ -18,8 +17,8 @@ export default function Security() {
|
||||||
const user = trpc.useQuery(["viewer.me"]).data;
|
const user = trpc.useQuery(["viewer.me"]).data;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
|
<SettingsShell heading={t("security")} subtitle={t("manage_account_security")}>
|
||||||
<SettingsShell>
|
<>
|
||||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
@ -45,7 +44,7 @@ export default function Security() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SAMLConfiguration teamsView={false} teamId={null} />
|
<SAMLConfiguration teamsView={false} teamId={null} />
|
||||||
|
</>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,21 +4,20 @@ import { useSession } from "next-auth/react";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
import { trpc } from "@lib/trpc";
|
import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import EmptyScreen from "@components/EmptyScreen";
|
import EmptyScreen from "@components/EmptyScreen";
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import SettingsShell from "@components/SettingsShell";
|
import SettingsShell from "@components/SettingsShell";
|
||||||
import Shell from "@components/Shell";
|
|
||||||
import TeamCreateModal from "@components/team/TeamCreateModal";
|
import TeamCreateModal from "@components/team/TeamCreateModal";
|
||||||
import TeamList from "@components/team/TeamList";
|
import TeamList from "@components/team/TeamList";
|
||||||
|
|
||||||
export default function Teams() {
|
function Teams() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const loading = status === "loading";
|
const loading = status === "loading";
|
||||||
|
@ -40,8 +39,8 @@ export default function Teams() {
|
||||||
const isFreePlan = me.data?.plan === "FREE";
|
const isFreePlan = me.data?.plan === "FREE";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
|
<SettingsShell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
|
||||||
<SettingsShell>
|
<>
|
||||||
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
{isFreePlan && (
|
{isFreePlan && (
|
||||||
<Alert
|
<Alert
|
||||||
|
@ -87,7 +86,11 @@ export default function Teams() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{teams.length > 0 && <TeamList teams={teams}></TeamList>}
|
{teams.length > 0 && <TeamList teams={teams}></TeamList>}
|
||||||
|
</>
|
||||||
</SettingsShell>
|
</SettingsShell>
|
||||||
</Shell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Teams.requiresLicense = false;
|
||||||
|
|
||||||
|
export default Teams;
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
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";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
|
@ -148,7 +148,7 @@ type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
export default function Success(props: SuccessProps) {
|
export default function Success(props: SuccessProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { location: _location, name, reschedule, status } = router.query;
|
const { location: _location, name, reschedule, listingStatus, status } = router.query;
|
||||||
const location = Array.isArray(_location) ? _location[0] : _location;
|
const location = Array.isArray(_location) ? _location[0] : _location;
|
||||||
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
@ -176,6 +176,7 @@ export default function Success(props: SuccessProps) {
|
||||||
|
|
||||||
const eventName = getEventName(eventNameObject);
|
const eventName = getEventName(eventNameObject);
|
||||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
||||||
|
const isCancelled = status === "CANCELLED" || status === "REJECTED";
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
telemetry.withJitsu((jitsu) =>
|
telemetry.withJitsu((jitsu) =>
|
||||||
|
@ -238,6 +239,9 @@ export default function Success(props: SuccessProps) {
|
||||||
|
|
||||||
function getTitle(): string {
|
function getTitle(): string {
|
||||||
const titleSuffix = props.recurringBookings ? "_recurring" : "";
|
const titleSuffix = props.recurringBookings ? "_recurring" : "";
|
||||||
|
if (isCancelled) {
|
||||||
|
return t("emailed_information_about_cancelled_event");
|
||||||
|
}
|
||||||
if (needsConfirmation) {
|
if (needsConfirmation) {
|
||||||
if (props.profile.name !== null) {
|
if (props.profile.name !== null) {
|
||||||
return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, {
|
return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, {
|
||||||
|
@ -298,25 +302,31 @@ export default function Success(props: SuccessProps) {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mx-auto flex items-center justify-center",
|
"mx-auto flex items-center justify-center",
|
||||||
!giphyImage ? "h-12 w-12 rounded-full bg-green-100" : ""
|
!giphyImage && !isCancelled ? "h-12 w-12 rounded-full bg-green-100" : "",
|
||||||
|
isCancelled ? "h-12 w-12 rounded-full bg-red-100" : ""
|
||||||
)}>
|
)}>
|
||||||
{giphyImage && !needsConfirmation && (
|
{giphyImage && !needsConfirmation && (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img src={giphyImage} alt={"Gif from Giphy"} />
|
<img src={giphyImage} alt={"Gif from Giphy"} />
|
||||||
)}
|
)}
|
||||||
{!giphyImage && !needsConfirmation && (
|
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||||
<CheckIcon className="h-8 w-8 text-green-600" />
|
<CheckIcon className="h-8 w-8 text-green-600" />
|
||||||
)}
|
)}
|
||||||
{needsConfirmation && <ClockIcon className="h-8 w-8 text-green-600" />}
|
{needsConfirmation && !isCancelled && (
|
||||||
|
<ClockIcon className="h-8 w-8 text-green-600" />
|
||||||
|
)}
|
||||||
|
{isCancelled && <XIcon className="h-8 w-8 text-red-600" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3
|
<h3
|
||||||
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
|
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
|
||||||
id="modal-headline">
|
id="modal-headline">
|
||||||
{needsConfirmation
|
{needsConfirmation && !isCancelled
|
||||||
? props.recurringBookings
|
? props.recurringBookings
|
||||||
? t("submitted_recurring")
|
? t("submitted_recurring")
|
||||||
: t("submitted")
|
: t("submitted")
|
||||||
|
: isCancelled
|
||||||
|
? t("event_cancelled")
|
||||||
: props.recurringBookings
|
: props.recurringBookings
|
||||||
? t("meeting_is_scheduled_recurring")
|
? t("meeting_is_scheduled_recurring")
|
||||||
: t("meeting_is_scheduled")}
|
: t("meeting_is_scheduled")}
|
||||||
|
@ -333,11 +343,13 @@ export default function Success(props: SuccessProps) {
|
||||||
isReschedule={reschedule === "true"}
|
isReschedule={reschedule === "true"}
|
||||||
eventType={props.eventType}
|
eventType={props.eventType}
|
||||||
recurringBookings={props.recurringBookings}
|
recurringBookings={props.recurringBookings}
|
||||||
status={(status as string) || "upcoming"}
|
listingStatus={(listingStatus as string) || "upcoming"}
|
||||||
date={date}
|
date={date}
|
||||||
is24h={is24h}
|
is24h={is24h}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{(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">
|
<div className="col-span-2 mb-6">
|
||||||
{bookingInfo?.user && (
|
{bookingInfo?.user && (
|
||||||
|
@ -355,10 +367,12 @@ export default function Success(props: SuccessProps) {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{location && (
|
{location && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-6 font-medium">{t("where")}</div>
|
<div className="mt-3 font-medium">{t("where")}</div>
|
||||||
<div className="col-span-2 mt-6">
|
<div className="col-span-2 mt-3">
|
||||||
{location.startsWith("http") ? (
|
{location.startsWith("http") ? (
|
||||||
<a title="Meeting Link" href={location}>
|
<a title="Meeting Link" href={location}>
|
||||||
{location}
|
{location}
|
||||||
|
@ -401,6 +415,7 @@ export default function Success(props: SuccessProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!needsConfirmation &&
|
{!needsConfirmation &&
|
||||||
|
!isCancelled &&
|
||||||
(!isCancellationMode ? (
|
(!isCancellationMode ? (
|
||||||
<div className="border-bookinglightest text-bookingdark mt-2 grid grid-cols-3 border-b py-4 text-left dark:border-gray-900">
|
<div className="border-bookinglightest text-bookingdark mt-2 grid grid-cols-3 border-b py-4 text-left dark:border-gray-900">
|
||||||
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||||
|
@ -425,7 +440,7 @@ export default function Success(props: SuccessProps) {
|
||||||
theme={userIsOwner ? "light" : props.profile.theme}
|
theme={userIsOwner ? "light" : props.profile.theme}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{userIsOwner && !needsConfirmation && !isCancellationMode && (
|
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
|
||||||
<div className="border-bookinglightest mt-9 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4">
|
<div className="border-bookinglightest mt-9 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4">
|
||||||
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||||
{t("add_to_calendar")}
|
{t("add_to_calendar")}
|
||||||
|
@ -591,7 +606,7 @@ type RecurringBookingsProps = {
|
||||||
recurringBookings: SuccessProps["recurringBookings"];
|
recurringBookings: SuccessProps["recurringBookings"];
|
||||||
date: dayjs.Dayjs;
|
date: dayjs.Dayjs;
|
||||||
is24h: boolean;
|
is24h: boolean;
|
||||||
status: string;
|
listingStatus: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function RecurringBookings({
|
function RecurringBookings({
|
||||||
|
@ -599,11 +614,11 @@ function RecurringBookings({
|
||||||
eventType,
|
eventType,
|
||||||
recurringBookings,
|
recurringBookings,
|
||||||
date,
|
date,
|
||||||
status,
|
listingStatus,
|
||||||
}: RecurringBookingsProps) {
|
}: RecurringBookingsProps) {
|
||||||
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return !isReschedule && recurringBookings && status === "upcoming" ? (
|
return !isReschedule && recurringBookings && listingStatus === "upcoming" ? (
|
||||||
<>
|
<>
|
||||||
{eventType.recurringEvent?.count &&
|
{eventType.recurringEvent?.count &&
|
||||||
recurringBookings.slice(0, 4).map((dateStr, idx) => (
|
recurringBookings.slice(0, 4).map((dateStr, idx) => (
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core";
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
|
||||||
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
||||||
|
@ -23,7 +23,6 @@ import { HeadSeo } from "@components/seo/head-seo";
|
||||||
import Team from "@components/team/screens/Team";
|
import Team from "@components/team/screens/Team";
|
||||||
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 Text from "@components/ui/Text";
|
|
||||||
|
|
||||||
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
function TeamPage({ team }: TeamPageProps) {
|
function TeamPage({ team }: TeamPageProps) {
|
||||||
|
@ -68,7 +67,7 @@ function TeamPage({ team }: TeamPageProps) {
|
||||||
size={10}
|
size={10}
|
||||||
items={type.users.map((user) => ({
|
items={type.users.map((user) => ({
|
||||||
alt: user.name || "",
|
alt: user.name || "",
|
||||||
image: WEBSITE_URL + "/" + user.username + "/avatar.png" || "",
|
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,12 +92,8 @@ function TeamPage({ team }: TeamPageProps) {
|
||||||
imageSrc={getPlaceholderAvatar(team.logo, team.name)}
|
imageSrc={getPlaceholderAvatar(team.logo, team.name)}
|
||||||
className="mx-auto mb-4 h-20 w-20 rounded-full"
|
className="mx-auto mb-4 h-20 w-20 rounded-full"
|
||||||
/>
|
/>
|
||||||
<Text variant="largetitle" className="text-gray-900 dark:text-white">
|
<p className="font-cal mb-2 text-3xl tracking-wider text-gray-900 dark:text-white">{teamName}</p>
|
||||||
{teamName}
|
<p className="mt-2 text-sm font-normal text-neutral-500 dark:text-white">{team.bio}</p>
|
||||||
</Text>
|
|
||||||
<Text variant="subtitle" className="mt-2">
|
|
||||||
{team.bio}
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
||||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||||
|
@ -147,7 +142,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
...type,
|
...type,
|
||||||
users: type.users.map((user) => ({
|
users: type.users.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
avatar: WEBSITE_URL + "/" + user.username + "/avatar.png",
|
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getWorkingHours } from "@lib/availability";
|
import { getWorkingHours } from "@lib/availability";
|
||||||
import getBooking, { GetBookingType } from "@lib/getBooking";
|
import getBooking, { GetBookingType } from "@lib/getBooking";
|
||||||
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location";
|
import { locationHiddenFilter, LocationObject } from "@lib/location";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user