Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4803817055 | ||
|
|
dac6bff5b1 | ||
|
|
a3c10d0beb | ||
|
|
d2b45e52ff | ||
|
|
0da6878188 | ||
|
|
572b54b7f0 | ||
|
|
d9bf89beab | ||
|
|
0d1f484c92 | ||
|
|
51d02c9873 | ||
|
|
6226baa244 | ||
|
|
81d842ade7 | ||
|
|
d26732c9dc | ||
|
|
f5318c6be3 | ||
|
|
dafbebe6f3 | ||
|
|
4adfcb4b6b | ||
|
|
3ac6a8aa39 | ||
|
|
c8019edb57 | ||
|
|
bfc81b770a | ||
|
|
4a5404bf21 | ||
|
|
7a9b40f218 | ||
|
|
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 |
+11
-4
@@ -1,20 +1,21 @@
|
||||
# ********** INDEX **********
|
||||
#
|
||||
# - LICENSE
|
||||
# - LICENSE (DEPRECATED)
|
||||
# - DATABASE
|
||||
# - SHARED
|
||||
# - NEXTAUTH
|
||||
# - E-MAIL SETTINGS
|
||||
|
||||
# - LICENSE *************************************************************************************************
|
||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||
#
|
||||
# Summary of terms:
|
||||
# - The codebase has to stay open source, whether it was modified or not
|
||||
# - You can not repackage or sell the codebase
|
||||
# - 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
|
||||
CALCOM_LICENSE_KEY=
|
||||
# ***********************************************************************************************************
|
||||
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||
# Set this to true in to disable new signups
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
|
||||
# Content Security Policy
|
||||
CSP_POLICY=
|
||||
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# This is a comment.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
# @global-owner1 and @global-owner2 will be requested for
|
||||
# review when someone opens a pull request.
|
||||
* @calcom/reviewers
|
||||
|
||||
.github/ @calcom/core
|
||||
.husky/ @calcom/core
|
||||
.vcode/ @calcom/core
|
||||
.snaplet/ @calcom/core
|
||||
deploy/ @calcom/core
|
||||
scripts/ @calcom/core
|
||||
|
||||
apps/docs @calcom/docs
|
||||
apps/swagger @calcom/docs
|
||||
|
||||
packages/app-store-cli @hariombalhara
|
||||
packages/dayjs @zomars
|
||||
packages/emails @zomars
|
||||
packages/embeds @hariombalhara
|
||||
packages/eslint-plugin @zomars
|
||||
packages/features @zomars
|
||||
packages/features/kbar @alishaz-polymath
|
||||
packages/features/ee/impersonation @sean-brydon
|
||||
packages/features/ee/payments @alannnc
|
||||
packages/features/ee/workflows @CarinaWolli
|
||||
packages/features/tips @PeerRich
|
||||
packages/ui @calcom/ui
|
||||
packages/config @calcom/core
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Cache production build binaries
|
||||
description: "Cache or restore if necessary"
|
||||
env:
|
||||
node_version: v16.x
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache production build
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: prod-build
|
||||
key-1: ${{ env.node_version }}-${{ hashFiles('yarn.lock') }}
|
||||
key-2: ${{ hashFiles('apps/web/next.config.js') }}
|
||||
with:
|
||||
path: |
|
||||
apps/web/.next
|
||||
node_modules/.cache/turbo
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-
|
||||
${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Cache database between jobs
|
||||
description: "Cache or restore if necessary"
|
||||
inputs:
|
||||
DATABASE_URL:
|
||||
required: true
|
||||
path:
|
||||
required: false
|
||||
default: "backups/backup.sql"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache database
|
||||
id: cache-db
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-db
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-
|
||||
- name: Postgres Dump Backup
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
uses: tj-actions/pg-dump@v2.3
|
||||
with:
|
||||
database_url: ${{ inputs.DATABASE_URL }}
|
||||
path: ${{ inputs.path }}
|
||||
options: "-O"
|
||||
- name: Postgres Backup Restore
|
||||
if: steps.cache-db.outputs.cache-hit == 'true'
|
||||
uses: tj-actions/pg-restore@v4.5
|
||||
with:
|
||||
database_url: ${{ inputs.DATABASE_URL }}
|
||||
backup_file: ${{ inputs.path }}
|
||||
@@ -0,0 +1,10 @@
|
||||
name: Dangerous git Checkout
|
||||
description: "Git Checkout from PR code so we can run checks from forks"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
@@ -0,0 +1,104 @@
|
||||
name: Production Build
|
||||
description: "Creates a production build, caches it and restores if necessary"
|
||||
inputs:
|
||||
DATABASE_URL:
|
||||
required: true
|
||||
NEXT_PUBLIC_WEBAPP_URL:
|
||||
required: true
|
||||
NEXT_PUBLIC_WEBSITE_URL:
|
||||
required: true
|
||||
NEXTAUTH_SECRET:
|
||||
required: true
|
||||
GOOGLE_API_CREDENTIALS:
|
||||
required: true
|
||||
GOOGLE_LOGIN_ENABLED:
|
||||
required: true
|
||||
CRON_API_KEY:
|
||||
required: false
|
||||
CALENDSO_ENCRYPTION_KEY:
|
||||
required: true
|
||||
DAILY_API_KEY:
|
||||
required: true
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY:
|
||||
required: true
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE:
|
||||
required: true
|
||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE:
|
||||
required: true
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE:
|
||||
required: true
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE:
|
||||
required: true
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN:
|
||||
required: true
|
||||
STRIPE_PRIVATE_KEY:
|
||||
required: true
|
||||
STRIPE_CLIENT_ID:
|
||||
required: true
|
||||
STRIPE_WEBHOOK_SECRET:
|
||||
required: true
|
||||
STRIPE_PRO_PLAN_PRODUCT_ID:
|
||||
required: true
|
||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID:
|
||||
required: true
|
||||
STRIPE_FREE_PLAN_PRODUCT_ID:
|
||||
required: true
|
||||
PAYMENT_FEE_PERCENTAGE:
|
||||
required: true
|
||||
PAYMENT_FEE_FIXED:
|
||||
required: true
|
||||
SAML_DATABASE_URL:
|
||||
required: true
|
||||
SAML_ADMINS:
|
||||
required: true
|
||||
NEXTAUTH_URL:
|
||||
required: true
|
||||
NEXT_PUBLIC_IS_E2E:
|
||||
required: true
|
||||
TURBO_TOKEN:
|
||||
required: true
|
||||
TURBO_TEAM:
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
id: prod-build-cache
|
||||
- name: Next.js production build
|
||||
if: steps.prod-build-cache.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
DATABASE_URL: ${{ inputs.DATABASE_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ inputs.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ inputs.NEXT_PUBLIC_WEBSITE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ inputs.NEXTAUTH_SECRET }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ inputs.GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ inputs.GOOGLE_LOGIN_ENABLED }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ inputs.CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ inputs.DAILY_API_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ inputs.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ inputs.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ inputs.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
||||
STRIPE_PRIVATE_KEY: ${{ inputs.STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ inputs.STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ inputs.STRIPE_WEBHOOK_SECRET }}
|
||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ inputs.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ inputs.PAYMENT_FEE_PERCENTAGE }}
|
||||
PAYMENT_FEE_FIXED: ${{ inputs.PAYMENT_FEE_FIXED }}
|
||||
SAML_DATABASE_URL: ${{ inputs.SAML_DATABASE_URL }}
|
||||
SAML_ADMINS: ${{ inputs.SAML_ADMINS }}
|
||||
NEXTAUTH_URL: ${{ inputs.NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ inputs.NEXT_PUBLIC_IS_E2E }}
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
run: |
|
||||
yarn db-seed
|
||||
yarn build
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Yarn install
|
||||
description: "Install all NPM dependencies, caches them and restores if necessary"
|
||||
env:
|
||||
node_version: v16.x
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Use Node ${{ env.node_version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.node_version }}
|
||||
cache: "yarn"
|
||||
- name: node_modules cache
|
||||
id: node-modules-cache
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: node-modules-yarn
|
||||
cache-key: ${{ env.node_version }}-${{ hashFiles('yarn.lock', 'packages/prisma/schema.prisma') }}
|
||||
with:
|
||||
path: |
|
||||
**/node_modules/**
|
||||
packages/prisma/zod
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.cache-key }}
|
||||
restore-keys: ${{ runner.os }}-${{ env.cache-name }}-
|
||||
- name: Yarn install
|
||||
shell: bash
|
||||
if: steps.node-modules-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn install --prefer-offline --frozen-lockfile
|
||||
yarn prisma generate
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Install playwright binaries
|
||||
description: "Install playwright, cache and restore if necessary"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Cache playwright binaries
|
||||
id: playwright-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- name: Yarn playwright install
|
||||
shell: bash
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -1,34 +1,17 @@
|
||||
name: Check types
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.tsx?"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
workflow_call:
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
jobs:
|
||||
check-types:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
|
||||
- name: Show info
|
||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- run: yarn type-check
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
required-checks:
|
||||
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@1.4.9
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
db-staging-snapshot:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Snaplet CLI
|
||||
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
|
||||
- name: Restore Snapshot
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
name: E2E App-Store Apps
|
||||
on:
|
||||
push:
|
||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- apps/api/**
|
||||
- apps/console/**
|
||||
- apps/docs/**
|
||||
- apps/swagger/**
|
||||
- apps/website/**
|
||||
- apps/web/public/**
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
e2e-app-store:
|
||||
timeout-minutes: 20
|
||||
name: E2E App-Store Apps
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16.x"]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
@@ -65,32 +48,15 @@ jobs:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- name: Use Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Install playwright deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn test-e2e:app-store
|
||||
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||
on:
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/embeds/**
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e-embed:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||
@@ -61,35 +51,17 @@ jobs:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Install playwright deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn turbo run embed-tests-update-snapshots:ci --scope=@calcom/embed-react --concurrency=1
|
||||
|
||||
- name: Upload embed-core results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
name: E2E test
|
||||
on:
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/**
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||
@@ -64,34 +54,16 @@ jobs:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-playwright-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Install playwright deps
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||
- uses: ./.github/actions/cache-build
|
||||
- run: yarn test-e2e
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
@@ -1,38 +1,14 @@
|
||||
name: Lint
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.tsx?"
|
||||
- "**.jsx?"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
workflow_call:
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install deps
|
||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Test Code Linting
|
||||
run: yarn lint
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Save Code Linting Reports
|
||||
run: yarn lint:report
|
||||
continue-on-error: true
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
name: "Next.js Bundle Analysis"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Production build
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
analyze:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
cd apps/web
|
||||
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: |
|
||||
cd apps/web
|
||||
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: |
|
||||
cd apps/web
|
||||
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,87 @@
|
||||
name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
app-store: ${{ steps.filter.outputs.app-store }}
|
||||
embed: ${{ steps.filter.outputs.embed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
|
||||
setup:
|
||||
name: Yarn install & cache
|
||||
uses: ./.github/workflows/yarn-install.yml
|
||||
secrets: inherit
|
||||
|
||||
type-check:
|
||||
name: Type check
|
||||
needs: setup
|
||||
uses: ./.github/workflows/check-types.yml
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Unit tests
|
||||
needs: setup
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Linters
|
||||
needs: setup
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production build
|
||||
needs: setup
|
||||
uses: ./.github/workflows/production-build.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e:
|
||||
name: E2E tests
|
||||
needs: [lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-app-store:
|
||||
name: E2E App Store tests
|
||||
if: ${{ needs.changes.outputs.app-store == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e-app-store.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-embed:
|
||||
name: E2E embeds tests
|
||||
if: ${{ needs.changes.outputs.embed == 'true' }}
|
||||
needs: [changes, lint, build]
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
analyze:
|
||||
needs: build
|
||||
uses: ./.github/workflows/nextjs-bundle-analysis.yml
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,85 @@
|
||||
name: Production Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: secret
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: true
|
||||
# CRON_API_KEY: xxx
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
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 }}
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/yarn-build
|
||||
with:
|
||||
# GitHub Composite actions cannot read secrets, we have to pass them in here
|
||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ env.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBSITE_URL: ${{ env.NEXT_PUBLIC_WEBSITE_URL }}
|
||||
NEXTAUTH_SECRET: ${{ env.NEXTAUTH_SECRET }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ env.GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: ${{ env.GOOGLE_LOGIN_ENABLED }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ env.CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ env.DAILY_API_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE: ${{ env.NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN: ${{ env.NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN }}
|
||||
STRIPE_PRIVATE_KEY: ${{ env.STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ env.STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ env.STRIPE_WEBHOOK_SECRET }}
|
||||
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ env.STRIPE_PRO_PLAN_PRODUCT_ID }}
|
||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ env.STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
|
||||
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ env.STRIPE_FREE_PLAN_PRODUCT_ID }}
|
||||
PAYMENT_FEE_PERCENTAGE: ${{ env.PAYMENT_FEE_PERCENTAGE }}
|
||||
PAYMENT_FEE_FIXED: ${{ env.PAYMENT_FEE_FIXED }}
|
||||
SAML_DATABASE_URL: ${{ env.SAML_DATABASE_URL }}
|
||||
SAML_ADMINS: ${{ env.SAML_ADMINS }}
|
||||
NEXTAUTH_URL: ${{ env.NEXTAUTH_URL }}
|
||||
NEXT_PUBLIC_IS_E2E: ${{ env.NEXT_PUBLIC_IS_E2E }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- uses: ./.github/actions/cache-db
|
||||
with:
|
||||
DATABASE_URL: ${{ env.DATABASE_URL }}
|
||||
@@ -1,44 +0,0 @@
|
||||
name: "Meta Workflow: Require Conditional Status Checks"
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_call:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
required-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Ensure All Conditional Checks Have Passed
|
||||
uses: blend/require-conditional-status-checks@2022.02.04
|
||||
with:
|
||||
timeout: 15m
|
||||
github-token: ${{ github.token }}
|
||||
interval: 20s
|
||||
# Question marks aren't working
|
||||
# @see https://github.com/blend/require-conditional-status-checks/pull/11
|
||||
checks-yaml: |
|
||||
- job: lint
|
||||
paths:
|
||||
- /**/*.jsx
|
||||
- /**/*.js
|
||||
- /**/*.tsx
|
||||
- /**/*.ts
|
||||
- job: check-types
|
||||
paths:
|
||||
- /**/*.tsx
|
||||
- /**/*.ts
|
||||
- job: e2e
|
||||
paths:
|
||||
- /apps/web/**
|
||||
- /packages/**
|
||||
- job: e2e-embed
|
||||
paths:
|
||||
- /apps/web/**
|
||||
- /packages/embeds/**
|
||||
- job: test
|
||||
paths:
|
||||
- /apps/web/**
|
||||
- /packages/**
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
@@ -1,33 +1,17 @@
|
||||
name: Unit tests
|
||||
on:
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/web/**
|
||||
- packages/**
|
||||
workflow_call:
|
||||
workflow_run:
|
||||
workflows: [Crowdin Action]
|
||||
types: [completed]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: "yarn"
|
||||
- run: yarn --frozen-lockfile
|
||||
- uses: ./.github/actions/yarn-install
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
- run: yarn test
|
||||
|
||||
@@ -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"]}]'
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Yarn install
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
name: Yarn install & cache
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
@@ -77,6 +77,9 @@ lint-results
|
||||
#Storybook
|
||||
apps/storybook/build-storybook.log
|
||||
|
||||
# Infisical
|
||||
.infisical.json
|
||||
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.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.
|
||||
|
||||
## 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
|
||||
|
||||
The development branch is `main`. This is the branch that all pull
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
||||
"addons": [
|
||||
{
|
||||
"plan": "heroku-postgresql:hobby-dev"
|
||||
"plan": "heroku-postgresql:mini"
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
@@ -43,15 +43,18 @@
|
||||
},
|
||||
"SENDGRID_API_KEY": {
|
||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"SENDGRID_SYNC_API_KEY": {
|
||||
"description": "Sendgrid internal sync service",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"SENDGRID_EMAIL": {
|
||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_FROM": {
|
||||
"description": "Configures the global From: header whilst sending emails",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 "./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-dropdown-menu": "^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-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
@@ -36,8 +36,8 @@
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"babel-loader": "^8.2.5",
|
||||
|
||||
@@ -2,4 +2,9 @@
|
||||
@tailwind components;
|
||||
@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": {
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { SwaggerUI } from "swagger-ui-react";
|
||||
import type { SwaggerUI } from "swagger-ui-react";
|
||||
|
||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import Select from "react-select";
|
||||
import { OptionProps } from "react-select";
|
||||
import type { OptionProps } from "react-select";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -62,10 +63,9 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
|
||||
<div className="flex gap-x-3">
|
||||
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||
<div className="flex gap-x-3 p-4">
|
||||
{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 items-center gap-x-2">
|
||||
<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 { 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";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import classNames from "classnames";
|
||||
import { NextRouter, useRouter } from "next/router";
|
||||
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
|
||||
import { components, ControlProps } from "react-select";
|
||||
import type { NextRouter } from "next/router";
|
||||
import { useRouter } from "next/router";
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -12,16 +15,15 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
HorizontalTabs,
|
||||
InputLeading,
|
||||
Label,
|
||||
showToast,
|
||||
Switch,
|
||||
TextArea,
|
||||
TextField,
|
||||
ColorPicker,
|
||||
} from "@calcom/ui";
|
||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||
@@ -676,7 +678,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
return `${router.asPath.split("?")[0]}?${searchParams.toString()}`;
|
||||
};
|
||||
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
|
||||
.filter((tab) => tab.type === "code")
|
||||
.forEach((codeTab) => {
|
||||
@@ -720,7 +722,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
|
||||
const calLink = decodeURIComponent(embedUrl);
|
||||
|
||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
||||
const addToPalette = (update: (typeof previewState)["palette"]) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
@@ -1061,7 +1063,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
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 { Dispatch, useState, useEffect } from "react";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
import type { Dispatch } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { JSONObject } from "superjson/dist/types";
|
||||
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Slider from "@components/Slider";
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { AdminRequired } from "components/ui/AdminRequired";
|
||||
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 { FC, Fragment, MouseEventHandler } from "react";
|
||||
import type { FC, MouseEventHandler } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||
|
||||
export interface NavTabProps {
|
||||
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 { 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 { 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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 {
|
||||
FiBookOpen,
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
|
||||
import Select from "react-select";
|
||||
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
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 { 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 { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
@@ -44,7 +45,7 @@ export default function InstalledAppsLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const variant: typeof InstalledAppVariants[number] = "payment";
|
||||
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
||||
|
||||
const query = trpc.viewer.integrations.useQuery({
|
||||
variant,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { UserPermissionRole } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
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 Shell from "@calcom/features/shell/Shell";
|
||||
import type Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
|
||||
import { UserPermissionRole } from ".prisma/client";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
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"] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
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">
|
||||
{locations.map((location) => {
|
||||
<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, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
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" ? (
|
||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
@@ -26,13 +26,13 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"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`}
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
||||
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
|
||||
<p className="line-clamp-1">{t(locationKeyToString(location) ?? "")}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
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 { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
@@ -37,6 +40,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
seatsPerTimeSlot,
|
||||
ethSignature,
|
||||
}) => {
|
||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const { rescheduleUid } = router.query;
|
||||
@@ -47,122 +51,124 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
setBrand(getComputedStyle(document.documentElement).getPropertyValue("--brand-color").trim());
|
||||
}, []);
|
||||
|
||||
if (!date) return null;
|
||||
|
||||
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 className="mb-6 flex items-center text-left text-base">
|
||||
<div className="mr-4">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<ToggleGroup
|
||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||
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]">
|
||||
{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 ref={slotPickerRef}>
|
||||
{!!date ? (
|
||||
<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 className="mb-6 flex items-center text-left text-base">
|
||||
<div className="mr-4">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<ToggleGroup
|
||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||
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]">
|
||||
{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 && (
|
||||
<>
|
||||
<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>
|
||||
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(
|
||||
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
|
||||
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { classNames } from "@calcom/lib";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui";
|
||||
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -2,14 +2,17 @@ import { BookingStatus } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
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 ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -17,13 +20,11 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
MeetingTimeInTimezones,
|
||||
showToast,
|
||||
TextArea,
|
||||
Tooltip,
|
||||
ActionType,
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
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 mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.status === BookingStatus.REJECTED) {
|
||||
if (data?.status === BookingStatus.REJECTED) {
|
||||
setRejectionDialogIsOpen(false);
|
||||
showToast(t("booking_rejection_success"), "success");
|
||||
} else {
|
||||
@@ -245,24 +246,23 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader title={t("rejection_reason_title")} />
|
||||
|
||||
<p className="-mt-4 text-sm text-gray-500">{t("rejection_reason_description")}</p>
|
||||
<p className="mt-6 mb-2 text-sm font-bold text-black">
|
||||
{t("rejection_reason")}
|
||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||
</p>
|
||||
<TextArea
|
||||
name={t("rejection_reason")}
|
||||
value={rejectionReason}
|
||||
onChange={(e) => setRejectionReason(e.target.value)}
|
||||
className="mb-5 sm:mb-6"
|
||||
/>
|
||||
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||
<div>
|
||||
<TextAreaField
|
||||
name="rejectionReason"
|
||||
label={
|
||||
<>
|
||||
{t("rejection_reason")}
|
||||
<span className="font-normal text-gray-500"> (Optional)</span>
|
||||
</>
|
||||
}
|
||||
value={rejectionReason}
|
||||
onChange={(e) => setRejectionReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
|
||||
<Button
|
||||
disabled={mutation.isLoading}
|
||||
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 { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import type { ITimezoneOption } from "@calcom/ui";
|
||||
import { TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { timeZone } from "../../lib/clock";
|
||||
|
||||
@@ -11,7 +15,8 @@ type Props = {
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const { t } = useLocale();
|
||||
const query = useMeQuery();
|
||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTimeZone(timeZone());
|
||||
@@ -23,18 +28,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
}
|
||||
}, [selectedTimeZone, onSelectTimeZone]);
|
||||
|
||||
return selectedTimeZone !== "" ? (
|
||||
<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">
|
||||
<div className="mb-4 flex">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
</div>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
const customStyles = {
|
||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||
padding: "10px 12px !important",
|
||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||
...(userTheme === "dark" && {
|
||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||
}),
|
||||
}),
|
||||
control: () => ({
|
||||
display: "flex",
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
||||
import type { AvatarGroupProps } from "@calcom/ui";
|
||||
import { AvatarGroup } from "@calcom/ui";
|
||||
|
||||
export const UserAvatars = ({
|
||||
profile,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { EventType } from "@prisma/client";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { z } from "zod";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useEmbedStyles,
|
||||
@@ -17,238 +14,39 @@ import {
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
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 useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
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 { 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 useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import Gates, { Gate, GateState } from "@components/Gates";
|
||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||
import type { Gate, GateState } from "@components/Gates";
|
||||
import Gates from "@components/Gates";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
import TimeOptions from "@components/booking/TimeOptions";
|
||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||
|
||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||
|
||||
const useSlots = ({
|
||||
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"]>({});
|
||||
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.slots) {
|
||||
setCachedSlots((c) => ({ ...c, ...data?.slots }));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// The very first time isPaused is set if auto-fetch is disabled, so isPaused should also be considered a loading state.
|
||||
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 Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||
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]" />,
|
||||
});*/
|
||||
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const dateQuerySchema = z.object({
|
||||
rescheduleUid: z.string().optional().default(""),
|
||||
@@ -311,7 +109,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||
[timeZone]
|
||||
);
|
||||
const stripeAppData = getStripeAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
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.
|
||||
@@ -362,10 +160,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
isBackgroundTransparent
|
||||
? ""
|
||||
: "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"
|
||||
)}>
|
||||
<div className="overflow-hidden md:flex">
|
||||
<div className="md:flex">
|
||||
{showEventTypeDetails && (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -399,14 +197,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{stripeAppData.price > 0 && (
|
||||
{paymentAppData.price > 0 && (
|
||||
<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]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
|
||||
@@ -6,14 +6,14 @@ import { useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
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 { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import {
|
||||
EventLocationType,
|
||||
getEventLocationType,
|
||||
getEventLocationValue,
|
||||
getHumanReadableLocationValue,
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
} from "@calcom/app-store/locations";
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
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 {
|
||||
useEmbedNonStylesConfig,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
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 useTheme from "@calcom/lib/hooks/useTheme";
|
||||
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 { 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 { BookPageProps } from "../../../pages/[user]/book";
|
||||
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||
|
||||
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
|
||||
let duration = eventType.length;
|
||||
if (
|
||||
@@ -148,6 +150,7 @@ const BookingPage = ({
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async (responseData) => {
|
||||
const { uid, paymentUid } = responseData;
|
||||
|
||||
if (paymentUid) {
|
||||
return await router.push(
|
||||
createPaymentLink({
|
||||
@@ -166,7 +169,7 @@ const BookingPage = ({
|
||||
isSuccessBookingPage: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -557,14 +560,14 @@ const BookingPage = ({
|
||||
{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">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
{stripeAppData.price > 0 && (
|
||||
{paymentAppData.price > 0 && (
|
||||
<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]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
currency={paymentAppData?.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
|
||||
@@ -7,23 +7,24 @@ import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
|
||||
import {
|
||||
EventLocationType,
|
||||
getEventLocationType,
|
||||
getHumanReadableLocationValue,
|
||||
getMessageForOrganizer,
|
||||
LocationObject,
|
||||
LocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
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 { FiMapPin } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
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];
|
||||
|
||||
@@ -187,6 +188,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<CheckboxField
|
||||
data-testid="display-location"
|
||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) =>
|
||||
@@ -344,7 +346,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button type="submit">{t("update")}</Button>
|
||||
<Button data-testid="update-location" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
|
||||
@@ -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 { 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 { components, OptionProps, SingleValueProps } from "react-select";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
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"))
|
||||
);
|
||||
|
||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
const EventTypeScheduleDetails = () => {
|
||||
const { data: loggedInUser } = useMeQuery();
|
||||
const timeFormat = loggedInUser?.timeFormat;
|
||||
const { t, i18n } = useLocale();
|
||||
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 { 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) =>
|
||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||
|
||||
if (!isTeamEvent) {
|
||||
return <EventTypeSchedule />;
|
||||
}
|
||||
return (
|
||||
<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 (
|
||||
<Controller
|
||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||
@@ -201,6 +208,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
checked={!value}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (checked) {
|
||||
resetField("schedule");
|
||||
} else {
|
||||
setValue("schedule", null);
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
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 { EventTypeCustomInputType } from "@prisma/client";
|
||||
import type { CustomInputParsed } from "pages/event-types/[type]";
|
||||
import { FC } from "react";
|
||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||
import type { FC } from "react";
|
||||
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 { Button, Label, Select, TextField } from "@calcom/ui";
|
||||
|
||||
@@ -74,7 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
formMethods.getValues("customInputs").splice(index, 1);
|
||||
@@ -408,7 +408,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<DialogContent
|
||||
title={t("custom_event_name")}
|
||||
description={t("custom_event_name_description")}
|
||||
type="creation">
|
||||
type="creation"
|
||||
enableOverflow>
|
||||
<TextField
|
||||
label={t("event_name_in_calendar")}
|
||||
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 { 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 { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen } from "@calcom/ui";
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
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 { 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 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 { 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 { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
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";
|
||||
|
||||
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 (
|
||||
<div className="">
|
||||
|
||||
@@ -6,10 +6,11 @@ import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { MultiValue } from "react-select";
|
||||
import type { MultiValue } from "react-select";
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import LocationSelect, {
|
||||
SingleValueLocationOption,
|
||||
LocationOption,
|
||||
} from "@components/ui/form/LocationSelect";
|
||||
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||
|
||||
const getLocationFromType = (
|
||||
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 = (
|
||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||
) => {
|
||||
@@ -66,10 +75,15 @@ export const EventSetupTab = (
|
||||
setShowLocationModal(true);
|
||||
};
|
||||
|
||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
||||
const removeLocation = (selectedLocation: (typeof eventType.locations)[number]) => {
|
||||
formMethods.setValue(
|
||||
"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 }
|
||||
);
|
||||
};
|
||||
@@ -84,14 +98,14 @@ export const EventSetupTab = (
|
||||
...details,
|
||||
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 {
|
||||
formMethods.setValue(
|
||||
"locations",
|
||||
@@ -138,13 +152,11 @@ export const EventSetupTab = (
|
||||
return true;
|
||||
});
|
||||
|
||||
const defaultValue = locationOptions.find((item) => item.label === "video")?.options;
|
||||
return (
|
||||
<div className="w-full">
|
||||
{validLocations.length === 0 && (
|
||||
<div className="flex">
|
||||
<LocationSelect
|
||||
defaultValue={defaultValue}
|
||||
placeholder={t("select")}
|
||||
options={locationOptions}
|
||||
isSearchable={false}
|
||||
@@ -175,18 +187,24 @@ export const EventSetupTab = (
|
||||
if (!eventLocationType) {
|
||||
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 (
|
||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
<div className="flex max-w-full justify-between">
|
||||
<div key={index} className="flex flex-grow items-center">
|
||||
<li
|
||||
key={`${location.type}${index}`}
|
||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className="h-4 w-4"
|
||||
alt={`${eventLocationType.label} logo`}
|
||||
/>
|
||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
||||
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
||||
</span>
|
||||
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
@@ -230,7 +248,11 @@ export const EventSetupTab = (
|
||||
)}
|
||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||
<li>
|
||||
<Button StartIcon={FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
<Button
|
||||
data-testid="add-location"
|
||||
StartIcon={FiPlus}
|
||||
color="minimal"
|
||||
onClick={() => setShowLocationModal(true)}>
|
||||
{t("add_location")}
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { ComponentProps } from "react";
|
||||
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
||||
import { Options } from "react-select";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Control } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import type { Options } from "react-select";
|
||||
|
||||
import CheckedTeamSelect, {
|
||||
CheckedSelectOption,
|
||||
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
||||
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 = ({
|
||||
control,
|
||||
labelText,
|
||||
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
];
|
||||
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && (
|
||||
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
|
||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
)}
|
||||
|
||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||
<>
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembers.map(mapUserToValue)}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods
|
||||
.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 { TbWebhook } from "react-icons/tb";
|
||||
|
||||
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 { APP_NAME } from "@calcom/lib/constants";
|
||||
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 { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
@@ -100,7 +100,7 @@ export const EventTeamWebhooksTab = ({
|
||||
<>
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="mt-4 mb-8 rounded-md border">
|
||||
<div className="mb-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
import type { TFunction } from "next-i18next";
|
||||
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 { UseFormReturn } from "react-hook-form";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { TbWebhook } from "react-icons/tb";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -286,6 +286,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={FiTrash}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
@@ -338,7 +339,7 @@ function EventTypeSingleLayout({
|
||||
<div
|
||||
className={classNames(
|
||||
"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}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import { UnitTypeLongPlural } from "dayjs";
|
||||
import type { UnitTypeLongPlural } from "dayjs";
|
||||
import { Trans } from "next-i18next";
|
||||
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 z from "zod";
|
||||
import type z from "zod";
|
||||
|
||||
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";
|
||||
|
||||
type RequiresConfirmationControllerProps = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useForm } from "react-hook-form";
|
||||
import { Schedule } from "@calcom/features/schedules";
|
||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Form } from "@calcom/ui";
|
||||
|
||||
interface ISetupAvailabilityProps {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import MarkdownIt from "markdown-it";
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
@@ -34,6 +36,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
const utils = trpc.useContext();
|
||||
const router = useRouter();
|
||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (_data, context) => {
|
||||
@@ -64,6 +67,8 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
const onSubmit = handleSubmit((data: { bio: string }) => {
|
||||
const { bio } = data;
|
||||
|
||||
telemetry.event(telemetryEventTypes.onboardingFinished);
|
||||
|
||||
mutation.mutate({
|
||||
bio,
|
||||
completedOnboarding: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
@@ -20,7 +21,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { user, nextStep } = props;
|
||||
const { t } = useLocale();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -31,6 +32,11 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||
}, [telemetry]);
|
||||
|
||||
const defaultOptions = { required: true, maxLength: 255 };
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
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 { 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 { ErrorCode } from "@calcom/lib/auth";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge, Button } from "@calcom/ui";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||
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 { ErrorCode } from "@calcom/lib/auth";
|
||||
|
||||
+30
-10
@@ -1,17 +1,39 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import classNames from "classnames";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
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 }) => {
|
||||
const router = useRouter();
|
||||
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||
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 formSchema = z.object({
|
||||
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
});
|
||||
|
||||
const onError = () => {
|
||||
props.setIsLoading(false);
|
||||
props.onError();
|
||||
};
|
||||
|
||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||
props.setIsLoading(true);
|
||||
props.onSubmit();
|
||||
const response = await fetch("/api/auth/setup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
email: data.email_address.toLowerCase(),
|
||||
password: data.password,
|
||||
});
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
props.onSuccess();
|
||||
} else {
|
||||
router.replace("/auth/setup");
|
||||
props.onError();
|
||||
}
|
||||
}, onError);
|
||||
|
||||
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
</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 type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const StepDone = () => {
|
||||
const StepDone = (props: {
|
||||
currentStep: number;
|
||||
nextStepPath: string;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<form
|
||||
id="wizard-step-1"
|
||||
name="wizard-step-1"
|
||||
className="space-y-4"
|
||||
id={`wizard-step-${props.currentStep}`}
|
||||
name={`wizard-step-${props.currentStep}`}
|
||||
className="flex justify-center space-y-4"
|
||||
onSubmit={(e) => {
|
||||
props.setIsLoading(true);
|
||||
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="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 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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
@@ -20,7 +19,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
|
||||
return (
|
||||
<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
|
||||
size="md"
|
||||
alt={member.name || ""}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { FC, Fragment } from "react";
|
||||
import type { FC } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
type AdminRequiredProps = {
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
import type { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
||||
<label className="min-w-8 relative inline-block">
|
||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||
{!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
|
||||
{...passThroughProps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { SVGComponent } from "@lib/types/SVGComponent";
|
||||
import type { SVGComponent } from "@lib/types/SVGComponent";
|
||||
|
||||
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
Icon: SVGComponent;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import classNames from "classnames";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { Dialog, DialogContent } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import classNames from "classnames";
|
||||
import { debounce, noop } from "lodash";
|
||||
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 { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { User } from "@calcom/prisma/client";
|
||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
||||
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import classNames from "classnames";
|
||||
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 { 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 { 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 { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { User } from "@calcom/prisma/client";
|
||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import { PremiumTextfield } from "./PremiumTextfield";
|
||||
import { UsernameTextfield } from "./UsernameTextfield";
|
||||
@@ -21,7 +24,13 @@ export const UsernameAvailabilityField = ({
|
||||
onErrorMutation,
|
||||
user,
|
||||
}: 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({
|
||||
defaultValues: {
|
||||
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";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Props } from "react-select";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { classNames } from "@calcom/lib";
|
||||
@@ -39,6 +40,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
||||
return (
|
||||
<Select<LocationOption>
|
||||
name="location"
|
||||
id="location-select"
|
||||
components={{
|
||||
Option: (props) => (
|
||||
<components.Option {...props}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user