Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dca1d35cb4 | ||
|
|
af970c049a | ||
|
|
6fc3840ca3 | ||
|
|
c51150350a | ||
|
|
51664bf733 | ||
|
|
831506447a | ||
|
|
ee54e670cb | ||
|
|
99cd6fe7da | ||
|
|
75c7e03efe | ||
|
|
2ebfcf448c | ||
|
|
235ec0e906 | ||
|
|
ea73870ce9 | ||
|
|
d1440bb5d2 | ||
|
|
a000030040 | ||
|
|
44864a801d | ||
|
|
b9fd0b4c78 | ||
|
|
0e7276370f | ||
|
|
4d2cf28c7e | ||
|
|
55ee939cf9 | ||
|
|
2fe54ded15 | ||
|
|
8e3fa4be29 | ||
|
|
d7b4d5932d | ||
|
|
3c7fc85b34 | ||
|
|
af018f0e8b | ||
|
|
6573f75a83 | ||
|
|
a3b26cd1e7 | ||
|
|
4f99746f68 | ||
|
|
fd76bc9cab | ||
|
|
602c813928 | ||
|
|
2c54ea90f9 | ||
|
+17 |
2698cde311 | ||
|
|
1433ec6946 | ||
|
|
48e5d18ea1 | ||
|
|
5da76c698e | ||
|
|
578308a8cb | ||
|
|
6b3862f96d | ||
|
|
94f239bc42 | ||
|
|
2f2f6006e6 | ||
|
|
f8558e16e8 | ||
|
|
38be2bef9a | ||
|
|
485619602f | ||
|
|
ce01537fe6 | ||
|
|
f77e7476d2 | ||
|
|
b6016c8f20 | ||
|
|
a5e5d103a0 | ||
|
|
fec02979ba | ||
|
|
dc200b95cb | ||
|
|
78a54e5f54 | ||
|
|
00ebda2fc6 | ||
|
|
7c749299bb | ||
|
|
3df289b557 | ||
|
|
f4c849b75c | ||
|
|
09cc6f07e4 | ||
|
|
78656f2116 | ||
|
|
1fa5e296af | ||
|
|
0f76530c11 | ||
|
|
ab6758194b | ||
|
|
5528d60897 | ||
|
|
bb1c55e07f | ||
|
|
c899921591 | ||
|
|
ea34545d72 | ||
|
|
9d3540511b | ||
|
|
e8cf92361a | ||
|
|
e8525b963b | ||
|
|
24dabd0ddc | ||
|
|
5203168b2e | ||
|
|
c3e40f84a9 | ||
|
|
8e2ece7526 | ||
|
|
b6eca6e083 | ||
|
|
4cb264f1f7 | ||
|
|
e248fda36b | ||
|
|
1c97e549eb | ||
|
|
77600f8168 | ||
|
|
6cad606ba1 | ||
|
|
7cae7a7bd4 | ||
|
|
93f65807e7 | ||
|
|
0a63820699 | ||
|
|
05ad437b27 | ||
|
|
26a57a71ae | ||
|
|
34a10205a3 | ||
|
|
fbf8ff6db7 | ||
|
|
bef6d18fdf | ||
|
|
2e96e2ef3f | ||
|
|
2ca6769a70 | ||
|
|
1eb514dfcf | ||
|
|
d6c004ba22 | ||
|
+17 |
afb9605f0a | ||
|
|
a2549bd7b6 | ||
|
|
c2a8f799ff | ||
|
|
c68ff54e77 | ||
|
|
74fe20802d | ||
|
|
d59cda3286 | ||
|
|
1cae2b6f3d | ||
|
|
9d9fa8e575 | ||
|
|
f04c634c48 | ||
|
|
c3e7981917 | ||
|
|
d6594dab0f | ||
|
|
5fcfbb2d90 | ||
|
|
8a99c3d2a5 | ||
|
|
336c2d3643 | ||
|
|
cfe3ef4ab0 | ||
|
|
4ec8241a86 | ||
|
|
e6313aafac | ||
|
|
f87f030bb5 | ||
|
|
f87254cdc3 | ||
|
|
1f74837189 | ||
|
|
c7064906c9 | ||
|
|
86c94a3cd5 | ||
|
|
2056ebe186 | ||
|
|
53424ca9ae | ||
|
|
8197ba18d4 | ||
|
|
d341ea5111 | ||
|
|
42dcd4a2a9 | ||
|
|
c63225fdbf | ||
|
|
41774e4932 | ||
|
|
af7849f951 | ||
|
|
5353610d9a | ||
|
+14 |
156f3e5de2 | ||
|
|
05f51459a6 | ||
|
|
f0db97bbb1 | ||
|
|
05c33bd94b | ||
|
|
7d4cc59cd2 | ||
|
|
a94465ff04 | ||
|
|
4d54ac3ca2 | ||
|
|
15da690270 | ||
|
|
2462e8e597 | ||
|
|
2f628a17df | ||
|
|
1ee3783db3 | ||
|
|
b3d8d7e944 | ||
|
|
a4b7f1af65 | ||
|
|
f7445bf8a1 | ||
|
|
9cad7086d7 | ||
|
|
3345ec61ee | ||
|
|
7797421ce9 | ||
|
|
075a274207 | ||
|
|
a46a31040a | ||
|
|
7a28e1d6f4 | ||
|
|
51fd7fdb3e | ||
|
|
d01963303a | ||
|
|
6877d0f693 | ||
|
|
39040fca9a | ||
|
|
6fa4befc9b | ||
|
|
8c9871c6ce | ||
|
+8 |
a9af2fb255 | ||
|
|
8c150264f4 | ||
|
|
cfc3644d51 | ||
|
|
bdb4a7353b | ||
|
|
30c0e6d1d7 | ||
|
|
390967990a | ||
|
|
cc56602474 | ||
|
|
43a044d59e | ||
|
|
5b8ed9c3ef | ||
|
|
c3d6df2dcf | ||
|
|
9d02cb493c | ||
|
|
26ed478e50 | ||
|
|
09e338970d | ||
|
|
f60399ca8b | ||
|
|
5cf7e32852 | ||
|
|
24be8a63f2 | ||
|
|
75509c97fd | ||
|
|
9b26c7aee6 | ||
|
|
f18e80cfcf | ||
|
|
0a4b1ca014 | ||
|
|
9fa6aced33 | ||
|
|
9093909075 | ||
|
|
832fce91e0 | ||
|
|
6da41fce63 | ||
|
|
3b51cc9aef | ||
|
|
50c8959cfa | ||
|
|
c96d258f5e | ||
|
|
6e45b1c383 | ||
|
|
551ebe863b | ||
|
|
fa7b94bfe9 | ||
|
|
56c5d39cd3 | ||
|
|
7574fb6f44 | ||
|
|
901d1f0e01 | ||
|
|
828fcbe1d9 | ||
|
|
34db46869e | ||
|
|
1f1cab2be1 |
No files matched your search
+11
-4
@@ -1,20 +1,21 @@
|
|||||||
# ********** INDEX **********
|
# ********** INDEX **********
|
||||||
#
|
#
|
||||||
# - LICENSE
|
# - LICENSE (DEPRECATED)
|
||||||
# - DATABASE
|
# - DATABASE
|
||||||
# - SHARED
|
# - SHARED
|
||||||
# - NEXTAUTH
|
# - NEXTAUTH
|
||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - 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
|
||||||
#
|
#
|
||||||
# To enable enterprise-only features, fill your license key in here.
|
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
|
||||||
|
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
|||||||
# Set this to true in to disable new signups
|
# Set this to true in to disable new signups
|
||||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||||
|
|
||||||
|
# Content Security Policy
|
||||||
|
CSP_POLICY=
|
||||||
|
|
||||||
|
# Vercel Edge Config
|
||||||
|
EDGE_CONFIG=
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Cron - mark stale for inactive issues
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
|
schedule:
|
||||||
|
# Runs every day (see https://crontab.guru)
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v7
|
||||||
|
with:
|
||||||
|
days-before-stale: 60
|
||||||
|
include-only-assigned: true
|
||||||
|
days-before-close: -1
|
||||||
|
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
e2e:
|
e2e:
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
runs-on: ubuntu-latest
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
name: "Next.js Bundle Analysis"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
# change this if your nextjs app does not live at the root of the repo
|
||||||
|
working-directory: ./apps/web/
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
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
|
||||||
|
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||||
|
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||||
|
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ secrets.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||||
|
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: 1
|
||||||
|
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||||
|
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||||
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||||
|
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||||
|
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||||
|
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
|
||||||
|
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
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
uses: bahmutov/npm-install@v1
|
||||||
|
|
||||||
|
- name: Restore next build
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: restore-build-cache
|
||||||
|
env:
|
||||||
|
cache-name: cache-next-build
|
||||||
|
with:
|
||||||
|
path: apps/web/.next/cache
|
||||||
|
# change this if you prefer a more strict cache
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}
|
||||||
|
|
||||||
|
- name: Build next.js app
|
||||||
|
# change this if your site requires a custom build command
|
||||||
|
run: cd ../.. && yarn db-seed && yarn build
|
||||||
|
|
||||||
|
# Here's the first place where next-bundle-analysis' own script is used
|
||||||
|
# This step pulls the raw bundle stats for the current bundle
|
||||||
|
- name: Analyze bundle
|
||||||
|
run: npx -p nextjs-bundle-analysis report
|
||||||
|
|
||||||
|
- name: Upload bundle
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: bundle
|
||||||
|
path: apps/web/.next/analyze/__bundle_analysis.json
|
||||||
|
|
||||||
|
- name: Download base branch bundle stats
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
if: success() && github.event.number
|
||||||
|
with:
|
||||||
|
workflow: nextjs-bundle-analysis.yml
|
||||||
|
branch: ${{ github.event.pull_request.base.ref }}
|
||||||
|
path: apps/web/.next/analyze/base
|
||||||
|
|
||||||
|
# And here's the second place - this runs after we have both the current and
|
||||||
|
# base branch bundle stats, and will compare them to determine what changed.
|
||||||
|
# There are two configurable arguments that come from package.json:
|
||||||
|
#
|
||||||
|
# - budget: optional, set a budget (bytes) against which size changes are measured
|
||||||
|
# it's set to 350kb here by default, as informed by the following piece:
|
||||||
|
# https://infrequently.org/2021/03/the-performance-inequality-gap/
|
||||||
|
#
|
||||||
|
# - red-status-percentage: sets the percent size increase where you get a red
|
||||||
|
# status indicator, defaults to 20%
|
||||||
|
#
|
||||||
|
# Either of these arguments can be changed or removed by editing the `nextBundleAnalysis`
|
||||||
|
# entry in your package.json file.
|
||||||
|
- name: Compare with base branch bundle
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: ls -laR .next/analyze/base && npx -p nextjs-bundle-analysis compare
|
||||||
|
|
||||||
|
- name: Get comment body
|
||||||
|
id: get-comment-body
|
||||||
|
if: success() && github.event.number
|
||||||
|
run: |
|
||||||
|
body=$(cat .next/analyze/__bundle_analysis_comment.txt)
|
||||||
|
body="${body//'%'/'%25'}"
|
||||||
|
body="${body//$'\n'/'%0A'}"
|
||||||
|
body="${body//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=body::$body
|
||||||
|
|
||||||
|
- name: Find Comment
|
||||||
|
uses: peter-evans/find-comment@v1
|
||||||
|
if: success() && github.event.number
|
||||||
|
id: fc
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
||||||
|
|
||||||
|
- name: Create Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id == 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
|
||||||
|
- name: Update Comment
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
if: success() && github.event.number && steps.fc.outputs.comment-id != 0
|
||||||
|
with:
|
||||||
|
issue-number: ${{ github.event.number }}
|
||||||
|
body: ${{ steps.get-comment-body.outputs.body }}
|
||||||
|
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||||
|
edit-mode: replace
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: TODO to Issue
|
||||||
|
on:
|
||||||
|
pull_request_target: # So we can test on forks
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
MANUAL_COMMIT_REF:
|
||||||
|
description: "The SHA of the commit to get the diff for"
|
||||||
|
required: true
|
||||||
|
MANUAL_BASE_REF:
|
||||||
|
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
||||||
|
required: false
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
todos:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: "Add TODOs to issues"
|
||||||
|
uses: "alstr/todo-to-issue-action@v4"
|
||||||
|
env:
|
||||||
|
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
||||||
|
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
||||||
|
with:
|
||||||
|
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
||||||
@@ -77,6 +77,9 @@ lint-results
|
|||||||
#Storybook
|
#Storybook
|
||||||
apps/storybook/build-storybook.log
|
apps/storybook/build-storybook.log
|
||||||
|
|
||||||
|
# Infisical
|
||||||
|
.infisical.json
|
||||||
|
|
||||||
# Snaplet
|
# Snaplet
|
||||||
.snaplet/snapshots
|
.snaplet/snapshots
|
||||||
.snaplet/structure.d.ts
|
.snaplet/structure.d.ts
|
||||||
|
|||||||
@@ -4,6 +4,60 @@ Contributions are what make the open source community such an amazing place to l
|
|||||||
|
|
||||||
- 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.
|
||||||
|
|
||||||
|
## Priorities
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Type of Issue
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Priority
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Minor improvements, non-core feature requests
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Low+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Low%20Priority-green">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Confusing UX (... but working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22Medium+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-Medium%20Priority-yellow">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Features (Booking page, availabilty, timezone calculation)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
|
||||||
|
<img src="https://img.shields.io/badge/-High%20Priority-orange">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
Core Bugs (Login, Booking page, Emails are not working)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:Urgent">
|
||||||
|
<img src="https://img.shields.io/badge/-Urgent-red">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></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>
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- ABOUT THE PROJECT -->
|
||||||
@@ -76,8 +77,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
@@ -341,6 +340,17 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
|
<!-- BOUNTIES -->
|
||||||
|
|
||||||
|
### Bounties
|
||||||
|
|
||||||
|
<a href="https://console.algora.io/org/cal/bounties?status=open">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
||||||
|
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- TRANSLATIONS -->
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
@@ -350,7 +360,9 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
|
|||||||
|
|
||||||
                                             except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"plan": "heroku-postgresql:hobby-dev"
|
"plan": "heroku-postgresql:mini"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
@@ -43,15 +43,18 @@
|
|||||||
},
|
},
|
||||||
"SENDGRID_API_KEY": {
|
"SENDGRID_API_KEY": {
|
||||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_SYNC_API_KEY": {
|
"SENDGRID_SYNC_API_KEY": {
|
||||||
"description": "Sendgrid internal sync service",
|
"description": "Sendgrid internal sync service",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"SENDGRID_EMAIL": {
|
"SENDGRID_EMAIL": {
|
||||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"EMAIL_FROM": {
|
"EMAIL_FROM": {
|
||||||
"description": "Configures the global From: header whilst sending emails",
|
"description": "Configures the global From: header whilst sending emails",
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -25,6 +25,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/config": "*",
|
"@calcom/config": "*",
|
||||||
"eslint": "^8.22.0"
|
"eslint": "^8.34.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import "nextra-theme-docs/style.css";
|
import "nextra-theme-docs/style.css";
|
||||||
|
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { create } from "@storybook/theming";
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: "light",
|
||||||
|
colorPrimary: "#111827",
|
||||||
|
colorSecondary: "#111827",
|
||||||
|
appContentBg: "#F9FAFC",
|
||||||
|
brandTitle: "Cal.com UI",
|
||||||
|
brandUrl: "https://cal.com",
|
||||||
|
brandImage: "https://cal.com/logo.svg",
|
||||||
|
brandTarget: "_self",
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from "@storybook/addons";
|
||||||
|
|
||||||
|
import yourTheme from "./YourTheme";
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: yourTheme,
|
||||||
|
});
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.0",
|
"@radix-ui/react-popover": "^1.0.2",
|
||||||
"@radix-ui/react-radio-group": "^1.0.0",
|
"@radix-ui/react-radio-group": "^1.0.0",
|
||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
|
|||||||
@@ -2,4 +2,9 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import "../../../packages/ui/styles/shared-globals.css"
|
@import "../../../packages/ui/styles/shared-globals.css";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-inter: "Inter var";
|
||||||
|
--font-cal: "Cal Sans";
|
||||||
|
}
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.9.1",
|
"@types/node": "16.9.1",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "18.0.9",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { SwaggerUI } from "swagger-ui-react";
|
import type { SwaggerUI } from "swagger-ui-react";
|
||||||
|
|
||||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
import { OptionProps } from "react-select";
|
import type { OptionProps } from "react-select";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -62,10 +63,9 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
|
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||||
<div className="flex gap-x-3">
|
<div className="flex gap-x-3 p-4">
|
||||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||||
|
|
||||||
<div className="flex grow flex-col gap-y-1 truncate">
|
<div className="flex grow flex-col gap-y-1 truncate">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NextRouter, useRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
import { useRouter } from "next/router";
|
||||||
import { components, ControlProps } from "react-select";
|
import type { MutableRefObject, RefObject } from "react";
|
||||||
|
import { createRef, forwardRef, useRef, useState } from "react";
|
||||||
|
import type { ControlProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -12,16 +15,15 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
InputLeading,
|
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
TextArea,
|
||||||
TextField,
|
TextField,
|
||||||
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import ColorPicker from "@components/ui/colorpicker";
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
@@ -676,7 +678,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
const parsedTabs = tabs.map((t) => ({ ...t, href: s(t.href) }));
|
||||||
const embedCodeRefs: Record<typeof tabs[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
const embedCodeRefs: Record<(typeof tabs)[0]["name"], RefObject<HTMLTextAreaElement>> = {};
|
||||||
tabs
|
tabs
|
||||||
.filter((tab) => tab.type === "code")
|
.filter((tab) => tab.type === "code")
|
||||||
.forEach((codeTab) => {
|
.forEach((codeTab) => {
|
||||||
@@ -720,7 +722,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
|
|
||||||
const calLink = decodeURIComponent(embedUrl);
|
const calLink = decodeURIComponent(embedUrl);
|
||||||
|
|
||||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
const addToPalette = (update: (typeof previewState)["palette"]) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
return {
|
return {
|
||||||
...previewState,
|
...previewState,
|
||||||
@@ -1061,7 +1063,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
defaultValue="#000000"
|
defaultValue="#000000"
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
addToPalette({
|
addToPalette({
|
||||||
[palette.name as keyof typeof previewState["palette"]]: color,
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Dispatch, useState, useEffect } from "react";
|
import type { Dispatch } from "react";
|
||||||
import { JSONObject } from "superjson/dist/types";
|
import { useState, useEffect } from "react";
|
||||||
|
import type { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Cropper from "react-easy-crop";
|
import Cropper from "react-easy-crop";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
||||||
|
|
||||||
import { Area, getCroppedImg } from "@lib/cropImage";
|
import type { Area } from "@lib/cropImage";
|
||||||
|
import { getCroppedImg } from "@lib/cropImage";
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
import { useFileReader } from "@lib/hooks/useFileReader";
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import Slider from "@components/Slider";
|
import Slider from "@components/Slider";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import noop from "lodash/noop";
|
||||||
import Link, { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, Fragment, MouseEventHandler } from "react";
|
import type { FC, MouseEventHandler } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
export interface NavTabProps {
|
export interface NavTabProps {
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OptionProps } from "react-select";
|
import type { OptionProps } from "react-select";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import classNames from "@calcom/lib/classNames";
|
|||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiBookOpen,
|
FiBookOpen,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
import type { DestinationCalendar } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
@@ -44,7 +45,7 @@ export default function InstalledAppsLayout({
|
|||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
const variant: typeof InstalledAppVariants[number] = "payment";
|
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
||||||
|
|
||||||
const query = trpc.viewer.integrations.useQuery({
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
variant,
|
variant,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps, useEffect } from "react";
|
import type { ComponentProps } from "react";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
import { UserPermissionRole } from ".prisma/client";
|
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiLink } from "@calcom/ui/components/icon";
|
import { FiLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
return locations.length ? (
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||||
{locations.map((location) => {
|
{locations.map((location, index) => {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
// It's possible that the location app got uninstalled
|
// It's possible that the location app got uninstalled
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||||
) : (
|
) : (
|
||||||
@@ -26,7 +26,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
@@ -37,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { rescheduleUid } = router.query;
|
const { rescheduleUid } = router.query;
|
||||||
@@ -47,122 +51,124 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!date) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
<div ref={slotPickerRef}>
|
||||||
<div className="mb-6 flex items-center text-left text-base">
|
{!!date ? (
|
||||||
<div className="mr-4">
|
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
<div className="mb-6 flex items-center text-left text-base">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
<div className="mr-4">
|
||||||
</span>
|
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||||
<span className="text-bookinglight font-medium">
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
</span>
|
||||||
</span>
|
<span className="text-bookinglight font-medium">
|
||||||
</div>
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
<div className="ml-auto">
|
</span>
|
||||||
<ToggleGroup
|
</div>
|
||||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
<div className="ml-auto">
|
||||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
<ToggleGroup
|
||||||
options={[
|
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||||
{ value: "12", label: t("12_hour_short") },
|
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||||
{ value: "24", label: t("24_hour_short") },
|
options={[
|
||||||
]}
|
{ value: "12", label: t("12_hour_short") },
|
||||||
/>
|
{ value: "24", label: t("24_hour_short") },
|
||||||
</div>
|
]}
|
||||||
</div>
|
/>
|
||||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
</div>
|
||||||
{slots.length > 0 &&
|
|
||||||
slots.map((slot) => {
|
|
||||||
type BookingURL = {
|
|
||||||
pathname: string;
|
|
||||||
query: Record<string, string | number | string[] | undefined>;
|
|
||||||
};
|
|
||||||
const bookingUrl: BookingURL = {
|
|
||||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
|
||||||
type: eventTypeId,
|
|
||||||
slug: eventTypeSlug,
|
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
|
||||||
...(ethSignature ? { ethSignature } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (rescheduleUid) {
|
|
||||||
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If event already has an attendee add booking id
|
|
||||||
if (slot.bookingUid) {
|
|
||||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
|
||||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
|
||||||
{/* Current there is no way to disable Next.js Links */}
|
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}>
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href={bookingUrl}
|
|
||||||
prefetch={false}
|
|
||||||
className={classNames(
|
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
|
||||||
)}
|
|
||||||
data-testid="time">
|
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
|
||||||
{!!seatsPerTimeSlot && (
|
|
||||||
<p
|
|
||||||
className={`${
|
|
||||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
|
||||||
? "text-rose-600"
|
|
||||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
|
||||||
? "text-yellow-500"
|
|
||||||
: "text-emerald-400"
|
|
||||||
} text-sm`}>
|
|
||||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
|
||||||
{seatsPerTimeSlot} {t("seats_available")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{!isLoading && !slots.length && (
|
|
||||||
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
|
||||||
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||||
|
{slots.length > 0 &&
|
||||||
|
slots.map((slot) => {
|
||||||
|
type BookingURL = {
|
||||||
|
pathname: string;
|
||||||
|
query: Record<string, string | number | string[] | undefined>;
|
||||||
|
};
|
||||||
|
const bookingUrl: BookingURL = {
|
||||||
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||||
|
type: eventTypeId,
|
||||||
|
slug: eventTypeSlug,
|
||||||
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
{isLoading && !slots.length && (
|
if (rescheduleUid) {
|
||||||
<>
|
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
||||||
<SkeletonContainer className="mb-2">
|
}
|
||||||
<SkeletonText className="h-5 w-full" />
|
|
||||||
</SkeletonContainer>
|
// If event already has an attendee add booking id
|
||||||
<SkeletonContainer className="mb-2">
|
if (slot.bookingUid) {
|
||||||
<SkeletonText className="h-5 w-full" />
|
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||||
</SkeletonContainer>
|
}
|
||||||
<SkeletonContainer className="mb-2">
|
|
||||||
<SkeletonText className="h-5 w-full" />
|
return (
|
||||||
</SkeletonContainer>
|
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||||
</>
|
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||||
)}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
</div>
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
|
||||||
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
|
)}>
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={bookingUrl}
|
||||||
|
prefetch={false}
|
||||||
|
className={classNames(
|
||||||
|
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||||
|
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
||||||
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
|
)}
|
||||||
|
data-testid="time">
|
||||||
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
|
{!!seatsPerTimeSlot && (
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||||
|
? "text-rose-600"
|
||||||
|
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-emerald-400"
|
||||||
|
} text-sm`}>
|
||||||
|
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||||
|
{seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!isLoading && !slots.length && (
|
||||||
|
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
|
||||||
|
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && !slots.length && (
|
||||||
|
<>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
<SkeletonContainer className="mb-2">
|
||||||
|
<SkeletonText className="h-5 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { FC, ReactNode, useEffect } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { classNames } from "@calcom/lib";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { BookingStatus } from "@prisma/client";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { ActionType } from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,13 +20,11 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
|
||||||
MeetingTimeInTimezones,
|
MeetingTimeInTimezones,
|
||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionType,
|
|
||||||
TableActions,
|
TableActions,
|
||||||
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.status === BookingStatus.REJECTED) {
|
if (data?.status === BookingStatus.REJECTED) {
|
||||||
setRejectionDialogIsOpen(false);
|
setRejectionDialogIsOpen(false);
|
||||||
showToast(t("booking_rejection_success"), "success");
|
showToast(t("booking_rejection_success"), "success");
|
||||||
} else {
|
} else {
|
||||||
@@ -245,24 +246,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{/* 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}>
|
||||||
<DialogContent>
|
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||||
<DialogHeader title={t("rejection_reason_title")} />
|
<div>
|
||||||
|
<TextAreaField
|
||||||
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
|
name="rejectionReason"
|
||||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
label={
|
||||||
{t("rejection_reason")}
|
<>
|
||||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
{t("rejection_reason")}
|
||||||
</p>
|
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||||
<TextArea
|
</>
|
||||||
name={t("rejection_reason")}
|
}
|
||||||
value={rejectionReason}
|
value={rejectionReason}
|
||||||
onChange={(e) => setRejectionReason(e.target.value)}
|
onChange={(e) => setRejectionReason(e.target.value)}
|
||||||
className="mb-5 sm:mb-6"
|
/>
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import type { EventType } from "@prisma/client";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
|
const AvailableTimes = dynamic(() => import("@components/booking/AvailableTimes"));
|
||||||
|
|
||||||
|
const getRefetchInterval = (refetchCount: number): number => {
|
||||||
|
const intervals = [3000, 3000, 5000, 10000, 20000, 30000] as const;
|
||||||
|
return intervals[refetchCount] || intervals[intervals.length - 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSlots = ({
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
usernameList,
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
enabled = true,
|
||||||
|
}: {
|
||||||
|
eventTypeId: number;
|
||||||
|
eventTypeSlug: string;
|
||||||
|
startTime?: Dayjs;
|
||||||
|
endTime?: Dayjs;
|
||||||
|
usernameList: string[];
|
||||||
|
timeZone?: string;
|
||||||
|
duration?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [refetchCount, setRefetchCount] = useState(0);
|
||||||
|
const refetchInterval = getRefetchInterval(refetchCount);
|
||||||
|
const { data, isLoading, isPaused, fetchStatus } = trpc.viewer.public.slots.getSchedule.useQuery(
|
||||||
|
{
|
||||||
|
eventTypeId,
|
||||||
|
eventTypeSlug,
|
||||||
|
usernameList,
|
||||||
|
startTime: startTime?.toISOString() || "",
|
||||||
|
endTime: endTime?.toISOString() || "",
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!startTime && !!endTime && enabled,
|
||||||
|
refetchInterval,
|
||||||
|
trpc: { context: { skipBatch: true } },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!data && fetchStatus === "idle") {
|
||||||
|
setRefetchCount(refetchCount + 1);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [fetchStatus, data]);
|
||||||
|
|
||||||
|
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
||||||
|
return { slots: data?.slots || {}, isLoading: isLoading || isPaused };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SlotPicker = ({
|
||||||
|
eventType,
|
||||||
|
timeFormat,
|
||||||
|
onTimeFormatChange,
|
||||||
|
timeZone,
|
||||||
|
recurringEventCount,
|
||||||
|
users,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
weekStart = 0,
|
||||||
|
ethSignature,
|
||||||
|
}: {
|
||||||
|
eventType: Pick<
|
||||||
|
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||||
|
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
||||||
|
>;
|
||||||
|
timeFormat: TimeFormat;
|
||||||
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
seatsPerTimeSlot?: number;
|
||||||
|
recurringEventCount?: number;
|
||||||
|
users: string[];
|
||||||
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
ethSignature?: string;
|
||||||
|
}) => {
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||||
|
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||||
|
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
||||||
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!eventType.metadata?.multipleDuration) {
|
||||||
|
duration = eventType.length.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!router.isReady) return;
|
||||||
|
|
||||||
|
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
||||||
|
if (timeZone === "Etc/GMT") {
|
||||||
|
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
||||||
|
if (date) {
|
||||||
|
setSelectedDate(dayjs.utc(date));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Set the start of the month without shifting time like startOf() may do.
|
||||||
|
setBrowsingDate(
|
||||||
|
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
||||||
|
);
|
||||||
|
if (date) {
|
||||||
|
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
||||||
|
setSelectedDate(dayjs.tz(date, timeZone));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [router.isReady, month, date, duration, timeZone]);
|
||||||
|
|
||||||
|
const { i18n, isLocaleReady } = useLocale();
|
||||||
|
const { slots: monthSlots, isLoading } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime:
|
||||||
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||||
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||||
|
: browsingDate?.startOf("month"),
|
||||||
|
endTime: browsingDate?.endOf("month"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
const { slots: selectedDateSlots, isLoading: _isLoadingSelectedDateSlots } = useSlots({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
eventTypeSlug: eventType.slug,
|
||||||
|
usernameList: users,
|
||||||
|
startTime: selectedDate?.startOf("day"),
|
||||||
|
endTime: selectedDate?.endOf("day"),
|
||||||
|
timeZone,
|
||||||
|
duration,
|
||||||
|
/** Prevent refetching is we already have this data from month slots */
|
||||||
|
enabled: !!selectedDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Hide skeleton if we have the slot loaded in the month query */
|
||||||
|
const isLoadingSelectedDateSlots = (() => {
|
||||||
|
if (!selectedDate) return _isLoadingSelectedDateSlots;
|
||||||
|
if (!!selectedDateSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
if (!!monthSlots[selectedDate.format("YYYY-MM-DD")]) return false;
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DatePicker
|
||||||
|
isLoading={isLoading}
|
||||||
|
className={classNames(
|
||||||
|
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
|
||||||
|
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
||||||
|
)}
|
||||||
|
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||||
|
locale={isLocaleReady ? i18n.language : "en"}
|
||||||
|
selected={selectedDate}
|
||||||
|
onChange={(newDate) => {
|
||||||
|
setDate(newDate.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
|
onMonthChange={(newMonth) => {
|
||||||
|
setMonth(newMonth.format("YYYY-MM"));
|
||||||
|
}}
|
||||||
|
browsingDate={browsingDate}
|
||||||
|
weekStart={weekStart}
|
||||||
|
/>
|
||||||
|
<AvailableTimes
|
||||||
|
isLoading={isLoadingSelectedDateSlots}
|
||||||
|
slots={
|
||||||
|
selectedDate &&
|
||||||
|
(selectedDateSlots[selectedDate.format("YYYY-MM-DD")] ||
|
||||||
|
monthSlots[selectedDate.format("YYYY-MM-DD")])
|
||||||
|
}
|
||||||
|
date={selectedDate}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onTimeFormatChange={onTimeFormatChange}
|
||||||
|
eventTypeId={eventType.id}
|
||||||
|
eventTypeSlug={eventType.slug}
|
||||||
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
|
recurringCount={recurringEventCount}
|
||||||
|
ethSignature={ethSignature}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import type { FC } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
@@ -11,7 +15,8 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const { t } = useLocale();
|
const query = useMeQuery();
|
||||||
|
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -23,18 +28,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
const customStyles = {
|
||||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||||
<div className="mb-4 flex">
|
padding: "10px 12px !important",
|
||||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||||
</div>
|
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||||
<TimezoneSelect
|
...(userTheme === "dark" && {
|
||||||
id="timeZone"
|
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||||
value={selectedTimeZone}
|
}),
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
}),
|
||||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
control: () => ({
|
||||||
/>
|
display: "flex",
|
||||||
</div>
|
cursor: "pointer",
|
||||||
|
backgroundColor: "transparent !important",
|
||||||
|
minWidth: "5rem",
|
||||||
|
height: "24px",
|
||||||
|
minHeight: "24px !important",
|
||||||
|
boxShadow: "none !important",
|
||||||
|
}),
|
||||||
|
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||||
|
...provided,
|
||||||
|
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||||
|
...(userTheme === "dark" && {
|
||||||
|
color: "#a5a5a5 !important",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
menu: (provided: object) => ({
|
||||||
|
...provided,
|
||||||
|
minWidth: "20rem",
|
||||||
|
margin: "8px 0 0 -20px",
|
||||||
|
}),
|
||||||
|
valueContainer: (provided: object) => ({
|
||||||
|
...provided,
|
||||||
|
padding: "0 0 0 6px",
|
||||||
|
}),
|
||||||
|
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||||
|
...provided,
|
||||||
|
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
||||||
|
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||||
|
marginLeft: 4,
|
||||||
|
padding: 0,
|
||||||
|
...(userTheme === "dark" && {
|
||||||
|
color: "#80868B",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return !!selectedTimeZone ? (
|
||||||
|
<TimezoneSelect
|
||||||
|
id="timeZone"
|
||||||
|
value={selectedTimeZone}
|
||||||
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
|
className="flex h-6 text-sm font-medium"
|
||||||
|
styles={customStyles}
|
||||||
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FiGlobe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
|
import TimeOptions from "@components/booking/TimeOptions";
|
||||||
|
|
||||||
|
export function TimezoneDropdown({
|
||||||
|
onChangeTimeZone,
|
||||||
|
}: {
|
||||||
|
onChangeTimeZone: (newTimeZone: string) => void;
|
||||||
|
timeZone?: string;
|
||||||
|
}) {
|
||||||
|
const handleSelectTimeZone = (newTimeZone: string) => {
|
||||||
|
onChangeTimeZone(newTimeZone);
|
||||||
|
localStorageTimeZone(newTimeZone);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
||||||
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||||
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
import type { AvatarGroupProps } from "@calcom/ui";
|
||||||
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
|
|
||||||
export const UserAvatars = ({
|
export const UserAvatars = ({
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import dynamic from "next/dynamic";
|
||||||
import { EventType } from "@prisma/client";
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
@@ -17,238 +14,39 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { FiChevronDown, FiChevronUp, FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
|
import { FiCreditCard, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import Gates from "@components/Gates";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import TimeOptions from "@components/booking/TimeOptions";
|
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
const useSlots = ({
|
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
usernameList,
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
}: {
|
|
||||||
eventTypeId: number;
|
|
||||||
eventTypeSlug: string;
|
|
||||||
startTime?: Dayjs;
|
|
||||||
endTime?: Dayjs;
|
|
||||||
usernameList: string[];
|
|
||||||
timeZone?: string;
|
|
||||||
duration?: string;
|
|
||||||
}) => {
|
|
||||||
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
|
|
||||||
{
|
|
||||||
eventTypeId,
|
|
||||||
eventTypeSlug,
|
|
||||||
usernameList,
|
|
||||||
startTime: startTime?.toISOString() || "",
|
|
||||||
endTime: endTime?.toISOString() || "",
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!startTime && !!endTime,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
if (data?.slots) {
|
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||||
setCachedSlots((c) => ({ ...c, ...data?.slots }));
|
ssr: false,
|
||||||
}
|
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
|
||||||
}, [data]);
|
});*/
|
||||||
|
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
|
||||||
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
ssr: false,
|
||||||
return { slots: cachedSlots, isLoading: isLoading || isPaused };
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const SlotPicker = ({
|
|
||||||
eventType,
|
|
||||||
timeFormat,
|
|
||||||
onTimeFormatChange,
|
|
||||||
timeZone,
|
|
||||||
recurringEventCount,
|
|
||||||
users,
|
|
||||||
seatsPerTimeSlot,
|
|
||||||
weekStart = 0,
|
|
||||||
ethSignature,
|
|
||||||
}: {
|
|
||||||
eventType: Pick<
|
|
||||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
|
||||||
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
|
||||||
>;
|
|
||||||
timeFormat: TimeFormat;
|
|
||||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
seatsPerTimeSlot?: number;
|
|
||||||
recurringEventCount?: number;
|
|
||||||
users: string[];
|
|
||||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
ethSignature?: string;
|
|
||||||
}) => {
|
|
||||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
|
||||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
|
||||||
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
|
||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (!eventType.metadata?.multipleDuration) {
|
|
||||||
duration = eventType.length.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!router.isReady) return;
|
|
||||||
|
|
||||||
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
|
||||||
if (timeZone === "Etc/GMT") {
|
|
||||||
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
|
||||||
if (date) {
|
|
||||||
setSelectedDate(dayjs.utc(date));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Set the start of the month without shifting time like startOf() may do.
|
|
||||||
setBrowsingDate(
|
|
||||||
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
|
||||||
);
|
|
||||||
if (date) {
|
|
||||||
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
|
||||||
setSelectedDate(dayjs.tz(date, timeZone));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [router.isReady, month, date, duration, timeZone]);
|
|
||||||
|
|
||||||
const { i18n, isLocaleReady } = useLocale();
|
|
||||||
const { slots: _1 } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime: selectedDate?.startOf("day"),
|
|
||||||
endTime: selectedDate?.endOf("day"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
const { slots: _2, isLoading } = useSlots({
|
|
||||||
eventTypeId: eventType.id,
|
|
||||||
eventTypeSlug: eventType.slug,
|
|
||||||
usernameList: users,
|
|
||||||
startTime:
|
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
|
||||||
timeZone,
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DatePicker
|
|
||||||
isLoading={isLoading}
|
|
||||||
className={classNames(
|
|
||||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
|
|
||||||
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
|
||||||
)}
|
|
||||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
|
||||||
locale={isLocaleReady ? i18n.language : "en"}
|
|
||||||
selected={selectedDate}
|
|
||||||
onChange={(newDate) => {
|
|
||||||
setDate(newDate.format("YYYY-MM-DD"));
|
|
||||||
}}
|
|
||||||
onMonthChange={(newMonth) => {
|
|
||||||
setMonth(newMonth.format("YYYY-MM"));
|
|
||||||
}}
|
|
||||||
browsingDate={browsingDate}
|
|
||||||
weekStart={weekStart}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref={slotPickerRef}>
|
|
||||||
<AvailableTimes
|
|
||||||
isLoading={isLoading}
|
|
||||||
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
|
|
||||||
date={selectedDate}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onTimeFormatChange={onTimeFormatChange}
|
|
||||||
eventTypeId={eventType.id}
|
|
||||||
eventTypeSlug={eventType.slug}
|
|
||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
|
||||||
recurringCount={recurringEventCount}
|
|
||||||
ethSignature={ethSignature}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TimezoneDropdown({
|
|
||||||
onChangeTimeZone,
|
|
||||||
timeZone,
|
|
||||||
}: {
|
|
||||||
onChangeTimeZone: (newTimeZone: string) => void;
|
|
||||||
timeZone?: string;
|
|
||||||
}) {
|
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleSelectTimeZone = (newTimeZone: string) => {
|
|
||||||
onChangeTimeZone(newTimeZone);
|
|
||||||
localStorageTimeZone(newTimeZone);
|
|
||||||
setIsTimeOptionsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
|
||||||
<p className="flex items-center text-sm font-medium">
|
|
||||||
<FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
|
|
||||||
{timeZone}
|
|
||||||
{isTimeOptionsOpen ? (
|
|
||||||
<FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
) : (
|
|
||||||
<FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Popover.Trigger>
|
|
||||||
<Popover.Portal>
|
|
||||||
<Popover.Content
|
|
||||||
hideWhenDetached
|
|
||||||
align="start"
|
|
||||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Portal>
|
|
||||||
</Popover.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateQuerySchema = z.object({
|
const dateQuerySchema = z.object({
|
||||||
rescheduleUid: z.string().optional().default(""),
|
rescheduleUid: z.string().optional().default(""),
|
||||||
@@ -311,7 +109,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
@@ -362,10 +160,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isBackgroundTransparent
|
isBackgroundTransparent
|
||||||
? ""
|
? ""
|
||||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
||||||
"border-bookinglightest overflow-hidden md:rounded-md md:border",
|
"border-bookinglightest md:rounded-md md:border",
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="md:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -399,14 +197,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{stripeAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={paymentAppData.price / 100.0}
|
||||||
style="currency"
|
style="currency"
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import { useSession } from "next-auth/react";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
|
import { useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
getEventLocationType,
|
||||||
getEventLocationValue,
|
getEventLocationValue,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
@@ -21,7 +21,8 @@ import {
|
|||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||||
import { LocationObject, LocationType } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
|
import { LocationType } from "@calcom/core/location";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
@@ -32,7 +33,7 @@ import {
|
|||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
@@ -69,12 +70,13 @@ import createBooking from "@lib/mutations/bookings/create-booking";
|
|||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
|
import Gates from "@components/Gates";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
|
|
||||||
import { BookPageProps } from "../../../pages/[user]/book";
|
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||||
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||||
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||||
|
|
||||||
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (
|
if (
|
||||||
@@ -148,6 +150,7 @@ const BookingPage = ({
|
|||||||
const mutation = useMutation(createBooking, {
|
const mutation = useMutation(createBooking, {
|
||||||
onSuccess: async (responseData) => {
|
onSuccess: async (responseData) => {
|
||||||
const { uid, paymentUid } = responseData;
|
const { uid, paymentUid } = responseData;
|
||||||
|
|
||||||
if (paymentUid) {
|
if (paymentUid) {
|
||||||
return await router.push(
|
return await router.push(
|
||||||
createPaymentLink({
|
createPaymentLink({
|
||||||
@@ -166,7 +169,7 @@ const BookingPage = ({
|
|||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
formerTime: booking?.startTime.toString(),
|
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -557,14 +560,14 @@ const BookingPage = ({
|
|||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
{stripeAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={paymentAppData.price / 100.0}
|
||||||
style="currency"
|
style="currency"
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
currency={paymentAppData?.currency?.toUpperCase()}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -7,23 +7,24 @@ import { useEffect } from "react";
|
|||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
||||||
import {
|
import {
|
||||||
EventLocationType,
|
|
||||||
getEventLocationType,
|
getEventLocationType,
|
||||||
getHumanReadableLocationValue,
|
getHumanReadableLocationValue,
|
||||||
getMessageForOrganizer,
|
getMessageForOrganizer,
|
||||||
LocationObject,
|
|
||||||
LocationType,
|
LocationType,
|
||||||
} from "@calcom/app-store/locations";
|
} from "@calcom/app-store/locations";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
|
import type { LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import { components, OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
@@ -99,100 +100,106 @@ const format = (date: Date, hour12: boolean) =>
|
|||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
const EventTypeScheduleDetails = () => {
|
||||||
|
const { data: loggedInUser } = useMeQuery();
|
||||||
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
|
||||||
const me = useMeQuery();
|
|
||||||
const timeFormat = me?.data?.timeFormat;
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<div className="min-w-4 mb-2">
|
|
||||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
|
||||||
{t("availability")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => (
|
|
||||||
<AvailabilitySelect
|
|
||||||
value={field.value}
|
|
||||||
onBlur={field.onBlur}
|
|
||||||
name={field.name}
|
|
||||||
onChange={(selected) => {
|
|
||||||
field.onChange(selected?.value || null);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
|
||||||
<ol className="table border-collapse text-sm">
|
|
||||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
|
||||||
const isAvailable = !!filterDays(index).length;
|
|
||||||
return (
|
|
||||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"w-20 font-medium sm:w-32",
|
|
||||||
!isAvailable && "text-gray-500 opacity-50"
|
|
||||||
)}>
|
|
||||||
{day}
|
|
||||||
</span>
|
|
||||||
{isLoading ? (
|
|
||||||
<SkeletonText className="block h-5 w-60" />
|
|
||||||
) : isAvailable ? (
|
|
||||||
<div className="space-y-3 text-right">
|
|
||||||
{filterDays(index).map((dayRange, i) => (
|
|
||||||
<div key={i} className="flex items-center leading-4">
|
|
||||||
<span className="w-16 sm:w-28 sm:text-left">
|
|
||||||
{format(dayRange.startTime, timeFormat === 12)}
|
|
||||||
</span>
|
|
||||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
<hr />
|
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
|
||||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
|
||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
href={`/availability/${schedule?.schedule.id}`}
|
|
||||||
color="minimal"
|
|
||||||
EndIcon={FiExternalLink}
|
|
||||||
target="_blank"
|
|
||||||
className="justify-center border sm:border-0"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{t("edit_availability")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
|
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||||
|
);
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
const filterDays = (dayNum: number) =>
|
||||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||||
|
|
||||||
if (!isTeamEvent) {
|
return (
|
||||||
return <EventTypeSchedule />;
|
<div className="space-y-4 rounded border px-6 pb-4">
|
||||||
}
|
<ol className="table border-collapse text-sm">
|
||||||
|
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||||
|
const isAvailable = !!filterDays(index).length;
|
||||||
|
return (
|
||||||
|
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"w-20 font-medium sm:w-32",
|
||||||
|
!isAvailable && "text-gray-500 opacity-50"
|
||||||
|
)}>
|
||||||
|
{day}
|
||||||
|
</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonText className="block h-5 w-60" />
|
||||||
|
) : isAvailable ? (
|
||||||
|
<div className="space-y-3 text-right">
|
||||||
|
{filterDays(index).map((dayRange, i) => (
|
||||||
|
<div key={i} className="flex items-center leading-4">
|
||||||
|
<span className="w-16 sm:w-28 sm:text-left">
|
||||||
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
|
</span>
|
||||||
|
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||||
|
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
<hr />
|
||||||
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
|
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||||
|
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||||
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
href={`/availability/${schedule?.schedule.id}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
color="minimal"
|
||||||
|
EndIcon={FiExternalLink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
{t("edit_availability")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EventTypeSchedule = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
|
||||||
|
{t("availability")}
|
||||||
|
</label>
|
||||||
|
<Controller
|
||||||
|
name="schedule"
|
||||||
|
render={({ field }) => (
|
||||||
|
<AvailabilitySelect
|
||||||
|
value={field.value}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
name={field.name}
|
||||||
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<EventTypeScheduleDetails />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UseCommonScheduleSettingsToggle = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { resetField, setValue } = useFormContext<FormValues>();
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||||
@@ -201,6 +208,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
checked={!value}
|
checked={!value}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
onChange(!checked);
|
onChange(!checked);
|
||||||
|
if (checked) {
|
||||||
|
resetField("schedule");
|
||||||
|
} else {
|
||||||
|
setValue("schedule", null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title={t("choose_common_schedule_team_event")}
|
title={t("choose_common_schedule_team_event")}
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
@@ -210,3 +222,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||||
|
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInputType } from "@prisma/client";
|
import { EventTypeCustomInputType } from "@prisma/client";
|
||||||
import type { CustomInputParsed } from "pages/event-types/[type]";
|
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||||
import { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
import type { Control, UseFormRegister } from "react-hook-form";
|
||||||
|
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Label, Select, TextField } from "@calcom/ui";
|
import { Button, Label, Select, TextField } from "@calcom/ui";
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
const removeCustom = (index: number) => {
|
||||||
formMethods.getValues("customInputs").splice(index, 1);
|
formMethods.getValues("customInputs").splice(index, 1);
|
||||||
@@ -408,7 +408,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
title={t("custom_event_name")}
|
title={t("custom_event_name")}
|
||||||
description={t("custom_event_name_description")}
|
description={t("custom_event_name_description")}
|
||||||
type="creation">
|
type="creation"
|
||||||
|
enableOverflow>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, EmptyScreen } from "@calcom/ui";
|
import { Button, EmptyScreen } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||||
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
|
|
||||||
import RecurringEventController from "./RecurringEventController";
|
import RecurringEventController from "./RecurringEventController";
|
||||||
|
|
||||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
const requirePayment = stripeAppData.price > 0;
|
const requirePayment = paymentAppData.price > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import Link from "next/link";
|
|||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
@@ -17,10 +18,8 @@ import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@cal
|
|||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import LocationSelect, {
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
SingleValueLocationOption,
|
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||||
LocationOption,
|
|
||||||
} from "@components/ui/form/LocationSelect";
|
|
||||||
|
|
||||||
const getLocationFromType = (
|
const getLocationFromType = (
|
||||||
type: EventLocationType["type"],
|
type: EventLocationType["type"],
|
||||||
@@ -34,6 +33,16 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||||
|
for (const locationType of options) {
|
||||||
|
for (const location of locationType.options) {
|
||||||
|
if (location.value === type && location.disabled === false) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||||
) => {
|
) => {
|
||||||
@@ -66,10 +75,15 @@ export const EventSetupTab = (
|
|||||||
setShowLocationModal(true);
|
setShowLocationModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type),
|
formMethods.getValues("locations").filter((location) => {
|
||||||
|
if (location.type === LocationType.InPerson) {
|
||||||
|
return location.address !== selectedLocation.address;
|
||||||
|
}
|
||||||
|
return location.type !== selectedLocation.type;
|
||||||
|
}),
|
||||||
{ shouldValidate: true }
|
{ shouldValidate: true }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -84,14 +98,14 @@ export const EventSetupTab = (
|
|||||||
...details,
|
...details,
|
||||||
type: newLocationType,
|
type: newLocationType,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
copy[existingIdx] = {
|
|
||||||
...formMethods.getValues("locations")[existingIdx],
|
|
||||||
...details,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formMethods.setValue("locations", copy);
|
formMethods.setValue("locations", [
|
||||||
|
...copy,
|
||||||
|
...(newLocationType === LocationType.InPerson && editingLocationType === ""
|
||||||
|
? [{ ...details, type: newLocationType }]
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
"locations",
|
"locations",
|
||||||
@@ -138,13 +152,11 @@ export const EventSetupTab = (
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultValue = locationOptions.find((item) => item.label === "video")?.options;
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LocationSelect
|
<LocationSelect
|
||||||
defaultValue={defaultValue}
|
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
@@ -175,8 +187,16 @@ export const EventSetupTab = (
|
|||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// We dont want to translate the string link - it doesnt exist in common.json and it gets prefixed/suffixed with __ or //
|
||||||
|
const eventLabel =
|
||||||
|
eventLocationType.defaultValueVariable === "link"
|
||||||
|
? eventLocationType.label
|
||||||
|
: t(location[eventLocationType.defaultValueVariable] || eventLocationType.label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
<li
|
||||||
|
key={`${location.type}${index}`}
|
||||||
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||||
<div className="flex max-w-full justify-between">
|
<div className="flex max-w-full justify-between">
|
||||||
<div key={index} className="flex flex-grow items-center">
|
<div key={index} className="flex flex-grow items-center">
|
||||||
<img
|
<img
|
||||||
@@ -184,9 +204,7 @@ export const EventSetupTab = (
|
|||||||
className="h-4 w-4"
|
className="h-4 w-4"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
||||||
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Options } from "react-select";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
import type { Options } from "react-select";
|
||||||
|
|
||||||
import CheckedTeamSelect, {
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
CheckedSelectOption,
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
|
||||||
import { WEBAPP_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 { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
|
if (a.label < b.label) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.label > b.label) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
const FixedHosts = ({
|
const FixedHosts = ({
|
||||||
control,
|
control,
|
||||||
labelText,
|
labelText,
|
||||||
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
];
|
];
|
||||||
|
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && (
|
||||||
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
|
|
||||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
placeholder={t("add_attendees")}
|
placeholder={t("add_attendees")}
|
||||||
labelText={t("team")}
|
labelText={t("team")}
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||||
<>
|
<>
|
||||||
<FixedHosts
|
<FixedHosts
|
||||||
options={teamMembersOptions}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
)
|
)
|
||||||
.filter(Boolean)}
|
.filter(Boolean)}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
options={teamMembers.map(mapUserToValue)}
|
options={teamMembersOptions.sort(sortByLabel)}
|
||||||
isOptionDisabled={(option) =>
|
isOptionDisabled={(option) =>
|
||||||
!!formMethods
|
!!formMethods
|
||||||
.getValues("hostsFixed")
|
.getValues("hostsFixed")
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Webhook } from "@calcom/prisma/client";
|
import type { Webhook } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
@@ -100,7 +100,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<>
|
<>
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 mb-8 rounded-md border">
|
<div className="mb-8 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState, Suspense } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
@@ -286,6 +286,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
|
color="destructive"
|
||||||
StartIcon={FiTrash}
|
StartIcon={FiTrash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
@@ -338,7 +339,7 @@ function EventTypeSingleLayout({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { UnitTypeLongPlural } from "dayjs";
|
import type { UnitTypeLongPlural } from "dayjs";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterInputs, trpc } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { useForm } from "react-hook-form";
|
|||||||
import { Schedule } from "@calcom/features/schedules";
|
import { Schedule } from "@calcom/features/schedules";
|
||||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Form } from "@calcom/ui";
|
import { Button, Form } from "@calcom/ui";
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { FormEvent, useRef, useState } from "react";
|
import type { FormEvent } from "react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
@@ -34,6 +36,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: async (_data, context) => {
|
onSuccess: async (_data, context) => {
|
||||||
@@ -64,6 +67,8 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const onSubmit = handleSubmit((data: { bio: string }) => {
|
const onSubmit = handleSubmit((data: { bio: string }) => {
|
||||||
const { bio } = data;
|
const { bio } = data;
|
||||||
|
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingFinished);
|
||||||
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
bio,
|
bio,
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
const { user, nextStep } = props;
|
const { user, nextStep } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
|
const telemetry = useTelemetry();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -31,6 +32,11 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
},
|
},
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||||
|
}, [telemetry]);
|
||||||
|
|
||||||
const defaultOptions = { required: true, maxLength: 255 };
|
const defaultOptions = { required: true, maxLength: 255 };
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { SyntheticEvent, useState } from "react";
|
import type { SyntheticEvent } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge, Button } from "@calcom/ui";
|
import { Badge, Button } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { BaseSyntheticEvent, useState } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
|||||||
+30
-10
@@ -1,17 +1,39 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import React from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
|
import { FiUserCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||||
const router = useRouter();
|
const { t } = useLocale();
|
||||||
|
if (props.userCount > 0)
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="wizard-step-1"
|
||||||
|
name="wizard-step-1"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
props.onSuccess();
|
||||||
|
}}>
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={FiUserCheck}
|
||||||
|
headline={t("admin_user_created")}
|
||||||
|
description={t("admin_user_created_description")}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
return <AdminUser {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onError = () => {
|
const onError = () => {
|
||||||
props.setIsLoading(false);
|
props.onError();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||||
props.setIsLoading(true);
|
props.onSubmit();
|
||||||
const response = await fetch("/api/auth/setup", {
|
const response = await fetch("/api/auth/setup", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
email: data.email_address.toLowerCase(),
|
email: data.email_address.toLowerCase(),
|
||||||
password: data.password,
|
password: data.password,
|
||||||
});
|
});
|
||||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
props.onSuccess();
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
props.onError();
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SetupFormStep1;
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
|
||||||
|
|
||||||
|
const ChooseLicense = (
|
||||||
|
props: {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onSubmit: (value: string) => void;
|
||||||
|
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
|
||||||
|
) => {
|
||||||
|
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
{...rest}
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSubmit(value);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Root
|
||||||
|
defaultValue={initialValue}
|
||||||
|
value={value}
|
||||||
|
aria-label={t("choose_a_license")}
|
||||||
|
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
|
||||||
|
onValueChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
setValue(value);
|
||||||
|
}}>
|
||||||
|
<RadioGroup.Item value="FREE">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||||
|
value === "FREE" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
|
||||||
|
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||||
|
<p className="text-gray-500">{t("forever_open_and_free")}</p>
|
||||||
|
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||||
|
<li>{t("required_to_keep_your_code_open_source")}</li>
|
||||||
|
<li>{t("cannot_repackage_and_resell")}</li>
|
||||||
|
<li>{t("no_enterprise_features")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<RadioGroup.Item value="EE">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||||
|
value === "EE" && "ring-2 ring-black"
|
||||||
|
)}>
|
||||||
|
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
|
||||||
|
<p className="font-medium text-green-800">
|
||||||
|
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
|
||||||
|
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||||
|
<li>{t("no_need_to_keep_your_code_open_source")}</li>
|
||||||
|
<li>{t("repackage_rebrand_resell")}</li>
|
||||||
|
<li>{t("a_vast_suite_of_enterprise_features")}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChooseLicense;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button, TextField } from "@calcom/ui";
|
||||||
|
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
type EnterpriseLicenseFormValues = {
|
||||||
|
licenseKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
|
||||||
|
z.object({
|
||||||
|
licenseKey: z
|
||||||
|
.string()
|
||||||
|
.uuid({
|
||||||
|
message: "License key must follow UUID format: 8-4-4-4-12",
|
||||||
|
})
|
||||||
|
.superRefine(async (data, ctx) => {
|
||||||
|
const parse = z.string().uuid().safeParse(data);
|
||||||
|
if (parse.success) {
|
||||||
|
args.callback(true);
|
||||||
|
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
|
||||||
|
args.callback(false);
|
||||||
|
const json = await response.json();
|
||||||
|
if (!json.valid) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: `License key ${json.message.toLowerCase()}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
args.onSuccessValidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const EnterpriseLicense = (
|
||||||
|
props: {
|
||||||
|
licenseKey?: string;
|
||||||
|
initialValue?: Partial<EnterpriseLicenseFormValues>;
|
||||||
|
onSuccessValidate: () => void;
|
||||||
|
onSubmit: (value: EnterpriseLicenseFormValues) => void;
|
||||||
|
onSuccess?: (
|
||||||
|
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
|
||||||
|
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
|
||||||
|
) => void;
|
||||||
|
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
|
||||||
|
) => {
|
||||||
|
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
|
||||||
|
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
|
||||||
|
onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
const schemaLicenseKey = useCallback(
|
||||||
|
() =>
|
||||||
|
makeSchemaLicenseKey({
|
||||||
|
callback: setCheckLicenseLoading,
|
||||||
|
onSuccessValidate,
|
||||||
|
}),
|
||||||
|
[setCheckLicenseLoading, onSuccessValidate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formMethods = useForm<EnterpriseLicenseFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
licenseKey: props.licenseKey || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schemaLicenseKey()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = formMethods.handleSubmit((values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
setCheckLicenseLoading(false);
|
||||||
|
mutation.mutate(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isDirty, errors } = useFormState(formMethods);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...formMethods}>
|
||||||
|
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="w-full justify-center text-lg"
|
||||||
|
EndIcon={FiExternalLink}
|
||||||
|
href="https://console.cal.com"
|
||||||
|
target="_blank">
|
||||||
|
{t("purchase_license")}
|
||||||
|
</Button>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
|
||||||
|
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
|
||||||
|
</div>
|
||||||
|
{t("already_have_key")}
|
||||||
|
<Controller
|
||||||
|
name="licenseKey"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { onBlur, onChange, value } }) => (
|
||||||
|
<TextField
|
||||||
|
{...formMethods.register("licenseKey")}
|
||||||
|
className={classNames(
|
||||||
|
"mb-0 group-hover:border-gray-400",
|
||||||
|
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||||
|
)}
|
||||||
|
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
||||||
|
labelSrOnly={true}
|
||||||
|
value={value}
|
||||||
|
addOnFilled={false}
|
||||||
|
addOnClassname={classNames(
|
||||||
|
"hover:border-gray-300",
|
||||||
|
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
|
||||||
|
)}
|
||||||
|
addOnSuffix={
|
||||||
|
checkLicenseLoading ? (
|
||||||
|
<FiLoader className="h-5 w-5 animate-spin" />
|
||||||
|
) : errors.licenseKey === undefined && isDirty ? (
|
||||||
|
<FiCheck className="h-5 w-5 text-green-700" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
color={errors.licenseKey ? "warn" : ""}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
formMethods.setValue("licenseKey", e.target.value);
|
||||||
|
await formMethods.trigger("licenseKey");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnterpriseLicense;
|
||||||
+11
-5
@@ -1,20 +1,26 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiCheck } from "@calcom/ui/components/icon";
|
import { FiCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const StepDone = () => {
|
const StepDone = (props: {
|
||||||
|
currentStep: number;
|
||||||
|
nextStepPath: string;
|
||||||
|
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
id="wizard-step-1"
|
id={`wizard-step-${props.currentStep}`}
|
||||||
name="wizard-step-1"
|
name={`wizard-step-${props.currentStep}`}
|
||||||
className="space-y-4"
|
className="flex justify-center space-y-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
|
props.setIsLoading(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
router.replace(props.nextStepPath);
|
||||||
}}>
|
}}>
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TeamPageProps } from "pages/team/[slug]";
|
import type { TeamPageProps } from "pages/team/[slug]";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||||
|
|
||||||
type TeamType = TeamPageProps["team"];
|
type TeamType = TeamPageProps["team"];
|
||||||
@@ -20,7 +19,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={`/${member.username}`}>
|
<Link key={member.id} href={`/${member.username}`}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size="md"
|
||||||
alt={member.name || ""}
|
alt={member.name || ""}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { FC, Fragment } from "react";
|
import type { FC } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
type AdminRequiredProps = {
|
type AdminRequiredProps = {
|
||||||
as?: keyof JSX.IntrinsicElements;
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ControllerRenderProps } from "react-hook-form";
|
import type { ControllerRenderProps } from "react-hook-form";
|
||||||
|
|
||||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
<FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
|
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
{...passThroughProps}
|
{...passThroughProps}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||||
|
|
||||||
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
Icon: SVGComponent;
|
Icon: SVGComponent;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { Dialog, DialogContent } from "@calcom/ui";
|
import { Dialog, DialogContent } from "@calcom/ui";
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import type { RefCallback } from "react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { RefCallback, useEffect, useMemo, useState } from "react";
|
import type { RefCallback } from "react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
import { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
import { PremiumTextfield } from "./PremiumTextfield";
|
import { PremiumTextfield } from "./PremiumTextfield";
|
||||||
import { UsernameTextfield } from "./UsernameTextfield";
|
import { UsernameTextfield } from "./UsernameTextfield";
|
||||||
@@ -21,7 +24,13 @@ export const UsernameAvailabilityField = ({
|
|||||||
onErrorMutation,
|
onErrorMutation,
|
||||||
user,
|
user,
|
||||||
}: UsernameAvailabilityFieldProps) => {
|
}: UsernameAvailabilityFieldProps) => {
|
||||||
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
|
const router = useRouter();
|
||||||
|
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||||
|
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||||
|
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||||
|
router.query["username"] && user.username === null
|
||||||
|
? { username: usernameFromQuery, setQuery: setUsernameFromQuery }
|
||||||
|
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
|
||||||
const formMethods = useForm({
|
const formMethods = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: currentUsername,
|
username: currentUsername,
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
|
||||||
|
|
||||||
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
|
|
||||||
import { Swatch } from "@calcom/ui";
|
|
||||||
|
|
||||||
type Handler = (event: MouseEvent | Event) => void;
|
|
||||||
function useEventListener<
|
|
||||||
KW extends keyof WindowEventMap,
|
|
||||||
KH extends keyof HTMLElementEventMap,
|
|
||||||
T extends HTMLElement | void = void
|
|
||||||
>(
|
|
||||||
eventName: KW | KH,
|
|
||||||
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
|
||||||
element?: React.RefObject<T>
|
|
||||||
) {
|
|
||||||
// Create a ref that stores handler
|
|
||||||
const savedHandler = useRef<typeof handler>();
|
|
||||||
useEffect(() => {
|
|
||||||
// Define the listening target
|
|
||||||
const targetElement: T | Window = element?.current || window;
|
|
||||||
if (!(targetElement && targetElement.addEventListener)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Update saved handler if necessary
|
|
||||||
if (savedHandler.current !== handler) {
|
|
||||||
savedHandler.current = handler;
|
|
||||||
}
|
|
||||||
// Create event listener that calls handler function stored in ref
|
|
||||||
const eventListener: typeof handler = (event) => {
|
|
||||||
// eslint-disable-next-line no-extra-boolean-cast
|
|
||||||
if (!!savedHandler?.current) {
|
|
||||||
savedHandler.current(event);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
targetElement.addEventListener(eventName, eventListener);
|
|
||||||
// Remove event listener on cleanup
|
|
||||||
return () => {
|
|
||||||
targetElement.removeEventListener(eventName, eventListener);
|
|
||||||
};
|
|
||||||
}, [eventName, element, handler]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
|
||||||
ref: React.RefObject<T>,
|
|
||||||
handler: Handler,
|
|
||||||
mouseEvent: "mousedown" | "mouseup" = "mousedown"
|
|
||||||
): void {
|
|
||||||
useEventListener(mouseEvent, (event) => {
|
|
||||||
const el = ref?.current;
|
|
||||||
// Do nothing if clicking ref's element or descendent elements
|
|
||||||
if (!el || el.contains(event.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handler(event);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export type ColorPickerProps = {
|
|
||||||
defaultValue: string;
|
|
||||||
onChange: (text: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ColorPicker = (props: ColorPickerProps) => {
|
|
||||||
const init = !isValidHexCode(props.defaultValue)
|
|
||||||
? fallBackHex(props.defaultValue, false)
|
|
||||||
: props.defaultValue;
|
|
||||||
const [color, setColor] = useState(init);
|
|
||||||
const [isOpen, toggle] = useState(false);
|
|
||||||
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
const close = useCallback(() => toggle(false), []);
|
|
||||||
useOnClickOutside(popover, close);
|
|
||||||
return (
|
|
||||||
<div className="relative mt-1 flex items-center justify-center">
|
|
||||||
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="popover" ref={popover}>
|
|
||||||
<HexColorPicker
|
|
||||||
className="!absolute !top-10 !left-0 !z-10 !h-32 !w-32"
|
|
||||||
color={color}
|
|
||||||
onChange={(val) => {
|
|
||||||
setColor(val);
|
|
||||||
props.onChange(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<HexColorInput
|
|
||||||
className="ml-1 block w-full rounded-r-md border border-gray-300 px-3 py-2 sm:text-sm"
|
|
||||||
color={color}
|
|
||||||
onChange={(val) => {
|
|
||||||
setColor(val);
|
|
||||||
props.onChange(val);
|
|
||||||
}}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ColorPicker;
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { forwardRef, InputHTMLAttributes } from "react";
|
import type { InputHTMLAttributes } from "react";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Props } from "react-select";
|
import type { Props } from "react-select";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { components, GroupBase, Props, SingleValue } from "react-select";
|
import type { GroupBase, Props, SingleValue } from "react-select";
|
||||||
|
import { components } from "react-select";
|
||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { forwardRef, InputHTMLAttributes, ReactNode } from "react";
|
import type { InputHTMLAttributes, ReactNode } from "react";
|
||||||
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
import type { GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
||||||
|
import ReactSelect, { components } from "react-select";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
@@ -28,7 +29,8 @@ function Select<
|
|||||||
IsMulti extends boolean = false,
|
IsMulti extends boolean = false,
|
||||||
Group extends GroupBase<Option> = GroupBase<Option>
|
Group extends GroupBase<Option> = GroupBase<Option>
|
||||||
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||||
const { resolvedTheme, forcedTheme, isReady } = useTheme();
|
const [mounted, setMounted] = useState<boolean>(false);
|
||||||
|
const { resolvedTheme, forcedTheme } = useTheme();
|
||||||
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
||||||
const darkThemeColors = {
|
const darkThemeColors = {
|
||||||
/** Dark Theme starts */
|
/** Dark Theme starts */
|
||||||
@@ -72,10 +74,15 @@ function Select<
|
|||||||
/** Dark Theme ends */
|
/** Dark Theme ends */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Till we know in JS the theme is ready, we can't render react-select as it would render with light theme instead
|
// Till we know in JS the theme is ready, we can't render react-select as it would render with light theme instead
|
||||||
if (!isReady) {
|
if (!mounted) {
|
||||||
return <input type="text" className={className} />;
|
return <input type="text" className={className} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
theme={(theme) => ({
|
theme={(theme) => ({
|
||||||
|
|||||||
Binary file not shown.
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import type {
|
||||||
QueryObserverLoadingErrorResult,
|
QueryObserverLoadingErrorResult,
|
||||||
QueryObserverLoadingResult,
|
QueryObserverLoadingResult,
|
||||||
QueryObserverRefetchErrorResult,
|
QueryObserverRefetchErrorResult,
|
||||||
QueryObserverSuccessResult,
|
QueryObserverSuccessResult,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
@@ -88,7 +88,7 @@ const withQuery = <
|
|||||||
TInput = inferProcedureInput<TQuery>,
|
TInput = inferProcedureInput<TQuery>,
|
||||||
TOutput = inferProcedureOutput<TQuery>
|
TOutput = inferProcedureOutput<TQuery>
|
||||||
>(
|
>(
|
||||||
queryProcedure: DecorateProcedure<TQuery, any>,
|
queryProcedure: DecorateProcedure<TQuery, any, any>,
|
||||||
input?: TInput,
|
input?: TInput,
|
||||||
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
|
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { EventCollectionProvider } from "next-collect/client";
|
import { EventCollectionProvider } from "next-collect/client";
|
||||||
import { appWithTranslation, SSRConfig } from "next-i18next";
|
import type { SSRConfig } from "next-i18next";
|
||||||
|
import { appWithTranslation } from "next-i18next";
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
||||||
import { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { ComponentProps, ReactNode } from "react";
|
import type { ComponentProps, ReactNode } from "react";
|
||||||
|
|
||||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||||
@@ -13,18 +14,20 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import { MetaProvider } from "@calcom/ui";
|
import { MetaProvider } from "@calcom/ui";
|
||||||
|
|
||||||
import usePublicPage from "@lib/hooks/usePublicPage";
|
import usePublicPage from "@lib/hooks/usePublicPage";
|
||||||
|
import type { WithNonceProps } from "@lib/withNonce";
|
||||||
|
|
||||||
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
|
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
|
||||||
({ children }) => <>{children}</>
|
({ children }) => <>{children}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = Omit<NextAppProps, "Component"> & {
|
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
||||||
Component: NextAppProps["Component"] & {
|
Component: NextAppProps["Component"] & {
|
||||||
requiresLicense?: boolean;
|
requiresLicense?: boolean;
|
||||||
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||||
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
err?: Error;
|
err?: Error;
|
||||||
};
|
};
|
||||||
@@ -77,6 +80,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
|
nonce={props.pageProps.nonce}
|
||||||
enableColorScheme={false}
|
enableColorScheme={false}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
forcedTheme={forcedTheme}
|
forcedTheme={forcedTheme}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import { compare, hash } from "bcryptjs";
|
import { compare, hash } from "bcryptjs";
|
||||||
import { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { getSession as getSessionInner, GetSessionParams } from "next-auth/react";
|
import type { GetSessionParams } from "next-auth/react";
|
||||||
|
import { getSession as getSessionInner } from "next-auth/react";
|
||||||
|
|
||||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||||
export async function hashPassword(password: string) {
|
export async function hashPassword(password: string) {
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Account, IdentityProvider, Prisma, PrismaClient, User, VerificationToken } from "@prisma/client";
|
import type {
|
||||||
|
Account,
|
||||||
|
IdentityProvider,
|
||||||
|
Prisma,
|
||||||
|
PrismaClient,
|
||||||
|
User,
|
||||||
|
VerificationToken,
|
||||||
|
} from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
|
|
||||||
import { identityProviderNameMap } from "@lib/auth";
|
import { identityProviderNameMap } from "@lib/auth";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DefaultSeoProps, NextSeoProps } from "next-seo";
|
import type { DefaultSeoProps, NextSeoProps } from "next-seo";
|
||||||
|
|
||||||
import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import parser from "accept-language-parser";
|
import parser from "accept-language-parser";
|
||||||
import { IncomingMessage } from "http";
|
import type { IncomingMessage } from "http";
|
||||||
|
|
||||||
import { Maybe } from "@calcom/trpc/server";
|
import type { Maybe } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import type { IncomingMessage, OutgoingMessage } from "http";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
function getCspPolicy(nonce: string) {
|
||||||
|
//TODO: Do we need to explicitly define it in turbo.json
|
||||||
|
const CSP_POLICY = process.env.CSP_POLICY;
|
||||||
|
|
||||||
|
// Note: "non-strict" policy only allows inline styles otherwise it's the same as "strict"
|
||||||
|
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||||
|
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||||
|
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||||
|
|
||||||
|
return `
|
||||||
|
default-src 'self' ${IS_PRODUCTION ? "" : "data:"};
|
||||||
|
script-src ${
|
||||||
|
IS_PRODUCTION
|
||||||
|
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
|
||||||
|
"'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:"
|
||||||
|
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
|
||||||
|
"'unsafe-inline' 'unsafe-eval' https: http:"
|
||||||
|
};
|
||||||
|
object-src 'none';
|
||||||
|
base-uri 'none';
|
||||||
|
child-src app.cal.com;
|
||||||
|
style-src 'self' ${
|
||||||
|
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
|
||||||
|
} app.cal.com;
|
||||||
|
font-src 'self';
|
||||||
|
img-src 'self' https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from @next-safe/middleware
|
||||||
|
const isPagePathRequest = (url: URL) => {
|
||||||
|
const isNonPagePathPrefix = /^\/(?:_next|api)\//;
|
||||||
|
const isFile = /\..*$/;
|
||||||
|
const { pathname } = url;
|
||||||
|
return !isNonPagePathPrefix.test(pathname) && !isFile.test(pathname);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) {
|
||||||
|
if (!req) {
|
||||||
|
return { nonce: undefined };
|
||||||
|
}
|
||||||
|
const existingNonce = req.headers["x-nonce"];
|
||||||
|
if (existingNonce) {
|
||||||
|
const existingNoneParsed = z.string().safeParse(existingNonce);
|
||||||
|
return { nonce: existingNoneParsed.success ? existingNoneParsed.data : "" };
|
||||||
|
}
|
||||||
|
if (!req.url) {
|
||||||
|
return { nonce: undefined };
|
||||||
|
}
|
||||||
|
const CSP_POLICY = process.env.CSP_POLICY;
|
||||||
|
const cspEnabledForInstance = CSP_POLICY;
|
||||||
|
const nonce = crypto.randomBytes(16).toString("base64");
|
||||||
|
|
||||||
|
const parsedUrl = new URL(req.url, "http://base_url");
|
||||||
|
const cspEnabledForPage = cspEnabledForInstance && isPagePathRequest(parsedUrl);
|
||||||
|
if (!cspEnabledForPage) {
|
||||||
|
return {
|
||||||
|
nonce: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Set x-nonce request header to be used by `getServerSideProps` or similar fns and `Document.getInitialProps` to read the nonce from
|
||||||
|
// It is generated for all page requests but only used by pages that need CSP
|
||||||
|
req.headers["x-nonce"] = nonce;
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
res.setHeader(
|
||||||
|
req.headers["x-csp-enforce"] === "true"
|
||||||
|
? "Content-Security-Policy"
|
||||||
|
: "Content-Security-Policy-Report-Only",
|
||||||
|
getCspPolicy(nonce)
|
||||||
|
.replace(/\s{2,}/g, " ")
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nonce };
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Prisma, PrismaClient } from "@prisma/client";
|
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||||
const booking = await prisma.booking.findFirst({
|
const booking = await prisma.booking.findFirst({
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
|
||||||
|
|
||||||
/** @deprecated use the one from `@calcom/lib/hooks/useLocale` */
|
|
||||||
export const useLocale = () => {
|
|
||||||
const { i18n, t } = useTranslation("common");
|
|
||||||
|
|
||||||
return {
|
|
||||||
i18n,
|
|
||||||
t,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
import type { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
||||||
|
|
||||||
const createBooking = async (data: BookingCreateBody) => {
|
const createBooking = async (data: BookingCreateBody) => {
|
||||||
const response = await fetch.post<BookingCreateBody, BookingResponse>("/api/book/event", data);
|
const response = await fetch.post<BookingCreateBody, BookingResponse>("/api/book/event", data);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AppsStatus } from "@calcom/types/Calendar";
|
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
import type { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
||||||
|
|
||||||
type ExtendedBookingCreateBody = BookingCreateBody & {
|
type ExtendedBookingCreateBody = BookingCreateBody & {
|
||||||
noEmail?: boolean;
|
noEmail?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { I18n } from "next-i18next";
|
import type { I18n } from "next-i18next";
|
||||||
import { RRule } from "rrule";
|
import { RRule } from "rrule";
|
||||||
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Attendee, Booking } from "@prisma/client";
|
import type { Attendee, Booking } from "@prisma/client";
|
||||||
|
|
||||||
import { AppsStatus } from "@calcom/types/Calendar";
|
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export type BookingCreateBody = {
|
export type BookingCreateBody = {
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||||
|
|
||||||
export type EmbedProps = {
|
export type EmbedProps = {
|
||||||
isEmbed?: boolean;
|
isEmbed?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { csp } from "@lib/csp";
|
||||||
|
|
||||||
|
export type WithNonceProps = {
|
||||||
|
nonce?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make any getServerSideProps fn return the nonce so that it can be used by Components in the page to add any script tag.
|
||||||
|
* Note that if the Components are not adding any script tag then this is not needed. Even in absence of this, Document.getInitialProps would be able to generate nonce itself which it needs to add script tags common to all pages
|
||||||
|
* There is no harm in wrapping a `getServerSideProps` fn with this even if it doesn't add any script tag.
|
||||||
|
*/
|
||||||
|
export default function withNonce(getServerSideProps: GetServerSideProps) {
|
||||||
|
return async (context: GetServerSidePropsContext) => {
|
||||||
|
const ssrResponse = await getServerSideProps(context);
|
||||||
|
const { nonce } = csp(context.req, context.res);
|
||||||
|
|
||||||
|
// Skip nonce property if it's not available instead of setting it to undefined because undefined can't be serialized.
|
||||||
|
const nonceProps = nonce
|
||||||
|
? {
|
||||||
|
nonce,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!("props" in ssrResponse)) {
|
||||||
|
return ssrResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helps in debugging that withNonce was used but a valid nonce couldn't be set
|
||||||
|
context.res.setHeader("x-csp", nonce ? "ssr" : "false");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...ssrResponse,
|
||||||
|
props: {
|
||||||
|
...ssrResponse.props,
|
||||||
|
...nonceProps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+54
-2
@@ -1,5 +1,7 @@
|
|||||||
|
import { get } from "@vercel/edge-config";
|
||||||
import { collectEvents } from "next-collect/server";
|
import { collectEvents } from "next-collect/server";
|
||||||
import { NextMiddleware, NextResponse, userAgent } from "next/server";
|
import type { NextMiddleware } from "next/server";
|
||||||
|
import { NextResponse, userAgent } from "next/server";
|
||||||
|
|
||||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||||
@@ -8,6 +10,28 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
|||||||
const middleware: NextMiddleware = async (req) => {
|
const middleware: NextMiddleware = async (req) => {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
|
|
||||||
|
if (!url.pathname.startsWith("/api")) {
|
||||||
|
//
|
||||||
|
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
||||||
|
// by the application the user is automatically redirected to /auth/login.
|
||||||
|
//
|
||||||
|
// - For this reason our matchers are sufficient for an app-wide maintenance page.
|
||||||
|
//
|
||||||
|
try {
|
||||||
|
// Check whether the maintenance page should be shown
|
||||||
|
const isInMaintenanceMode = await get<boolean>("isInMaintenanceMode");
|
||||||
|
// If is in maintenance mode, point the url pathname to the maintenance page
|
||||||
|
if (isInMaintenanceMode) {
|
||||||
|
req.nextUrl.pathname = `/maintenance`;
|
||||||
|
return NextResponse.rewrite(req.nextUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// show the default page if EDGE_CONFIG env var is missing,
|
||||||
|
// but log the error to the console
|
||||||
|
// console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (["/api/collect-events", "/api/auth"].some((p) => url.pathname.startsWith(p))) {
|
if (["/api/collect-events", "/api/auth"].some((p) => url.pathname.startsWith(p))) {
|
||||||
const callbackUrl = url.searchParams.get("callbackUrl");
|
const callbackUrl = url.searchParams.get("callbackUrl");
|
||||||
const { isBot } = userAgent(req);
|
const { isBot } = userAgent(req);
|
||||||
@@ -36,11 +60,39 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
return NextResponse.rewrite(url);
|
return NextResponse.rewrite(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/api/trpc/")) {
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
|
||||||
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname.startsWith("/auth/login")) {
|
||||||
|
const moreHeaders = new Headers(req.headers);
|
||||||
|
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
|
||||||
|
moreHeaders.set("x-csp-enforce", "true");
|
||||||
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: moreHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/api/collect-events/:path*", "/api/auth/:path*", "/apps/routing_forms/:path*", "/:path*/embed"],
|
matcher: [
|
||||||
|
"/api/collect-events/:path*",
|
||||||
|
"/api/auth/:path*",
|
||||||
|
"/apps/routing_forms/:path*",
|
||||||
|
"/:path*/embed",
|
||||||
|
"/api/trpc/:path*",
|
||||||
|
"/auth/login",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default collectEvents({
|
export default collectEvents({
|
||||||
|
|||||||
+32
-4
@@ -35,6 +35,12 @@ if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
|
|||||||
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.CSP_POLICY === "strict" && process.env.NODE_ENV === "production") {
|
||||||
|
throw new Error(
|
||||||
|
"Strict CSP policy(for style-src) is not yet supported in production. You can experiment with it in Dev Mode"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!process.env.EMAIL_FROM) {
|
if (!process.env.EMAIL_FROM) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"\x1b[33mwarn",
|
"\x1b[33mwarn",
|
||||||
@@ -76,7 +82,6 @@ if (process.env.ANALYZE === "true") {
|
|||||||
|
|
||||||
plugins.push(withTM);
|
plugins.push(withTM);
|
||||||
plugins.push(withAxiom);
|
plugins.push(withAxiom);
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
i18n,
|
i18n,
|
||||||
@@ -98,7 +103,7 @@ const nextConfig = {
|
|||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
webpack: (config) => {
|
webpack: (config, { webpack, buildId }) => {
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
@@ -120,6 +125,8 @@ const nextConfig = {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
config.plugins.push(new webpack.DefinePlugin({ "process.env.BUILD_ID": JSON.stringify(buildId) }));
|
||||||
|
|
||||||
config.resolve.fallback = {
|
config.resolve.fallback = {
|
||||||
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
||||||
// by next.js will be dropped. Doesn't make much sense, but how it is
|
// by next.js will be dropped. Doesn't make much sense, but how it is
|
||||||
@@ -179,8 +186,7 @@ const nextConfig = {
|
|||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
// prettier-ignore
|
source: "/auth/:path*",
|
||||||
source: "/:path*((?<!\/embed$)(?<!\/embed\/preview\.html$))",
|
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "X-Frame-Options",
|
key: "X-Frame-Options",
|
||||||
@@ -188,6 +194,28 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/signup",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Frame-Options",
|
||||||
|
value: "DENY",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/:path*",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Referrer-Policy",
|
||||||
|
value: "strict-origin-when-cross-origin",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
|
|||||||
Loaded 100 of 608 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user