Repository: calcom/platform-starter-kit
Branch: main
Commit: b7c72fbbeb65
Files: 128
Total size: 466.5 KB
Directory structure:
gitextract_y4om4g58/
├── .gitignore
├── README.md
└── with-platform-supabase-tailwind-prisma/
├── .eslintrc.cjs
├── LICENSE
├── README.md
├── components.json
├── next.config.js
├── package.json
├── postcss.config.cjs
├── prettier.config.mjs
├── prisma/
│ ├── client.ts
│ ├── migrations/
│ │ └── 0_init/
│ │ └── migration.sql
│ ├── schema.prisma
│ └── seed.ts
├── src/
│ ├── app/
│ │ ├── [expertUsername]/
│ │ │ ├── [eventSlug]/
│ │ │ │ └── page.tsx
│ │ │ ├── _components/
│ │ │ │ ├── AboutSection.tsx
│ │ │ │ ├── Container.tsx
│ │ │ │ └── expert-booker.tsx
│ │ │ ├── booking/
│ │ │ │ └── [bookingUid]/
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── _actions.tsx
│ │ ├── _components/
│ │ │ ├── autocomplete.tsx
│ │ │ ├── banner.tsx
│ │ │ ├── booking-result.tsx
│ │ │ ├── home/
│ │ │ │ ├── results.tsx
│ │ │ │ ├── sidebar-item.tsx
│ │ │ │ └── signup-card.tsx
│ │ │ ├── multi-select.tsx
│ │ │ ├── navigation.tsx
│ │ │ ├── search-bar.tsx
│ │ │ ├── submit-button.tsx
│ │ │ ├── universal/
│ │ │ │ ├── hero.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ └── logo.tsx
│ │ │ └── use-cal.tsx
│ │ ├── _hardcoded.ts
│ │ ├── _searchParams.ts
│ │ ├── api/
│ │ │ ├── auth/
│ │ │ │ └── [...nextauth]/
│ │ │ │ └── route.ts
│ │ │ ├── cal/
│ │ │ │ └── refresh/
│ │ │ │ └── route.ts
│ │ │ └── supabase/
│ │ │ └── storage/
│ │ │ └── route.ts
│ │ ├── dashboard/
│ │ │ ├── @breadcrumbs/
│ │ │ │ ├── [...dashboardSegments]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── @dashboardNavigationDesktop/
│ │ │ │ ├── [...dashboardSegments]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── @dashboardNavigationMobile/
│ │ │ │ ├── [...dashboardSegments]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── _components/
│ │ │ │ ├── bookings-table.tsx
│ │ │ │ ├── connect-calendar-step.tsx
│ │ │ │ ├── getting-started-steps.tsx
│ │ │ │ ├── user-details-step.tsx
│ │ │ │ └── user-filters-step.tsx
│ │ │ ├── data.tsx
│ │ │ ├── getting-started/
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── settings/
│ │ │ ├── _components/
│ │ │ │ ├── expert-edit.tsx
│ │ │ │ ├── settings-content.tsx
│ │ │ │ └── supabase-react-dropzone.tsx
│ │ │ ├── availability/
│ │ │ │ └── page.tsx
│ │ │ ├── booking-events/
│ │ │ │ ├── _actions.ts
│ │ │ │ ├── event-type-create.tsx
│ │ │ │ ├── event-type-delete.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── profile/
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── login/
│ │ │ ├── _components/
│ │ │ │ ├── input.tsx
│ │ │ │ └── login.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── providers.tsx
│ │ ├── signup/
│ │ │ ├── _components/
│ │ │ │ ├── input.tsx
│ │ │ │ └── signup.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ └── tailwind-indicator.tsx
│ ├── auth/
│ │ ├── config.edge.ts
│ │ └── index.tsx
│ ├── cal/
│ │ ├── __generated/
│ │ │ ├── cal-sdk.ts
│ │ │ └── cal-sdk.yml
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ └── utils.ts
│ ├── components/
│ │ └── ui/
│ │ ├── accordion.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── stepper.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── tooltip.tsx
│ │ └── use-toast.ts
│ ├── env.js
│ ├── lib/
│ │ ├── constants.ts
│ │ ├── supabase-image-loader.ts
│ │ └── utils.ts
│ ├── middleware.ts
│ └── styles/
│ └── globals.css
├── supabase/
│ ├── .gitignore
│ ├── config.toml
│ ├── migrations/
│ │ └── 20240615093934_init.sql
│ └── seed.sql
├── tailwind.config.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env.legacy
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
================================================
FILE: README.md
================================================
The platform starter kit has been archived and is no longer maintained.
Find more, smaller examples here: https://github.com/calcom/examples
================================================
FILE: with-platform-supabase-tailwind-prisma/.eslintrc.cjs
================================================
/** @type {import("eslint").Linter.Config} */
const config = {
root: true,
parser: "@typescript-eslint/parser",
ignorePatterns: ["*.config.mjs", "*.config.js", "*.config.cjs"],
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
},
plugins: ["@typescript-eslint", "unused-imports"],
extends: [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked",
],
rules: {
"@typescript-eslint/ban-ts-comment": [
"error",
{
"ts-expect-error": "allow-with-description",
"ts-ignore": "allow-with-description",
"ts-nocheck": "allow-with-description",
"ts-check": "allow-with-description",
},
],
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/consistent-type-imports": [
"warn",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: false,
},
],
},
};
module.exports = config;
================================================
FILE: with-platform-supabase-tailwind-prisma/LICENSE
================================================
Copyright (c) 2024 Cal.com, Inc
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: with-platform-supabase-tailwind-prisma/README.md
================================================
Cal.com Platform Starter Kit
Build your pixel-perfect booking experience
Demo
·
Video Tutorial
·
Docs
·
Deploy on Vercel
Discord
·
Website
·
Issues
# Platform Starter Kit Example
Cal.com Platform Starter Kit showcases the new Cal.com Platform API and Cal.com Atoms. It was built using the [T3 Stack](https://create.t3.gg/) with [Supabase](https://supabase.com/) as the Postgres Database and Image Storage host.
## Deploy your own
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain&env=NEXT_PUBLIC_REFRESH_URL,AUTH_SECRET,AUTH_TRUST_HOST,NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID,NEXT_PUBLIC_CAL_API_URL,CAL_SECRET&envDescription=You%20can%20see%20how%20to%20populate%20the%20environment%20variables%20in%20our%20starter%20example%20→&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain%2F.env.example&project-name=cal-platform-starter&repository-name=cal-platform-starter&demo-title=Cal.com%20Experts&demo-description=A%20marketplace%20to%20book%20appointments%20with%20experts&demo-url=https%3A%2F%2Fexperts.cal.com&demo-image=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Fassets%2F8019099%2F2e58f8da-a110-4a45-b9a4-dcffb45f9baa&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6&external-id=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain)
## How to use
```bash
npx @calcom/starter-kit my-platform
```
OR
**1. Clone the repository**
HTTPS:
```bash
git clone https://github.com/calcom/platform-starter-kit.git
```
GitHub CLI:
```bash
gh repo clone calcom/platform-starter-kit
```
**2. Move into the Starter**
```bash
cd platform-starter-kit/
```
**3. Install dependencies**
> [!IMPORTANT]
> **Package Manager:** This repository is deployed as-is and therefore contains a `pnpm-lock.yaml` file. As a result, you currently have to use `pnpm` as your package manager to ensure that the dependencies are installed correctly.
```bash
pnpm install
```
**4. Set Environment Variables**
We provide most environment variables out of the box (including Cal-related variables).
So get started by copying the `.env.example`:
```bash
cp .env.example .env
```
_4.1 Database_
This project uses Postgres with Supabase. You can create a free project at [database.new](https://database.new/).
Then, get the Database URL from the [Supabase dashboard](https://supabase.com/dashboard/project/_/settings/database) and update the respective values in your `.env` file:
```.env
POSTGRES_PRISMA_URL="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" # Transaction Mode
POSTGRES_URL_NON_POOLING="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres" # Session Mode
```
When working locally you can use the DB URL: `postgresql://postgres:postgres@127.0.0.1:54322/postgres` outputted by the `supabase start` command for both vairables.
[Only needed when deploying manually] Initialize the database:
Note that if you used the Vercel Deploy link from above, the Supabase Vercel integration sets this up automatically for you!
```bash
pnpm db:init
pnpm db:seed # Will throw an error if DB is already seeded, which you can ignore.
```
Prisma will create a `_prisma_migrations` table on the `public` database schema. In Supabase, the public schema is exposed via the API by default. To secure the table, navigate to the [Table Editor](https://supabase.com/dashboard/project/_/editor), click on "RLS diasbaled" > "Enable RLS for this table".
Alternatively, you can run the follow SQL statement on your database, e.g. via the [SQL Editor](https://supabase.com/dashboard/project/_/sql/new) in the Supabase Dashboard:
```sql
ALTER TABLE "public"."_prisma_migrations" ENABLE ROW LEVEL SECURITY;
```
Lastly, in your [Supabase Dashboard](https://supabase.com/dashboard/project/_/storage/buckets) create a public `avatars` bucket to store the profile pictures.
_4.2 Authentication_
Generate a NextAuth secret and add it to your `.env` file:
```bash
openssl rand -hex 32
```
```.env
# Next Auth
# You can generate a new secret on the command line with
# openssl rand -base64 32
#
AUTH_SECRET="SQhGk****"
```
_4.3 Cal_
For **development**, you're all set! We've provided you with our sandbox keys that you can find the `.env.example` file.
For **production**, keep in mind that you'll have to update the `NEXT_PUBLIC_REFRESH_URL` variable to make it point to your deployment, e.g.:
```.env
# 3/ *REFRESH URL.* You have to expose an endpoint that will be used from calcom: https://cal.com/docs/platform/quick-start#4.-backend:-setting-up-refresh-token-endpoint
NEXT_PUBLIC_REFRESH_URL="https://.vercel.app/api/cal/refresh"
```
**5. Development Server**
From here, you're all set. Just start the development server & get going.
```bash
pnpm dev
```
## What's next? How do I make an app with this?
We try to keep this project as simple as possible, so you can start with Cal.com Platform and the scaffolding we set up for you, and add additional things later when they become necessary.
If you are not familiar with the different technologies used in this project, please refer to the respective docs.
- [Cal.com Platform](https://cal.com/platform)
- [Next.js](https://nextjs.org)
- [Supabase](https://supabase.com)
- [NextAuth.js](https://next-auth.js.org)
- [Prisma](https://prisma.io)
- [Tailwind CSS](https://tailwindcss.com)
- [tRPC](https://trpc.io)
## Learn More about Cal.com Platform
Visit our documentation at [cal.com/docs/platform](https://cal.com/docs/platform) or join our [Discord](https://go.cal.com/discord).
Contact sales to purchase a commercial API key here: [cal.com/sales](https://cal.com/sales).
## Learn More about T3
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
- [Documentation](https://create.t3.gg/)
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
## Learn More about Supabase
Supabase is the fastest way to get up and running with Next.js and Postgres. Check out [this video](https://youtu.be/WdA6b0jPNv4?si=eeWpu03PI3W-t5pC) to learn more!
================================================
FILE: with-platform-supabase-tailwind-prisma/components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
================================================
FILE: with-platform-supabase-tailwind-prisma/next.config.js
================================================
import { resolve } from "path";
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.js");
/** @type {import("next").NextConfig} */
const config = {
experimental: {
ppr: true,
},
images: {
formats: ["image/avif", "image/webp"],
remotePatterns: [
{
protocol: "https",
hostname: "picsum.photos",
},
],
loader: "custom",
loaderFile: "./src/lib/supabase-image-loader.ts",
},
};
export default config;
================================================
FILE: with-platform-supabase-tailwind-prisma/package.json
================================================
{
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"db:studio": "prisma studio",
"db:seed": "NODE_ENV=development prisma db seed",
"db:init": "pnpm prisma migrate dev --name=init",
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "NODE_OPTIONS='--max-old-space-size=4096' next lint --fix",
"start": "next start",
"cal:generate": "openapi-endpoint-trimmer -u https://raw.githubusercontent.com/calcom/cal.com/51428087ef0a20f4d775fccbd3a34c2d885aed2b/apps/api/v2/swagger/documentation.json -p /v2/bookings,/v2/event-types,/v2/schedules,/v2/oauth-clients,/v2/oauth -o ./src/cal/__generated/cal-sdk.yml && typed-openapi ./src/cal/__generated/cal-sdk.yml --r zod -o ./src/cal/__generated/cal-sdk.ts",
"typecheck": "tsc --noEmit"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@auth/prisma-adapter": "^1.4.0",
"@calcom/atoms": "^1.0.44",
"@hookform/resolvers": "^3.3.4",
"@libsql/client": "^0.6.0",
"@opentelemetry/api": "^1.8.0",
"@prisma/adapter-libsql": "^5.12.1",
"@prisma/client": "^5.15.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "2.0.2",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@supabase/supabase-js": "^2.43.5",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.25.0",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
"@vercel/analytics": "^1.2.2",
"@zodios/core": "^10.9.6",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^1.0.0",
"dayjs": "^1.11.10",
"lucide-react": "^0.364.0",
"next": "14.3.0-canary.37",
"next-auth": "5.0.0-beta.16",
"next-axiom": "^1.1.1",
"next-themes": "^0.3.0",
"nuqs": "^1.17.4",
"pino": "^8.20.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.51.4",
"react-wrap-balancer": "^1.1.0",
"remeda": "^2.0.3",
"server-only": "^0.0.1",
"sonner": "^1.4.41",
"superjson": "^2.2.1",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"typed-openapi": "^0.4.1",
"usehooks-ts": "^3.1.0",
"zod": "^3.23.7"
},
"devDependencies": {
"@next/eslint-plugin-next": "^14.2.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/eslint": "^8.56.2",
"@types/node": "^20.11.20",
"@types/react": "^18.2.75",
"@types/react-dom": "^18.2.24",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"eslint": "^8.57.0",
"eslint-config-next": "^14.1.3",
"eslint-config-turbo": "^1.13.3",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^3.1.0",
"openapi-endpoint-trimmer": "^2.0.0",
"postcss": "^8.4.34",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"prisma": "^5.15.0",
"tailwindcss": "^3.4.1",
"tsx": "^4.7.2",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
},
"ct3aMetadata": {
"initVersion": "7.30.0"
}
}
================================================
FILE: with-platform-supabase-tailwind-prisma/postcss.config.cjs
================================================
const config = {
plugins: {
tailwindcss: {},
},
};
module.exports = config;
================================================
FILE: with-platform-supabase-tailwind-prisma/prettier.config.mjs
================================================
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
const config = {
bracketSpacing: true,
bracketSameLine: true,
singleQuote: false,
jsxSingleQuote: false,
trailingComma: "es5",
semi: true,
printWidth: 110,
arrowParens: "always",
endOfLine: "auto",
plugins: ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
};
export default config;
================================================
FILE: with-platform-supabase-tailwind-prisma/prisma/client.ts
================================================
import { env } from "@/env";
import { PrismaClient } from "@prisma/client";
const createPrismaClient = () =>
new PrismaClient({
log: env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType | undefined;
};
export const db = globalForPrisma.prisma ?? createPrismaClient();
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
================================================
FILE: with-platform-supabase-tailwind-prisma/prisma/migrations/0_init/migration.sql
================================================
-- CreateSchema
CREATE SCHEMA IF NOT EXISTS "prisma";
-- CreateEnum
CREATE TYPE "prisma"."UserStatus" AS ENUM ('APPROVED', 'PENDING');
-- CreateTable
CREATE TABLE "prisma"."Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "prisma"."Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "prisma"."User" (
"id" TEXT NOT NULL,
"name" TEXT,
"username" TEXT,
"bio" TEXT,
"email" TEXT,
"emailVerified" TIMESTAMP(3),
"hashedPassword" TEXT,
"image" TEXT,
"calAccountId" INTEGER,
"calAccessToken" TEXT,
"calRefreshToken" TEXT,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"status" "prisma"."UserStatus" NOT NULL DEFAULT 'PENDING',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "prisma"."CalAccount" (
"id" INTEGER NOT NULL,
"username" TEXT,
"email" TEXT NOT NULL,
"timeZone" TEXT NOT NULL,
"weekStart" TEXT NOT NULL,
"createdDate" TEXT NOT NULL,
"timeFormat" INTEGER,
"defaultScheduleId" INTEGER,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CalAccount_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "prisma"."VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "prisma"."FilterOption" (
"fieldId" TEXT NOT NULL,
"fieldValue" TEXT NOT NULL,
"fieldLabel" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"filterCategoryFieldId" TEXT NOT NULL,
"filterCategoryValue" TEXT NOT NULL,
"filterCategoryLabel" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "prisma"."FilterOptionsOnUser" (
"userId" TEXT NOT NULL,
"filterOptionFieldId" TEXT NOT NULL,
"filterCategoryFieldId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "prisma"."Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "prisma"."Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "prisma"."User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "prisma"."User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_calAccountId_key" ON "prisma"."User"("calAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "User_calAccessToken_key" ON "prisma"."User"("calAccessToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_calRefreshToken_key" ON "prisma"."User"("calRefreshToken");
-- CreateIndex
CREATE UNIQUE INDEX "CalAccount_username_key" ON "prisma"."CalAccount"("username");
-- CreateIndex
CREATE UNIQUE INDEX "CalAccount_email_key" ON "prisma"."CalAccount"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "prisma"."VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "prisma"."VerificationToken"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "FilterOption_fieldId_key" ON "prisma"."FilterOption"("fieldId");
-- CreateIndex
CREATE INDEX "FilterOption_fieldId_filterCategoryFieldId_idx" ON "prisma"."FilterOption"("fieldId", "filterCategoryFieldId");
-- CreateIndex
CREATE UNIQUE INDEX "FilterOption_fieldId_filterCategoryFieldId_key" ON "prisma"."FilterOption"("fieldId", "filterCategoryFieldId");
-- CreateIndex
CREATE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCatego_idx" ON "prisma"."FilterOptionsOnUser"("userId", "filterOptionFieldId", "filterCategoryFieldId");
-- CreateIndex
CREATE UNIQUE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCatego_key" ON "prisma"."FilterOptionsOnUser"("userId", "filterOptionFieldId", "filterCategoryFieldId");
-- AddForeignKey
ALTER TABLE "prisma"."Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prisma"."Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prisma"."User" ADD CONSTRAINT "User_calAccountId_fkey" FOREIGN KEY ("calAccountId") REFERENCES "prisma"."CalAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prisma"."FilterOptionsOnUser" ADD CONSTRAINT "FilterOptionsOnUser_filterOptionFieldId_filterCategoryFiel_fkey" FOREIGN KEY ("filterOptionFieldId", "filterCategoryFieldId") REFERENCES "prisma"."FilterOption"("fieldId", "filterCategoryFieldId") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "prisma"."FilterOptionsOnUser" ADD CONSTRAINT "FilterOptionsOnUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
================================================
FILE: with-platform-supabase-tailwind-prisma/prisma/schema.prisma
================================================
generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"]
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
schemas = ["prisma"]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime? @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@schema("prisma")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
createdAt DateTime? @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@schema("prisma")
}
model User {
id String @id @default(cuid())
name String?
username String? @unique
bio String?
email String? @unique
emailVerified DateTime?
hashedPassword String?
image String?
calAccountId Int? @unique
calAccessToken String? @unique
calRefreshToken String? @unique
createdAt DateTime? @default(now())
status UserStatus @default(PENDING)
accounts Account[]
selectedFilterOptions FilterOptionsOnUser[]
sessions Session[]
calAccount CalAccount? @relation(fields: [calAccountId], references: [id], onDelete: Cascade)
@@schema("prisma")
}
model CalAccount {
id Int @id
username String? @unique
email String @unique
timeZone String
weekStart String
createdDate String
timeFormat Int?
defaultScheduleId Int?
createdAt DateTime? @default(now())
user User?
@@schema("prisma")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
createdAt DateTime? @default(now())
@@unique([identifier, token])
@@schema("prisma")
}
model FilterOption {
fieldId String @id @unique
fieldValue String
fieldLabel String
createdAt DateTime? @default(now())
filterCategoryFieldId String
filterCategoryValue String
filterCategoryLabel String
selectedByUsers FilterOptionsOnUser[]
@@unique([fieldId, filterCategoryFieldId])
@@index([fieldId, filterCategoryFieldId])
@@schema("prisma")
}
model FilterOptionsOnUser {
userId String
filterOptionFieldId String
filterCategoryFieldId String
createdAt DateTime? @default(now())
filterOption FilterOption @relation(fields: [filterOptionFieldId, filterCategoryFieldId], references: [fieldId, filterCategoryFieldId])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, filterOptionFieldId, filterCategoryFieldId])
@@index([userId, filterOptionFieldId, filterCategoryFieldId])
@@schema("prisma")
}
enum UserStatus {
APPROVED
PENDING
@@schema("prisma")
}
================================================
FILE: with-platform-supabase-tailwind-prisma/prisma/seed.ts
================================================
import { filterOptions } from "@/app/_hardcoded";
import { PrismaClient } from "@prisma/client";
const devDb = new PrismaClient();
async function main() {
for (const filterOption of filterOptions) {
console.log(`attempting to upsert ${filterOption.fieldId}`);
await devDb.filterOption.upsert({
where: { fieldId: filterOption.fieldId },
create: filterOption,
update: filterOption,
});
console.log(`✅ {filterOption.fieldId} upserted`);
}
}
main()
.then(async () => {
await devDb.$disconnect();
})
.catch(async (e) => {
console.error(e);
await devDb.$disconnect();
process.exit(1);
});
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/[eventSlug]/page.tsx
================================================
import ExpertBooker from "../_components/expert-booker";
import { cal } from "@/cal/api";
import Image from "next/image";
import { db } from "prisma/client";
export const dynamic = "force-dynamic";
export default async function BookerPage({
params,
}: {
params: { expertUsername: string; eventSlug: string };
}) {
const expert = await db.user.findUnique({
where: { username: params.expertUsername },
select: {
id: true,
calAccessToken: true,
calRefreshToken: true,
calAccountId: true,
name: true,
username: true,
calAccount: {
select: {
id: true,
username: true,
},
},
},
});
if (!expert?.calAccount?.username) {
console.warn("Expert not found", params.expertUsername);
return Expert not found
;
}
const eventType = await cal({
user: {
calAccessToken: expert.calAccessToken,
calRefreshToken: expert.calRefreshToken,
calAccountId: expert.calAccountId,
id: expert.id,
},
}).get("/v2/event-types/{username}/{eventSlug}/public", {
path: {
username: expert.calAccount.username,
eventSlug: params.eventSlug,
},
query: {
isTeamEvent: false,
},
});
if (eventType.status === "error") {
console.warn(
`[BookerPage] Event not found for event slug '${params.eventSlug}'. Check logs above for more info.`
);
return Event not found
;
}
const descriptionWithoutHtmlTags = eventType.data?.description.replace(/<[^>]*>?/gm, "");
return (
{expert.name}: {eventType.data?.title}
{descriptionWithoutHtmlTags}
{Boolean(expert.calAccount) && (
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/AboutSection.tsx
================================================
'use client'
import { useState } from 'react'
import clsx from 'clsx'
import { Info } from 'lucide-react'
export function AboutSection(props: React.ComponentPropsWithoutRef<'section'>) {
const [isExpanded, setIsExpanded] = useState(false)
return (
About
In this show, Eric and Wes dig deep to get to the facts with guests who
have been labeled villains by a society quick to judge, without actually
getting the full story. Tune in every Thursday to get to the truth with
another misunderstood outcast as they share the missing context in their
tragic tale.
{!isExpanded && (
setIsExpanded(true)}
>
Show more
)}
)
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/Container.tsx
================================================
import { cn } from "@/lib/utils";
export function Container({ className, children, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/expert-booker.tsx
================================================
"use client";
import { Booker, useEventTypesPublic } from "@calcom/atoms";
import type { CalAccount, User } from "@prisma/client";
import { Loader } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner";
/**
* [@calcom] Make sure to wrap your app with our `CalProvider` to enable the use of our hooks.
* @link https://cal.com/docs/platform/quick-start#5.3-setup-root-of-your-app
*/
type BookerProps = Parameters[number];
export const ExpertBooker = (
props: {
className?: string;
calAccount: Pick;
expert: Pick;
} & Partial
) => {
const { className, calAccount, expert, ...rest } = props;
const router = useRouter();
const searchParams = useSearchParams();
const rescheduleUid = searchParams.get("rescheduleUid") ?? undefined;
const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypesPublic(calAccount.username ?? "");
if (!calAccount.username) {
return Sorry. We couldn't find this experts' user.
;
}
if (isLoadingEvents) {
return (
);
}
if (!eventTypes?.length) {
return (
Sorry. Unable to load ${expert.name}'s availabilities.
);
}
return (
{
toast.success("Booking successful! ");
router.push(
`/${expert.username}/booking/${booking.data.uid}${booking.data.fromReschedule ? `?${new URLSearchParams({ fromReschedule: booking.data.fromReschedule }).toString()}` : ""}`
);
}}
rescheduleUid={rescheduleUid}
{...rest}
/>
);
};
export default ExpertBooker;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/booking/[bookingUid]/page.tsx
================================================
import { BookingResult } from "@/app/_components/booking-result";
import { Suspense } from "react";
export default function Booking() {
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/layout.tsx
================================================
import { Logo } from "../_components/universal/logo";
import { SignedIn, SignedOut } from "@/auth";
import { Button } from "@/components/ui/button";
import { LogIn } from "lucide-react";
import Link from "next/link";
import { type ReactNode } from "react";
export default function ExpertLayout({ children }: { children?: ReactNode }) {
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/page.tsx
================================================
import { cal } from "@/cal/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ArrowRight } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { db } from "prisma/client";
export const dynamic = "force-dynamic";
export default async function ExpertDetails({ params }: { params: { expertUsername: string } }) {
const expert = await db.user.findUnique({
where: { username: params.expertUsername },
select: {
id: true,
calAccessToken: true,
calRefreshToken: true,
calAccountId: true,
name: true,
username: true,
bio: true,
calAccount: {
select: {
id: true,
username: true,
},
},
},
});
if (!expert?.calAccount?.username) {
console.warn("Expert not found", params.expertUsername);
return Expert not found
;
}
const eventTypes = await cal({
user: {
calAccessToken: expert.calAccessToken,
calRefreshToken: expert.calRefreshToken,
calAccountId: expert.calAccountId,
id: expert.id,
},
}).get("/v2/event-types/{username}/public", {
path: {
username: expert.calAccount.username,
},
});
if (eventTypes.status === "error") {
console.warn(
`[ExpertDetails] Event not found for expert username '${params.expertUsername}'. Check logs above for more info.`
);
}
return (
{eventTypes.status === "error" ? (
User Events not found
) : (
Book Us
Book us for any of the below events.
Name
Description
Duration (min)
Availability
{eventTypes.data.map((eventType) => (
{eventType.title}
/{eventType.slug}
{eventType.description}
{eventType.length}
))}
Showing{" "}
{eventTypes.data.length > 0 ? 1 : 0}-
{eventTypes.data.length > 10 ? 10 : eventTypes.data.length}
{" "}
of {eventTypes.data.length} event types
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_actions.tsx
================================================
"use server";
import { type LoginFormState } from "./login/_components/login";
import { LoginSchema, SignupSchema, auth, signIn, unstable_update, FiltersSchema } from "@/auth";
import { type User } from "@prisma/client";
import { type Prisma } from "@prisma/client";
import { AuthError } from "next-auth";
import { revalidatePath } from "next/cache";
import { isRedirectError } from "next/dist/client/components/redirect";
import { db } from "prisma/client";
import { z } from "zod";
export async function signInWithCredentials(_prevState: LoginFormState, formData: FormData) {
try {
const credentials = LoginSchema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
});
if (!credentials.success) {
return {
inputErrors: credentials.error.flatten().fieldErrors,
};
}
await signIn("credentials", formData);
return { error: null };
} catch (error) {
if (isRedirectError(error)) throw error;
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid credentials." };
default:
console.error("Uncaught error signing in (AuthError): ", error);
return { error: "Something went wrong." };
}
}
console.error("Uncaught error signing in", error);
throw error;
}
}
export async function addUserFilters(_prevState: { error?: string | null }, formData: FormData) {
try {
const sesh = await auth();
if (!sesh?.user?.id) return { error: "User not logged in " };
const filters = FiltersSchema.safeParse({
categories: formData.get("categories"),
capabilities: formData.get("capabilities"),
frameworks: formData.get("frameworks"),
budgets: formData.get("budgets"),
languages: formData.get("languages"),
regions: formData.get("regions"),
});
if (!filters.success) {
return {
inputErrors: filters.error.flatten().fieldErrors,
};
}
const selectedFilterOptions = [
{ filterOpdtionFieldIds: filters.data.budgets, filterCategoryFieldId: "budgets" },
{ filterOpdtionFieldIds: filters.data.capabilities, filterCategoryFieldId: "capabilities" },
{ filterOpdtionFieldIds: filters.data.categories, filterCategoryFieldId: "categories" },
{ filterOpdtionFieldIds: filters.data.frameworks, filterCategoryFieldId: "frameworks" },
{ filterOpdtionFieldIds: filters.data.languages, filterCategoryFieldId: "languages" },
]
.map(({ filterOpdtionFieldIds, filterCategoryFieldId }) => {
return filterOpdtionFieldIds.map((fieldId) => {
return {
filterCategoryFieldId,
filterOptionFieldId: fieldId,
userId: sesh?.user.id,
};
});
})
// to filter out any null values:
.filter(Boolean) as Prisma.FilterOptionsOnUserCreateManyInput[][];
const data = selectedFilterOptions.flat();
const createOrUpdateFilterPromises: Array> = [];
for (const filter of data) {
createOrUpdateFilterPromises.push(
db.filterOptionsOnUser.upsert({
where: {
userId_filterOptionFieldId_filterCategoryFieldId: {
userId: filter.userId,
filterOptionFieldId: filter.filterOptionFieldId,
filterCategoryFieldId: filter.filterCategoryFieldId,
},
},
update: filter,
create: filter,
})
);
}
await Promise.all(createOrUpdateFilterPromises);
return { success: true };
} catch (err) {
throw err;
}
}
export async function signUpWithCredentials(_prevState: { error?: string | null }, formData: FormData) {
try {
const credentials = SignupSchema.safeParse({
name: formData.get("name"),
username: formData.get("username"),
email: formData.get("email"),
password: formData.get("password"),
});
if (!credentials.success) {
return {
inputErrors: credentials.error.flatten().fieldErrors,
};
}
await signIn("credentials", formData);
return { error: null };
} catch (error) {
if (isRedirectError(error)) throw error;
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid credentials." };
default:
console.error("Uncaught error signing in (AuthError): ", error);
return { error: "Something went wrong." };
}
}
console.error("Uncaught error signing in", error);
throw error;
}
}
export async function expertEdit(
_prevState: { error: null | string } | { success: null | string },
formData: FormData
) {
console.log("[_actions] Updating expert with form data: ", formData);
const sesh = await auth();
if (!sesh?.user.id) {
console.log("[_actions] Unauthorized user edit", formData);
return { error: "Unauthorized" };
}
const formDataWithoutActionFields = Object.fromEntries(
Array.from(formData.entries()).filter(([key]) => !key.toLowerCase().startsWith("$action"))
);
const userEdit = z
.object({
name: z.string().min(1).max(255),
})
.or(z.object({ bio: z.string().min(1).max(255) }))
.safeParse(formDataWithoutActionFields);
if (!userEdit.success) {
console.log("[_actions] Inavlid form data", formData);
return { error: "Invalid form data" };
}
const key = Object.keys(userEdit.data)[0];
if (!key) {
console.error("[_actions] Invalid form data", formData);
return { error: "Invalid form data" };
}
let user: User | null;
try {
user = await db.user.update({
where: { id: sesh.user.id },
data: {
// @ts-expect-error - key as "name" | "bio" didn't work -- not sure why
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
[key]: userEdit.data[key],
},
});
} catch (error) {
console.error("Uncaught error updating expert", error);
return { error: "Internal Server Error" };
}
revalidatePath("/dashboard/settings/profile");
await unstable_update({ user: { name: user.name } });
// @ts-expect-error - key as "name" | "bio" didn't work -- not sure why
return { success: `Successfully updated your ${key} to: '${userEdit.data[key]}'.` };
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/autocomplete.tsx
================================================
"use client";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
import { useRouter } from "next/navigation";
import React, { forwardRef, useState } from "react";
export const defaultSort = {
title: "Relevance",
slug: null,
sortKey: "RELEVANCE",
reverse: false,
};
export const sorting = [
defaultSort,
{
title: "Availability",
slug: "available-desc",
sortKey: "MOST_AVAILABLE",
reverse: false,
}, // asc
];
export type Option = { value: string; label: string };
export interface AutocompleteSearchProps extends React.ComponentPropsWithoutRef {
className?: string;
options: Array;
initialSearch?: string;
placement?: "header";
}
export const AutocompleteSearch = forwardRef(
({ className, placement, options, initialSearch, ...props }, ref) => {
const initialSeletion = options.find((option) => option.value === initialSearch);
const [value, setValue] = useState(initialSeletion?.value ?? "");
const [open, setOpen] = useState(false);
const [query, setQuery] = useState(initialSeletion?.label ?? "");
const router = useRouter();
return (
{
if (e.currentTarget.contains(e.relatedTarget)) return;
if (value && !query) {
setQuery(options.find((option) => option.value === value)?.label ?? "");
}
setOpen(false);
}}>
{
setOpen(true);
if (query === options.find((option) => option.value === value)?.label) {
setQuery("");
}
}}
onValueChange={(value) => setQuery(value)}
className="h-full justify-center leading-[2.75rem]"
/>
{open && (
No expert found.
{options.map((option) => (
{
setValue(newValue);
setQuery(options.find((option) => option.value === newValue)?.label ?? "");
setOpen(false);
router.push(`/experts?${new URLSearchParams({ q: newValue }).toString()}`, {
scroll: false,
});
}}>
{option.label}
))}
)}
);
}
);
AutocompleteSearch.displayName = "AutocompleteSearch";
export default AutocompleteSearch;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/banner.tsx
================================================
import { Button } from "@/components/ui/button";
export default function Banner({
title,
description,
ctaLink,
}: {
title: string;
description: string;
ctaLink: string;
}) {
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/booking-result.tsx
================================================
"use client";
import { stripCalOAuthClientIdFromEmail, stripCalOAuthClientIdFromText } from "@/cal/utils";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { useGetBooking, useCancelBooking } from "@calcom/atoms";
import dayjs from "dayjs";
import { Check, ExternalLinkIcon, Loader, X } from "lucide-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
import type { BookingStatus } from "node_modules/@calcom/atoms/dist/packages/prisma/enums";
export const BookingResult = (props: {
expertusername?: string;
bookingUid?: string;
fromReschedule?: string;
}) => {
const params = useParams<{ expertUsername: string; bookingUid: string }>();
const expertUsername = props?.expertusername ?? params.expertUsername;
const bookingUid = props?.bookingUid ?? params.bookingUid;
const searchParams = useSearchParams();
const fromReschedule = props?.fromReschedule ?? searchParams.get("fromReschedule");
const { isLoading, data: booking, refetch } = useGetBooking(bookingUid ?? "");
// TODO: We're doing this to cast the type since @calcom/atoms doesn't type them properly
const bookingStatus = booking && "status" in booking ? (booking.status as BookingStatus) : undefined;
const { mutate: cancelBooking } = useCancelBooking({
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSuccess: async () => {
await refetch();
},
});
// [@calcom] The API returns the UID of the previous booking in case you'd like to show changed booking details in your UI.
const bookingPrevious = useGetBooking(fromReschedule ?? "");
if (!bookingUid) {
return No Booking UID.
;
}
if (isLoading) {
return ;
}
if (!booking) {
return Booking not found
;
}
const what = stripCalOAuthClientIdFromText(booking.title) ?? booking.title;
const formerWhat = bookingPrevious?.data
? stripCalOAuthClientIdFromText(bookingPrevious?.data?.title)
: null;
const when = `${dayjs(booking.startTime).format("dddd, MMMM DD YYYY @ h:mma")} (${booking?.user?.timeZone})`;
const formerWhen = bookingPrevious.data
? `${dayjs(bookingPrevious.data.startTime).format("dddd, MMMM DD YYYY @ h:mma")} (${bookingPrevious?.data?.user?.timeZone})`
: null;
const who = {
host: `${booking?.user?.name} (Host) - ${stripCalOAuthClientIdFromEmail(booking?.user?.email ?? "")}`,
attendees: booking.attendees.map(
(attendee) => `${attendee.name ? `${stripCalOAuthClientIdFromText(attendee.name)} - ` : ""}
${stripCalOAuthClientIdFromEmail(attendee.email)}`
),
};
const formerWho = bookingPrevious?.data
? {
host: `${bookingPrevious.data?.user?.name} (Host) - ${stripCalOAuthClientIdFromEmail(bookingPrevious.data?.user?.email ?? "")}`,
attendees: bookingPrevious.data.attendees.map(
(
previousAttendee
) => `${previousAttendee.name ? `${stripCalOAuthClientIdFromText(previousAttendee.name)} - ` : ""}
${stripCalOAuthClientIdFromEmail(previousAttendee.email)}`
),
}
: null;
return (
{bookingStatus?.toLowerCase() === "cancelled" && (
)}
{bookingStatus?.toLowerCase() === "accepted" && (
Meeting scheduled successfully
)}
What
{formerWhat !== what && (
{formerWhat}
)}
{what}
When
{formerWhen !== when && (
{formerWhen}
)}
{when}
Who
{who.host}
{who.attendees.map((attendee, idx) => (
formerAttendee === attendee) ===
// -1 && "font-semibold italic"
)}>
{attendee}
{formerWho?.attendees?.findIndex((formerAttendee) => formerAttendee === attendee) ===
-1 && (
New
)}
))}
{formerWho?.attendees?.map(
(formerAttendee, idx) =>
// if the attendee is in the current booking, we've already displayed them
who.attendees.findIndex((attendee) => attendee === formerAttendee) === -1 && (
attendee === formerAttendee) === -1 &&
"line-through"
)}>
{formerAttendee}
)
)}
Where
{/* Display the previous location only if it's different from the current booking */}
{bookingPrevious.data?.location !== booking.location && (
{bookingPrevious.data?.location === "integrations:daily" ? (
Online (Cal Video)
) : (
bookingPrevious.data?.location
)}
)}
{/* Display the location of the current booking */}
{booking?.location === "integrations:daily" ? (
Online (Cal Video)
) : (
booking.location
)}
{booking.description && (
Event Description
{booking.description !== bookingPrevious.data?.description && (
{bookingPrevious?.data?.description}
)}
{booking.description}
)}
{bookingStatus?.toLowerCase() === "cancelled" ? (
Want to book {booking?.user?.name}?
{" "}
See{" "}
availabilities
) : (
Need to make changes?
{" "}
Reschedule
{" "}
or{" "}
{
return cancelBooking({
id: booking.id,
uid: booking.uid,
cancellationReason: "User request",
allRemainingBookings: true,
});
}}>
Cancel
)}
);
};
export default BookingResult;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/home/results.tsx
================================================
"use client";
import { SearchBar } from "../search-bar";
import SidebarItem from "./sidebar-item";
import { filterOptions } from "@/app/_hardcoded";
import { filterSearchParamSchema } from "@/app/_searchParams";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import { type FilterOption, type User } from "@prisma/client";
import { ListFilter, Loader } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useQueryState, parseAsString, parseAsJson } from "nuqs";
import { type db } from "prisma/client";
import { Fragment, type ReactEventHandler, useState, type SyntheticEvent } from "react";
import React, { Suspense } from "react";
import { Balancer } from "react-wrap-balancer";
import { prop, uniqueBy } from "remeda";
export default function ResultsCard({
slug,
userId,
title,
description,
query,
}: {
slug: string;
userId?: string;
title: string;
description: string;
query?: string;
}) {
const queryIndexTitle = title.toLowerCase().indexOf(query?.toLowerCase() ?? "");
const queryIndexDescription = description.toLowerCase().indexOf(query?.toLowerCase() ?? "");
const [error, setError] = useState | null>(null);
const [isLoading, setIsLoading] = useState(true);
return (
{!error && (
setIsLoading(false)}
onError={setError}
/>
)}
{/* this highlights the search query for the title */}
{queryIndexTitle != undefined && query ? (
<>
{title.substring(0, queryIndexTitle)}
{title.substring(queryIndexTitle, queryIndexTitle + query.length)}
{title.substring(queryIndexTitle + query.length)}
>
) : (
title
)}
{/* this highlights the search query for the title */}
{queryIndexDescription != undefined && query ? (
<>
{description.substring(0, queryIndexDescription)}
{description.substring(queryIndexDescription, queryIndexDescription + query.length)}
{description.substring(queryIndexDescription + query.length)}
>
) : (
description
)}
);
}
type UsersWithFilterOptions = Awaited<
ReturnType<
typeof db.user.findMany<{
include: { selectedFilterOptions: { include: { filterOption: true } } };
}>
>
>;
export function Results(props: { experts: UsersWithFilterOptions; signedOut: JSX.Element }) {
const [query] = useQueryState("q", parseAsString);
// eslint-disable-next-line @typescript-eslint/unbound-method
const [filters] = useQueryState("f", parseAsJson(filterSearchParamSchema.parse));
const filtersByCategory = uniqueBy(filterOptions, prop("filterCategoryFieldId"));
// this is the query string search:
const experts = props.experts
.filter((expert) => {
if (!query) return true;
return (
expert?.name?.toLowerCase().includes(query?.toLowerCase()) ||
expert?.bio?.toLowerCase().includes(query?.toLowerCase())
);
})
// this is the filter search:
.filter((expert) => {
if (!filters) return true;
const expertSelectedOptions = expert.selectedFilterOptions ?? [];
// if we have filters selected, let's only show the experts who have all the selected filters:
return Object.entries(filters).every(([_filterCategoryFieldId, filterValues]) => {
if (!filterValues) return true;
return filterValues.every((filterValue) =>
expertSelectedOptions.find((option) => option.filterOption.fieldValue === filterValue)
);
});
});
return (
}>
Filter
{filtersByCategory.map((section) => (
{section.filterCategoryLabel}
{filterOptions
.filter(
(filterOption) =>
filterOption.filterCategoryFieldId === section.filterCategoryFieldId
)
.map((filterOption) => (
))}
))}
{!query && props.signedOut}
{experts.length ? (
experts.map(({ username, name, bio, id }) => (
))
) : (
No experts found
We’ve filtered our experts based on your search query and selected filters:
Query
{query ?? "No search query provided"}
Filters
{Object.keys(filters ?? {}).length
? Object.entries(filters ?? {})
.map(([filterCategory, filterValues]) => {
return `${filterCategory}: ${filterValues.join(", ")}`;
})
.join(", ")
: "No filters selected"}
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/home/sidebar-item.tsx
================================================
"use client";
import { type filterOptions } from "@/app/_hardcoded";
import { filterSearchParamSchema } from "@/app/_searchParams";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { useQueryState, parseAsJson } from "nuqs";
export default function SidebarItem({
id,
label,
className,
category,
...props
}: {
className?: string;
id: (typeof filterOptions)[number]["fieldId"];
label: (typeof filterOptions)[number]["fieldLabel"];
category: (typeof filterOptions)[number]["filterCategoryFieldId"];
} & React.ComponentPropsWithoutRef) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const [filters, setFilters] = useQueryState("f", parseAsJson(filterSearchParamSchema.parse));
const selectedIds = filters?.[category];
return (
{
const newSelectedIds = [
...(selectedIds?.filter((selectedId) => selectedId !== id) || []),
...(checked ? [id] : []),
];
const newCategoryFilter = { [category]: newSelectedIds };
const { [category]: _, ...withoutCurrentCategory } = newCategoryFilter; // remove current category from filters
// include new category filter if there are selected ids
const newFilters = {
...withoutCurrentCategory,
...(newSelectedIds.length > 0 ? newCategoryFilter : {}),
};
await setFilters(newFilters);
}}
{...props}
/>
{label}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/home/signup-card.tsx
================================================
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";
export const SignupCard = () => {
return (
Are you an expert of Cal.com ?
Sign up, connect your calendar and fill your schedule with exciting customers who need help with
anything Cal.com-related!
Sign Up
Already have an account?{" "}
Log in
);
};
export default SignupCard;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/multi-select.tsx
================================================
"use client";
import { Badge } from "@/components/ui/badge";
import { Command, CommandGroup, CommandItem, CommandList } from "@/components/ui/command";
import { Command as CommandPrimitive } from "cmdk";
import { X } from "lucide-react";
import * as React from "react";
export type Option = Record<"value" | "label", string>;
export function FancyMultiSelect(
props: {
options: Array;
placeholder: string;
} & React.ComponentProps
) {
const { options, id, name, ...inputProps } = props;
const initiallySelected = options?.[0];
const inputRef = React.useRef(null);
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState>([initiallySelected ?? null]);
const [inputValue, setInputValue] = React.useState("");
const handleUnselect = React.useCallback((option: Option) => {
setSelected((prev) => prev.filter((s) => s?.value !== option.value));
}, []);
const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
const input = inputRef.current;
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "") {
setSelected((prev) => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
});
}
}
// This is not a default behaviour of the field
if (e.key === "Escape") {
input.blur();
}
}
}, []);
const selectables = options.filter((option) => {
// Filter out already selected options
return !selected.some((selectedOption) => selectedOption?.value === option.value);
});
return (
{open && selectables.length > 0 ? (
{selectables.map((option) => {
return (
{
e.preventDefault();
e.stopPropagation();
}}
onSelect={(_value) => {
setInputValue("");
setSelected((prev) => [...prev, option]);
}}
className={"cursor-pointer"}>
{option.label}
);
})}
) : null}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/navigation.tsx
================================================
"use client";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { cn } from "@/lib/utils";
import Link from "next/link";
import * as React from "react";
const components: { title: string; href: string; description: string }[] = [
{
title: "Alert Dialog",
href: "/docs/primitives/alert-dialog",
description: "A modal dialog that interrupts the user with important content and expects a response.",
},
{
title: "Hover Card",
href: "/docs/primitives/hover-card",
description: "For sighted users to preview content available behind a link.",
},
{
title: "Progress",
href: "/docs/primitives/progress",
description:
"Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
},
{
title: "Scroll-area",
href: "/docs/primitives/scroll-area",
description: "Visually or semantically separates content.",
},
{
title: "Tabs",
href: "/docs/primitives/tabs",
description: "A set of layered sections of content—known as tab panels—that are displayed one at a time.",
},
{
title: "Tooltip",
href: "/docs/primitives/tooltip",
description:
"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
},
];
export function Navigation() {
return (
Getting started
Components
{components.map((component) => (
{component.description}
))}
Documentation
);
}
const ListItem = React.forwardRef, React.ComponentPropsWithoutRef<"a">>(
({ className, title, children, ...props }, ref) => {
return (
{title}
{children}
);
}
);
ListItem.displayName = "ListItem";
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/search-bar.tsx
================================================
"use client";
import { Input } from "@/components/ui/input";
import { useQueryState, parseAsString } from "nuqs";
export const SearchBar = () => {
const [query, setQuery] = useQueryState("q", parseAsString);
return (
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/submit-button.tsx
================================================
"use client";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Loader } from "lucide-react";
import { type ReactNode } from "react";
import { useFormStatus } from "react-dom";
export const ButtonSubmit = ({
className,
children,
...props
}: {
children: ReactNode;
className?: string;
} & ButtonProps) => {
const { pending } = useFormStatus();
return (
<>
{pending ? (
) : (
children
)}
>
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/universal/hero.tsx
================================================
import { Balancer } from "react-wrap-balancer";
interface HeroProps {
title: string;
children?: React.ReactNode;
}
export const Hero = ({ title, children }: HeroProps) => {
return (
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/universal/layout.tsx
================================================
import { cn } from "@/lib/utils";
import React from "react";
type BaseProps = {
children: React.ReactNode;
className?: string;
};
type LayoutProps = BaseProps & {
flex?: boolean;
fullWidth?: boolean;
align?: "center" | "start" | "end";
};
export const LayoutAside = ({ children, className }: BaseProps) => {
return ;
};
export const LayoutMain = ({ children, className }: BaseProps) => {
return {children} ;
};
export const Layout = ({
children,
className,
flex = false,
fullWidth = true,
align,
}: BaseProps & LayoutProps) => {
return (
{children}
);
};
Layout.Aside = LayoutAside;
Layout.Main = LayoutMain;
export default Layout;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/universal/logo.tsx
================================================
import { cn } from "@/lib/utils";
import Link from "next/link";
export const Logo = ({ href, className }: { href?: string; className?: string }) => {
return (
Cal.com ®
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/use-cal.tsx
================================================
"use client";
import { env } from "@/env";
import { CalProvider } from "@calcom/atoms";
import { use } from "react";
/**
* [@calcom] This is necessary since the CalProvider currently doesn't have the "use client" directive set (it's written for pre-server-components react)
*
* *Important*: You have to provide this component with a promise passed from the server compoent
* @Usage
```tsx
import { currentUser} from "@/auth";
export default async function RootLayout(props: { children: ReactNode }) {
return (
{* note how we're passing the unresolved promise as props*}
dbUser?.calAccessToken ?? undefined)}>
{props.children}
)
}
```
*/
export default function UseCalAtoms(props: {
children: React.ReactNode;
calAccessToken: Promise;
}) {
const accessToken = use(props.calAccessToken);
return (
{props.children}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_hardcoded.ts
================================================
import type { FilterOption } from "@prisma/client";
// TODO: move to database after signup
export const categoryOptions = [
{
fieldId: "freelancer",
fieldLabel: "Freelancer",
fieldValue: "freelancer",
filterCategoryFieldId: "categories",
filterCategoryLabel: "Category",
filterCategoryValue: "categories",
createdAt: new Date(),
},
{
fieldId: "agency",
fieldLabel: "Agency",
fieldValue: "agency",
filterCategoryFieldId: "categories",
filterCategoryLabel: "Category",
filterCategoryValue: "categories",
createdAt: new Date(),
},
{
fieldId: "product_studio",
fieldLabel: "Product Studio",
fieldValue: "product_studio",
filterCategoryFieldId: "categories",
filterCategoryLabel: "Category",
filterCategoryValue: "categories",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const capabilityOptions = [
{
fieldId: "ecommerce",
fieldLabel: "Ecommerce",
fieldValue: "ecommerce",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "product_management",
fieldLabel: "Product Management",
fieldValue: "product_management",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "app_development",
fieldLabel: "App Development",
fieldValue: "app_development",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "design",
fieldLabel: "Design",
fieldValue: "design",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "ui_ux",
fieldLabel: "UI/UX Development",
fieldValue: "ui_ux",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "integration_services",
fieldLabel: "Integration Services",
fieldValue: "integration_services",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "branding",
fieldLabel: "Branding",
fieldValue: "branding",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "digital_marketing",
fieldLabel: "Digital Marketing",
fieldValue: "digital_marketing",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "mobile_development",
fieldLabel: "Mobile Development",
fieldValue: "mobile_development",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "ai",
fieldLabel: "AI",
fieldValue: "ai",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
{
fieldId: "web3-crypto",
fieldLabel: "Web3 / Crypto",
fieldValue: "web3-crypto",
filterCategoryFieldId: "capabilities",
filterCategoryLabel: "Capabilities",
filterCategoryValue: "capabilities",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const frameworkOptions = [
{
fieldId: "nextjs",
fieldLabel: "Next.js",
fieldValue: "nextjs",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "nuxtjs",
fieldLabel: "Nuxt.js",
fieldValue: "nuxtjs",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "svelte",
fieldLabel: "Svelte",
fieldValue: "svelte",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "gatsby",
fieldLabel: "Gatsby",
fieldValue: "gatsby",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "angular",
fieldLabel: "Angular",
fieldValue: "angular",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "ember",
fieldLabel: "Ember",
fieldValue: "ember",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
{
fieldId: "vue",
fieldLabel: "Vue",
fieldValue: "vue",
filterCategoryFieldId: "frameworks",
filterCategoryLabel: "Frameworks",
filterCategoryValue: "frameworks",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const budgetOptions = [
{
fieldId: "1000",
fieldLabel: "$1,000 - $4,999",
fieldValue: "1000",
filterCategoryFieldId: "budgets",
filterCategoryLabel: "Budgets",
filterCategoryValue: "budgets",
createdAt: new Date(),
},
{
fieldId: "5000",
fieldLabel: "$5,000 - $9,999",
fieldValue: "5000",
filterCategoryFieldId: "budgets",
filterCategoryLabel: "Budgets",
filterCategoryValue: "budgets",
createdAt: new Date(),
},
{
fieldId: "10000",
fieldLabel: "$10,000 - $49,999",
fieldValue: "10000",
filterCategoryFieldId: "budgets",
filterCategoryLabel: "Budgets",
filterCategoryValue: "budgets",
createdAt: new Date(),
},
{
fieldId: "50000",
fieldLabel: "$50,000 - $99,999",
fieldValue: "50000",
filterCategoryFieldId: "budgets",
filterCategoryLabel: "Budgets",
filterCategoryValue: "budgets",
createdAt: new Date(),
},
{
fieldId: "100000",
fieldLabel: "$100,000+",
fieldValue: "100000",
filterCategoryFieldId: "budgets",
filterCategoryLabel: "Budgets",
filterCategoryValue: "budgets",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const languageOptions = [
{
fieldId: "english",
fieldLabel: "English",
fieldValue: "english",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "portugese",
fieldLabel: "Portuguese",
fieldValue: "portugese",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "spanish",
fieldLabel: "Spanish",
fieldValue: "spanish",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "chinese",
fieldLabel: "Chinese",
fieldValue: "chinese",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "french",
fieldLabel: "French",
fieldValue: "french",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "japanese",
fieldLabel: "Japanese",
fieldValue: "japanese",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
{
fieldId: "german",
fieldLabel: "German",
fieldValue: "german",
filterCategoryFieldId: "languages",
filterCategoryLabel: "Languages Spoken",
filterCategoryValue: "languages",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const regions = [
{
fieldId: "asia",
fieldLabel: "Asia",
fieldValue: "asia",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "australia",
fieldLabel: "Australia and New Zealand",
fieldValue: "australia",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "europe",
fieldLabel: "Europe",
fieldValue: "europe",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "latin_america",
fieldLabel: "Latin America",
fieldValue: "latin_america",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "middle_east",
fieldLabel: "Middle East",
fieldValue: "middle_east",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "north_america",
fieldLabel: "North America",
fieldValue: "north_america",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
{
fieldId: "remote",
fieldLabel: "Remote",
fieldValue: "remote",
filterCategoryFieldId: "regions",
filterCategoryLabel: "Region",
filterCategoryValue: "regions",
createdAt: new Date(),
},
] as const satisfies FilterOption[];
export const filterOptions = [
...categoryOptions,
...capabilityOptions,
...frameworkOptions,
...budgetOptions,
...languageOptions,
...regions,
] as const satisfies FilterOption[];
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/_searchParams.ts
================================================
import {
capabilityOptions,
categoryOptions,
budgetOptions,
frameworkOptions,
languageOptions,
regions,
} from "./_hardcoded";
import { z } from "zod";
export const renderingOptions = ["server", "client"] as const;
export type RenderingOptions = (typeof renderingOptions)[number];
function nonEmptyArray(arr: Array): [ElementType, ...ElementType[]] {
return [arr[0]!, ...arr.slice(1)];
}
const categoriesEnum = z.enum(nonEmptyArray(categoryOptions.flatMap((option) => option.fieldValue)));
const capabilitiesEnum = z.enum(nonEmptyArray(capabilityOptions.flatMap((option) => option.fieldValue)));
const budgetsEnum = z.enum(nonEmptyArray(budgetOptions.flatMap((option) => option.fieldValue)));
const frameworksEnum = z.enum(nonEmptyArray(frameworkOptions.flatMap((option) => option.fieldValue)));
const languagesEnum = z.enum(nonEmptyArray(languageOptions.flatMap((option) => option.fieldValue)));
const regionsEnum = z.enum(nonEmptyArray(regions.flatMap((option) => option.fieldValue)));
export const querySearchParamSchema = z.string().optional();
export const filterSearchParamSchema = z
.object({
categories: z.array(categoriesEnum).optional(),
capabilities: z.array(capabilitiesEnum).optional(),
budgets: z.array(budgetsEnum).optional(),
frameworks: z.array(frameworksEnum).optional(),
languages: z.array(languagesEnum).optional(),
regions: z.array(regionsEnum).optional(),
})
.optional();
export const searchParamsSchema = z
.object({
q: z.string().optional(),
f: z
.object({
categories: z.array(categoriesEnum).optional(),
capabilities: z.array(capabilitiesEnum).optional(),
budgets: z.array(budgetsEnum).optional(),
frameworks: z.array(frameworksEnum).optional(),
languages: z.array(languagesEnum).optional(),
regions: z.array(regionsEnum).optional(),
})
.optional(),
})
.optional();
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/api/auth/[...nextauth]/route.ts
================================================
export { GET, POST } from "@/auth";
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/api/cal/refresh/route.ts
================================================
import { authConfig } from "@/auth/config.edge";
import { type KeysResponseDto } from "@/cal/__generated/cal-sdk";
import { refreshTokens } from "@/cal/api";
import NextAuth from "next-auth";
import { db } from "prisma/client";
export const dynamic = "force-dynamic"; // defaults to auto
export const GET = NextAuth(authConfig).auth(async function GET(request) {
const authorizationHeader = request.headers.get("Authorization");
const token = authorizationHeader?.replace("Bearer ", "");
if (!request.auth) {
// deny any request that doesn't come from a browser from our website
console.warn(`[Cal Refresh] Refresh route was triggered without a session attached.`);
return new Response(JSON.stringify({ data: "Unauthorized" }), { status: 401 });
}
if (!token) {
console.warn(`[Cal Refresh] Refresh route was triggered without an Authorization token.`);
return new Response(JSON.stringify({ data: "Unauthorized" }), { status: 401 });
}
// try to look up the user
const user = await db.user.findUnique({
where: {
calAccessToken: token,
},
include: { calAccount: true },
});
// if we can't lookup the user from the provided token, return a 404
if (!user) {
return new Response(JSON.stringify({ data: "Not Found" }), { status: 404 });
}
try {
if (!user.calAccount || !user.calRefreshToken) {
throw new Error(`[Cal Refresh] User with id ${user.id} does not have a calAccount or a refresh token.`);
}
/** [@calcom] Attempt to refresh the token via the refresh flow
*/
const refreshFlow = await refreshTokens({
refreshToken: user.calRefreshToken,
calAccountId: user.calAccount.id,
});
if (refreshFlow.status === "error") {
console.error(`[Cal Refresh] Unable to refresh token. Check logs above`);
return new Response(JSON.stringify({ data: "Internal Server Error" }), { status: 500 });
}
const updatedDb = await db.user.update({
where: { id: user.id },
data: {
calAccessToken: refreshFlow.data.accessToken,
calRefreshToken: refreshFlow.data.refreshToken,
// TODO: uncomment this once the endpoint returns expiration as well
// calAccessTokenExpiresAt: body.data.accessTokenExpiresAt,
},
});
if (!updatedDb.calAccessToken || !updatedDb.calRefreshToken) {
throw new Error(`[Cal Refresh] Unable to update user with id ${user.id} with the new tokens.`);
}
/** [@calcom] You have to return the accessToken back to calcom/atoms api for future refresh requests. */
return new Response(
JSON.stringify({
accessToken: updatedDb.calAccessToken,
refreshToken: updatedDb.calRefreshToken,
} satisfies KeysResponseDto["data"]),
{
status: 200,
}
);
} catch (e) {
console.error(e);
return new Response(JSON.stringify({ data: "Internal Server Error" }), { status: 500 });
}
});
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/api/supabase/storage/route.ts
================================================
import { auth } from "@/auth";
import { env } from "@/env";
import { createClient } from "@supabase/supabase-js";
export const dynamic = "force-dynamic"; // defaults to auto
export async function GET(request: Request) {
try {
const session = await auth();
if (!session?.user.id) {
return new Response("Unauthorized", { status: 401 });
}
const {
user: { id },
} = session;
// Generate signed upload url to use on client.
const supabaseAdmin = createClient(env.NEXT_PUBLIC_SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
const { data, error } = await supabaseAdmin.storage
.from("avatars")
.createSignedUploadUrl(id, { upsert: true });
console.log(error);
if (error) throw error;
return new Response(JSON.stringify(data), {
status: 200,
});
} catch (e) {
console.error(e);
return new Response("Internal Server Error", { status: 500 });
}
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/[...dashboardSegments]/page.tsx
================================================
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import Link from "next/link";
import { Fragment } from "react";
export default function BreadcrumbsSlot(props: {
params: {
dashboardSegments: string[];
};
}) {
const { dashboardSegments } = props.params;
// the last section is always our "BreadcrumbPage", the remaining segments are our "BreadcrumbItems":
const breadcrumbPage = dashboardSegments.pop();
return (
{dashboardSegments.map((segment, idx) => {
const parentSegments = dashboardSegments.slice(0, idx);
const parentPath = parentSegments.length > 0 ? `/${parentSegments.join("/")}` : "";
const href = `${parentPath}/${segment}`;
return (
{idx > 0 && }
{segment}
);
})}
{breadcrumbPage}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/page.tsx
================================================
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from "@/components/ui/breadcrumb";
// Note: The root breadcrum is required since optional catch-all routes aren't supported yet by Nextjs parallel routes
export default function BreadcrumbsSlot() {
return (
Dashboard
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/[...dashboardSegments]/page.tsx
================================================
import { dashboardNavigationData } from "../../data";
import { cn } from "@/lib/utils";
import Link from "next/link";
export default function DashboardNavigationDesktopSlot(props: {
params: {
dashboardSegments: string[];
};
}) {
const { dashboardSegments } = props.params;
const pathname = `/${dashboardSegments.join("/")}`;
return (
{dashboardNavigationData.map((navigationItem) => {
return (
{navigationItem.label}
);
})}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/page.tsx
================================================
import { dashboardNavigationData } from "../data";
import { cn } from "@/lib/utils";
import Link from "next/link";
// Note: The root breadcrum is required since optional catch-all routes aren't supported yet by Nextjs parallel routes
export default function DashboardNavigationDesktopDefault() {
const pathname = "/dashboard";
return (
{dashboardNavigationData.map((navigationItem) => {
return (
{navigationItem.label}
);
})}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/[...dashboardSegments]/page.tsx
================================================
import { dashboardNavigationData } from "../../data";
import { Logo } from "@/app/_components/universal/logo";
import { cn } from "@/lib/utils";
import Link from "next/link";
export default function DashboardNavigationMobileSlot(props: {
params: {
dashboardSegments: string[];
};
}) {
const { dashboardSegments } = props.params;
const pathname = `/${dashboardSegments.join("/")}`;
return (
{dashboardNavigationData.map((navigationItem) => {
return (
{navigationItem.label}
);
})}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/page.tsx
================================================
import { dashboardNavigationData } from "../data";
import { Logo } from "@/app/_components/universal/logo";
import { cn } from "@/lib/utils";
import Link from "next/link";
// Note: The root breadcrum is required since optional catch-all routes aren't supported yet by Nextjs parallel routes
export default function DashboardNavigationMobileDefault() {
const pathname = "/dashboard";
return (
{dashboardNavigationData.map((navigationItem) => {
return (
{navigationItem.label}
);
})}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/bookings-table.tsx
================================================
"use client";
import { type GetBookingsDataEntry } from "@/cal/__generated/cal-sdk";
import { stripCalOAuthClientIdFromEmail, stripCalOAuthClientIdFromText } from "@/cal/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { type User, type CalAccount } from "@prisma/client";
import { Separator } from "@radix-ui/react-separator";
import dayjs from "dayjs";
import { ChevronLeft, ChevronRight, Copy } from "lucide-react";
import { type BookingStatus } from "node_modules/@calcom/atoms/dist/packages/prisma-client";
import { Fragment, useState } from "react";
export const BookingsTable = (props: {
bookings: {
all: Array;
currentWeek: Array;
currentMonth: Array;
currentYear: Array;
};
user: { timeZone: CalAccount["timeZone"]; username: User["username"]; email: User["email"] };
}) => {
// send this ref to: rable-row-hoverable element
// then use it in order-details element to read the currently hovered event
const [selectedElement, setSelectedElement] = useState(
props.bookings?.currentWeek?.[0] ?? null
);
const [selectedTab, setSelectedTab] = useState<"week" | "month" | "year">("week");
const tabs = [
{ label: "Week", value: "week" },
{ label: "Month", value: "month" },
{ label: "Year", value: "year" },
] as const;
// to display the booking detail's attendees
const who = {
host: `${selectedElement?.user?.name} (Host) - ${stripCalOAuthClientIdFromEmail(props.user.email ?? "")}`,
attendees: selectedElement?.attendees.map((attendee) => {
return {
name: stripCalOAuthClientIdFromText(attendee?.name ?? ""),
email: stripCalOAuthClientIdFromEmail(attendee?.email ?? ""),
};
}),
};
const bookings =
selectedTab === "week"
? props.bookings.currentWeek
: selectedTab === "month"
? props.bookings.currentMonth
: props.bookings.currentYear;
return (
setSelectedTab(val as "week" | "month" | "year")}>
{tabs.map((tab, idx) => (
{tab.label}
))}
{tabs.map((tab, idx) => {
return (
Bookings
Bookings from potential customers.
Initiator
Event Type
Status
Date
Time
{bookings.length ? (
bookings.map((booking, idx) => {
const initiator = booking.attendees[0];
const isEven = idx % 2 === 0;
return (
booking.id === selectedElement?.id) === idx
}
onClick={() => setSelectedElement(booking)}>
{stripCalOAuthClientIdFromText(initiator?.name ?? "")}
{stripCalOAuthClientIdFromEmail(initiator?.email ?? "")}
{booking.eventType.slug}
).includes(
// @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus
booking.status
)
? "destructive"
: (["PENDING", "ACCEPTED", "AWAITING HOST"] as Array)
// @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus
.includes(booking.status)
? "success"
: "default"
}
className={cn(
"text-xs",
// so that we show a gray badge for pending meetings
(booking.status as BookingStatus) === "PENDING" &&
"border-transparent bg-muted text-muted-foreground hover:bg-muted/80"
)}>
{/* @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus */}
{booking.status}
{dayjs(booking.startTime).format("YYYY-MM-DD")}
{dayjs(booking.startTime).format("h:mma")}
{props.user.timeZone}
);
})
) : (
In the current {tab.label.toLocaleLowerCase()}, you don’t have any
bookings.
)}
);
})}
{stripCalOAuthClientIdFromText(selectedElement?.title ?? "No booking selected")}
Copy Booking ID
Booking Details
Booking Uid:
{selectedElement?.uid}
Date:
{selectedElement?.startTime
? dayjs(selectedElement?.startTime).format("MMMM DD, YYYY")
: "MMMM DD, YYYY"}
Status:
).includes(
// @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus
selectedElement?.status
)
? "destructive"
: (["PENDING", "ACCEPTED", "AWAITING HOST"] as Array)
// @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus
.includes(selectedElement?.status)
? "success"
: "default"
}
className={cn(
"w-fit text-xs",
// so that we show a gray badge for pending meetings
(selectedElement?.status as BookingStatus) === "PENDING" &&
"border-transparent bg-muted text-muted-foreground hover:bg-muted/80"
)}>
{/* @ts-expect-error: There are missing types in the openapi specs for cal's api, this should likely be: BookingStatus */}
{selectedElement?.status}
Attendees
{who.attendees?.map((attendee, idx) => (
{attendee.name}
{attendee.email}
))}
Customer Information
Customer
{stripCalOAuthClientIdFromText(selectedElement?.attendees[0]?.name ?? "")}
Language
{selectedElement?.attendees[0]?.locale
? new Intl.DisplayNames([navigator.language], { type: "language" }).of(
selectedElement?.attendees[0]?.locale ?? "English"
)
: ""}{" "}
{selectedElement?.attendees[0]?.locale
? `(${selectedElement?.attendees[0]?.locale})`
: ""}
Updated Today
setSelectedElement((prev) => {
if (!prev) return bookings[0] ?? null;
const currentIndex = bookings?.findIndex((booking) => booking.id === prev.id);
return bookings[currentIndex - 1];
})
}
disabled={
bookings.findIndex((booking) => booking.id === selectedElement?.id) === 0 ||
bookings.length < 2
}>
Previous Booking
setSelectedElement((prev) => {
if (!prev) return bookings[0] ?? null;
const currentIndex = bookings.findIndex((booking) => booking.id === prev?.id);
return bookings[currentIndex + 1];
})
}
disabled={
bookings.findIndex((booking) => booking.id === selectedElement?.id) ===
bookings.length - 1 || bookings.length < 2
}>
Next Booking
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/connect-calendar-step.tsx
================================================
import { ButtonSubmit } from "@/app/_components/submit-button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useStepper } from "@/components/ui/stepper";
import { GcalConnect } from "@calcom/atoms";
import { Loader } from "lucide-react";
import { Suspense } from "react";
const ConnectCalendarStep = () => {
const { nextStep } = useStepper();
return (
<>
Getting Started
Connect your calendar to get started.
}>
Next
>
);
};
export default ConnectCalendarStep;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/getting-started-steps.tsx
================================================
"use client";
import ConnectCalendarStep from "./connect-calendar-step";
import UserFilters from "./user-filters-step";
import UserDetailsStep from "./user-details-step";
import { Step, Stepper, type StepItem } from "@/components/ui/stepper";
import { type FilterOption } from "@prisma/client";
const steps = [
{ id: "connect-calendar", label: "Step 1" },
{ id: "filters", label: "Step 2" },
{ id: "avatar-and-bio", label: "Step 3" },
] satisfies StepItem[];
const GettingStarted = ({
userId,
filterOptions,
}: {
userId: string;
filterOptions: Array;
}) => {
return (
{steps.map(({ id, label }, index) => {
return (
{index === 0 && (
)}
{index === 1 && }
{index === 2 && }
);
})}
);
};
export default GettingStarted;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-details-step.tsx
================================================
import SupabaseReactDropzone from "../settings/_components/supabase-react-dropzone";
import { expertEdit } from "@/app/_actions";
import { ButtonSubmit } from "@/app/_components/submit-button";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useStepper } from "@/components/ui/stepper";
import { Textarea } from "@/components/ui/textarea";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useFormState } from "react-dom";
type UserDetailsFormState = { error: null | string } | { success: null | string };
const UserDetailsStep = ({ userId }: { userId: string }) => {
const router = useRouter();
const [userDetailsFormState, dispatch] = useFormState(expertEdit, {
error: null,
});
const { isDisabledStep, prevStep } = useStepper();
useEffect(() => {
if ("success" in userDetailsFormState && userDetailsFormState?.success) {
router.push("/dashboard");
}
}, [userDetailsFormState]);
return (
);
};
export default UserDetailsStep;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-filters-step.tsx
================================================
import { addUserFilters } from "@/app/_actions";
import { FancyMultiSelect, type Option } from "@/app/_components/multi-select";
import { ButtonSubmit } from "@/app/_components/submit-button";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useStepper } from "@/components/ui/stepper";
import { type FilterOption } from "@prisma/client";
import { useEffect } from "react";
import { useFormState } from "react-dom";
import { uniqueBy, prop } from "remeda";
type TUserFiltersFormState = {
error?: string | null;
inputErrors?: {
categories?: string[];
capabilities?: string[];
frameworks?: string[];
budgets?: string[];
languages?: string[];
regions?: string[];
};
};
const UserFilters = ({ filterOptions }: { filterOptions: Array }) => {
const [formState, dispatch] = useFormState(addUserFilters, {
error: null,
});
const { isDisabledStep, prevStep, nextStep } = useStepper();
const filtersByCategory = uniqueBy(filterOptions, prop("filterCategoryFieldId"));
useEffect(() => {
if ("success" in formState && !!formState?.success) {
nextStep();
}
}, [formState]);
return (
{filtersByCategory.map(({ filterCategoryFieldId, filterCategoryLabel }) => (
{filterCategoryLabel}
filterOption.filterCategoryLabel === filterCategoryLabel)
.map(
(filterOption) =>
({
label: filterOption.fieldLabel,
value: filterOption.fieldValue,
}) satisfies Option
)}
placeholder={`Select your ${filterCategoryLabel.toLowerCase()}`}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
name={filterCategoryFieldId.toLowerCase()}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
id={filterCategoryFieldId.toLowerCase()}
/>
{formState?.inputErrors?.[filterCategoryFieldId as keyof TUserFiltersFormState["inputErrors"]] ? (
{
formState.inputErrors?.[
filterCategoryFieldId as keyof TUserFiltersFormState["inputErrors"]
]?.[0]
}
) : null}
))}
Prev
Next
);
};
export default UserFilters;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/data.tsx
================================================
import { Calendar, Clock, Home, User } from "lucide-react";
export const dashboardNavigationData = [
{
label: "Dashboard",
href: "/dashboard",
icon: (props: { className?: string }) => ,
},
{
label: "Booking Events",
href: "/dashboard/settings/booking-events",
icon: (props: { className?: string }) => ,
},
{
label: "Profile",
href: "/dashboard/settings/profile",
icon: (props: { className?: string }) => ,
},
{
label: "Availability",
href: "/dashboard/settings/availability",
icon: (props: { className?: string }) => ,
},
];
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/getting-started/page.tsx
================================================
import GettingStarted from "../_components/getting-started-steps";
import { auth } from "@/auth";
import { db } from "prisma/client";
export default async function Dashboard() {
const sesh = await auth();
const filterOptions = await db.filterOption.findMany();
if (!sesh?.user?.id) {
return Not logged in
;
}
return ;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/layout.tsx
================================================
import { ButtonSubmit } from "@/app/_components/submit-button";
import { Logo } from "@/app/_components/universal/logo";
import { SignedIn, signOut } from "@/auth";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { PanelLeft, User } from "lucide-react";
import Link from "next/link";
import { type ReactNode } from "react";
export default async function Layout({
children,
breadcrumbs,
dashboardNavigationDesktop,
dashboardNavigationMobile,
}: {
children: ReactNode;
breadcrumbs: ReactNode;
dashboardNavigationDesktop: ReactNode;
dashboardNavigationMobile: ReactNode;
}) {
return (
{({ user }) => (
{dashboardNavigationDesktop}
Toggle Menu
{dashboardNavigationMobile}
{breadcrumbs}
Logged in as {user?.username}
My Account
Settings
{
"use server";
await signOut({ redirectTo: "/" });
}}>
Logout
{children}
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/page.tsx
================================================
/* eslint-disable */
// @ts-nocheck
import { BookingsTable } from "./_components/bookings-table";
import { CalAccount, auth } from "@/auth";
import { cal } from "@/cal/api";
import { stripCalOAuthClientIdFromEmail } from "@/cal/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
import dayjs from "dayjs";
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import { type GetBookingsInput } from "node_modules/@calcom/atoms/dist/packages/platform/types";
import { Suspense } from "react";
export default async function Dashboard() {
const sesh = await auth();
if (!sesh.user.id) {
return Not logged in
;
}
/** [@calcom] We're fetching the bookings on the server to display them here
* Since `filters` is currently a required parameter, we have to iterate a bit and create our flatMap in the end
*/
const filters = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] satisfies Array<
GetBookingsInput["filters"]["status"]
>;
const bookingResponses = await Promise.all(
filters.map((filter) =>
cal({ user: { id: sesh?.user.id } }).get("/v2/bookings", {
query: { "filters[status]": filter, cursor: 0, limit: 20 },
})
)
);
const bookings = bookingResponses.flatMap((response, idx) => {
if (response.status === "error") {
console.warn(
`Unable to fetch bookings for filter '${filters[idx]}' with status '${response.status}'`,
response
);
return [];
}
return response.data.bookings;
});
/** [@calcom] End of fetching bookings */
const lastWeekBookings = bookings.filter((booking) => {
const startOfWeek = dayjs().startOf("week").subtract(1, "week");
const endOfWeek = dayjs().endOf("week").subtract(1, "week");
return dayjs(booking.startTime).isAfter(startOfWeek) && dayjs(booking.startTime).isBefore(endOfWeek);
});
const thisWeekBookings = bookings.filter((booking) => {
const startOfWeek = dayjs().startOf("week");
const endOfWeek = dayjs().endOf("week");
return dayjs(booking.startTime).isAfter(startOfWeek) && dayjs(booking.startTime).isBefore(endOfWeek);
});
const lastMonthBookings = bookings.filter((booking) => {
const startOfMonth = dayjs().startOf("month").subtract(1, "month");
const endOfMonth = dayjs().endOf("month").subtract(1, "month");
return dayjs(booking.startTime).isAfter(startOfMonth) && dayjs(booking.startTime).isBefore(endOfMonth);
});
const thisMonthBookings = bookings.filter((booking) => {
const startOfMonth = dayjs().startOf("month");
const endOfMonth = dayjs().endOf("month");
return dayjs(booking.startTime).isAfter(startOfMonth) && dayjs(booking.startTime).isBefore(endOfMonth);
});
const thisYearBookings = bookings.filter((booking) => {
// only show the bookings with booking.startTime for the current year:
const startOfYear = dayjs().startOf("year");
const endOfYear = dayjs().endOf("year");
return dayjs(booking.startTime).isAfter(startOfYear) && dayjs(booking.startTime).isBefore(endOfYear);
});
const changeFromPrevious = (current: number, previous: number) => {
return previous === 0 ? 100 : Math.round(((current - previous) / previous) * 100);
};
return (
Your Bookings
See all your bookings for your services.
Manage booking events
This Week
{thisWeekBookings.length}
{lastWeekBookings.length > 0
? `${changeFromPrevious(thisWeekBookings.length, lastWeekBookings.length)}% from last week`
: "From 0 last week"}
0
? `${changeFromPrevious(thisWeekBookings.length, lastWeekBookings.length)}% from last week`
: "Unknown percentage from last week (no bookings)"
}
className={cn(
Math.round(
((thisWeekBookings.length - lastWeekBookings.length) / lastWeekBookings.length) * 100
) < 0
? "[&>div]:bg-destructive/80"
: "[&>div]:bg-success"
)}
/>
This Month
{thisMonthBookings.length}
{lastMonthBookings.length > 0
? `${changeFromPrevious(thisMonthBookings.length, lastMonthBookings.length)}% from last week`
: "From 0 last month"}
0
? `${changeFromPrevious(thisMonthBookings.length, lastMonthBookings.length)}% from last week`
: "Unknown percentage from last month (no bookings)"
}
className={cn(
Math.round(
((thisMonthBookings.length - lastMonthBookings.length) / lastMonthBookings.length) * 100
) < 0
? "[&>div]:bg-destructive/80"
: "[&>div]:bg-success"
)}
/>
{(calAccount) => (
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/expert-edit.tsx
================================================
"use client";
import { expertEdit } from "@/app/_actions";
import { ButtonSubmit } from "@/app/_components/submit-button";
import { CardDescription } from "@/components/ui/card";
import { Input, type InputProps } from "@/components/ui/input";
import { Textarea, type TextareaProps } from "@/components/ui/textarea";
import { useActionState } from "react";
export default function ExpertEditForm(props: InputProps | TextareaProps) {
const [state, submitAction, isPendingAction] = useActionState<
{ error: null | string } | { success: null | string },
FormData
>(expertEdit, { error: null }, "/dashboard/settings/profile");
return (
{props.name === "bio" ? (
) : (
)}
{/* display action states (pending, idle, success & error) */}
{isPendingAction ? (
Saving...
) : "success" in state && state.success ? (
{state.success}
) : "error" in state && state.error ? (
{state.error}
) : (
Provide a new {props.name} and hit save to reflect the changes on your public page.
)}
Save
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/settings-content.tsx
================================================
"use client";
import { AvailabilitySettings } from "@calcom/atoms";
/**
* [@calcom] Make sure to wrap your app with our `CalProvider` to enable the use of our hooks.
* @link https://cal.com/docs/platform/quick-start#5.3-setup-root-of-your-app
*/
export const SettingsContent = () => {
return (
{
console.log("[@calcom/atoms]: Updated successfully");
}}
onUpdateError={() => {
console.log("[@calcom/atoms]: Update error");
}}
onDeleteError={() => {
console.log("[@calcom/atoms]: Deletion error");
}}
onDeleteSuccess={() => {
console.log("[@calcom/atoms]: Deleted successfully");
}}
/>
);
};
export default SettingsContent;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/supabase-react-dropzone.tsx
================================================
"use client";
import { Skeleton } from "@/components/ui/skeleton";
import { env } from "@/env";
import slugify, { cn } from "@/lib/utils";
import { createClient } from "@supabase/supabase-js";
import Image from "next/image";
import type StorageFileApi from "node_modules/.pnpm/@supabase+storage-js@2.6.0/node_modules/@supabase/storage-js/src/packages/StorageFileApi";
import React, { useState, useEffect } from "react";
import { useDropzone } from "react-dropzone";
type SupabaseStorage = (typeof StorageFileApi)["prototype"];
type FileBody = Parameters[2];
export default function SupabaseReactDropzone({ userId }: { userId: string; userInitials?: string }) {
const [status, setStatus] = useState<"idle" | "loading" | "error" | "success">("idle");
const supabaseBrowserClient = createClient(env.NEXT_PUBLIC_SUPABASE_URL, env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
const [avatar, setAvatar] = useState(`avatars/${userId}`);
useEffect(() => {
setStatus("idle");
}, [avatar]);
const { acceptedFiles, fileRejections, getRootProps, getInputProps } = useDropzone({
maxFiles: 1,
accept: {
"image/jpeg": [],
"image/png": [],
// avif as well:
"image/avif": [],
},
onDropAccepted: async (acceptedFiles) => {
setAvatar(null);
const { path, token }: { path: string; token: string } = await fetch("/api/supabase/storage").then(
(res) => res.json()
);
const { data, error } = await supabaseBrowserClient.storage
.from("avatars")
.uploadToSignedUrl(path, token, acceptedFiles[0] as FileBody);
if (typeof data?.fullPath === "string") {
// @ts-expect-error acceptedFiles isn't typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setAvatar(`${data?.fullPath}?filename=${encodeURIComponent(slugify(acceptedFiles[0].path))}`);
}
},
});
// const acceptedFileItems = acceptedFiles.map((file) => (
//
// {file.path} - {file.size} bytes
//
// ));
// const fileRejectionItems = fileRejections.map(({ file, errors }) => (
//
// {file.path} - {file.size} bytes
//
// {errors.map((e) => (
// {e.message}
// ))}
//
//
// ));
return (
{status === "error" ? (
) : status === "loading" || !avatar ? (
) : (
setStatus("success")}
onLoad={() => setStatus("loading")}
onError={() => setStatus("error")}
/>
)}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/availability/page.tsx
================================================
import SettingsContent from "../_components/settings-content";
export default async function DashboardSettingsAvailability() {
return ;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/_actions.ts
================================================
"use server";
import { auth } from "@/auth";
import { post_EventTypesController_createEventType } from "@/cal/__generated/cal-sdk";
import { cal } from "@/cal/api";
import { revalidatePath } from "next/cache";
import { type z } from "zod";
export async function createEventType(
_prevState: { error: null | string } | { success: null | string },
formData: FormData
) {
const sesh = await auth();
if (!sesh?.user.id) {
console.log("[_actions] Unauthorized user edit", formData);
return { error: "Unauthorized" };
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const updateEventTypeBodyData = Object.fromEntries(
Array.from(formData.entries())
.filter(([key]) => !key.toLowerCase().startsWith("$action"))
.map(([key, value]) => {
if (key === "length") return [key, Number(value)];
return [key, value];
})
);
const updateEventTypeParameters = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
body: updateEventTypeBodyData,
} satisfies z.infer;
const input = post_EventTypesController_createEventType.parameters.safeParse(updateEventTypeParameters);
if (!input.success) {
console.log("[_actions] Invalid form data", formData);
return { error: "Invalid form data" };
}
const res = await cal({ user: { id: sesh?.user.id } }).post("/v2/event-types", {
body: input.data.body,
});
if (res.status === "error") {
console.error(
`[_actions] Error creating event type for user with id '${sesh.user.id}'. Bad response from Cal Platform API
-- REQUEST DETAILS --
Endpoint URL: POST /v2/event-types
Options: ${JSON.stringify(input.data.body)}
-- RESPONSE DETAILS --
responseStatus: ${JSON.stringify(res.status)}
responseData: ${JSON.stringify(res.data)}
`
);
return { error: "Unable to create the booking event (something went wrong)." };
}
const permalink = String(formData.get("permalink"));
permalink && revalidatePath(permalink);
return { success: `Event type '${res.data.title}' created successfully.` };
}
export async function deleteEventType(
_prevState: { error: null | string } | { success: null | string },
eventTypeId: number
) {
const sesh = await auth();
if (!sesh?.user.id) {
console.error("[_actions] Unauthorized user delete");
return { error: "Unauthorized" };
}
const res = await cal({ user: { id: sesh?.user.id } }).delete(`/v2/event-types/{eventTypeId}`, {
path: { eventTypeId },
});
if (res.status === "error") {
console.error(
`[_actions] Error deleting event type for user with id '${sesh.user.id}'. Bad response from Cal Platform API
-- REQUEST DETAILS --
Endpoint URL: DELETE /v2/event-types/{eventTypeId}
-- RESPONSE DETAILS --
responseStatus: ${JSON.stringify(res.status)}
responseData: ${JSON.stringify(res.data)}
`
);
return { error: "Unable to delete the booking event (something went wrong)." };
}
return { success: `Event type '${res.data.title}' created successfully.` };
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-create.tsx
================================================
"use client";
import { createEventType } from "./_actions";
import { DialogDescription } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { useActionState } from "react";
export default function EventTypeCreateForm({
children,
className,
...props
}: {
children?: React.ReactNode;
className?: string;
} & { permalink?: NonNullable["2"]> }) {
const [state, submitAction, isPendingAction] = useActionState<
{ error: string | null } | { success: string | null },
FormData
>(createEventType, { error: null }, props.permalink);
return (
{isPendingAction ? (
Saving...
) : "success" in state && state.success ? (
{state.success} You can close the dialog now.
) : "error" in state && state.error ? (
{state.error.replace("'", "’")}
) : (
Create your new booking event here. Click save when you’re done.
)}
{children}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-delete.tsx
================================================
"use client";
import { deleteEventType } from "./_actions";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useRouter } from "next/navigation";
import { useActionState } from "react";
export function EventTypeDelete({ eventTypeId }: { eventTypeId: number }) {
const router = useRouter();
const [_, submitAction, isPendingAction] = useActionState<
{ error: string | null } | { success: string | null },
number
>(deleteEventType, { error: null });
const handleDelete = async () => {
submitAction(eventTypeId);
router.refresh();
};
return (
{isPendingAction ? "Deleting..." : "Delete"}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/page.tsx
================================================
import EventTypeCreateForm from "./event-type-create";
import { EventTypeDelete } from "./event-type-delete";
import { ButtonSubmit } from "@/app/_components/submit-button";
import { auth } from "@/auth";
import { cal } from "@/cal/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { MoreHorizontal, PlusCircle, Video } from "lucide-react";
import { Fragment } from "react";
export default async function DashboardSettingsBookingEvents() {
const sesh = await auth();
if (!sesh?.user.id) {
return Not logged in
;
}
const getEventTypes = await cal({ user: { id: sesh?.user.id } }).get("/v2/event-types");
if (getEventTypes.status === "error") {
console.error("[dashboard/settings/booking-events/page.tsx] Error fetching event types", getEventTypes);
// TODO debug this error
console.warn(`[dashboard/settings/booking-events/page.tsx] Error fetching event types. Check logs above`);
}
const eventTypes = getEventTypes?.data?.eventTypeGroups?.flatMap((group) => group.eventTypes) ?? [
{
length: 60,
slug: "60min",
title: "60min",
description: "A 60 minute session",
locations: [
{
type: "location",
link: "https://cal.com/locations/1",
},
],
id: 1,
},
{
length: 30,
slug: "30min",
title: "30min",
description: "A 30 minute session",
locations: [
{
type: "location",
link: "https://cal.com/locations/1",
},
],
id: 2,
},
];
return (
{/* TODO: add filter logic via url params */}
{/*
Filter
Filter by
Active
Draft
Archived
*/}
Add Event Type
Create a new Booking Event
{(
[
{
name: "length",
label: "Duration",
type: "number",
min: "15",
step: "15",
max: "300",
required: true,
},
{
name: "slug",
label: "URL Slug",
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
required: true,
},
{
name: "title",
label: "Title",
type: "text",
minlength: "3",
maxlength: "30",
required: true,
},
{
name: "description",
label: "Description",
type: "text",
minlength: "3",
maxlength: "300",
},
] as const
).map(({ name, label, ...inputAttributes }) => (
{label}
{name === "description" ? (
) : (
)}
))}
Save
Event Types
Manage your event type and view their sales performance.
Name
Description
Locations
Duration (min)
Actions
{eventTypes.map((eventType) => (
{eventType.title}
/{eventType.slug}
{eventType.description}
{eventType.locations?.map((location, idx) => (
{location.type === "integrations:daily" && (
Cal Video
)}
))}
{eventType.length}
Toggle menu
Actions
))}
Showing{" "}
{eventTypes.length > 0 ? 1 : 0}-{eventTypes.length > 10 ? 10 : eventTypes.length}
{" "}
of {eventTypes.length} event types
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/layout.tsx
================================================
export default function SettingsLayout(props: { children: React.ReactNode }) {
return (
{props.children}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/page.tsx
================================================
export default function SettingsOutlet() {
return null;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/profile/page.tsx
================================================
import ExpertEditForm from "../_components/expert-edit";
import SupabaseReactDropzone from "../_components/supabase-react-dropzone";
import { currentUser } from "@/auth";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Info } from "lucide-react";
export const dynamic = "force-dynamic";
export default async function DashboardSettingsProfile() {
const expert = await currentUser();
if (!expert) {
return Not logged in
;
}
return (
Image
Used on your public profile, once it is approved.
The Image upload auto-saves.
Name
Used on your public profile, once it is approved.
Bio
A couple of sentences about yourself. This will be displayed on your public profile.
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/layout.tsx
================================================
import Banner from "./_components/banner";
import UseCalAtoms from "./_components/use-cal";
import { Providers } from "./providers";
import { TailwindIndicator } from "./tailwind-indicator";
import { currentUser } from "@/auth";
import { cn } from "@/lib/utils";
import "@/styles/globals.css";
import "@/styles/globals.css";
/**
* [@calcom] In your root layout, make sure you import the atoms' global styles so that you get our shiny styles
* @link https://cal.com/docs/platform/quick-start#5.3-setup-root-of-your-app
*/
import "@calcom/atoms/globals.min.css";
import { Analytics } from "@vercel/analytics/react";
import { type Metadata } from "next";
import { AxiomWebVitals } from "next-axiom";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import { Toaster } from "sonner";
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "block",
weight: "600",
});
export const metadata: Metadata = {
title: {
default: "Cal.com Platform: Showcase App",
template: `Cal.com Platform | %s`,
},
description: "Cal.com Platform example app: Showcase usage of the 'Cal Atoms' React Components",
keywords: [
"cal.com",
"platform",
"example",
"app",
"scheduling software",
"scheduling components",
"scheduling react",
],
authors: [
{
name: "Richard Poelderl",
url: "https://x.com/richardpoelderl",
},
{ name: "Peer Richelsen", url: "https://x.com/peerrich" },
],
creator: "Cal.com",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
/** [@calcom] Ensure to set the diretion (either 'ltr' or 'rtl') since the calcom/atoms use their styles */
dbUser?.calAccessToken ?? null) ?? null}>
{children}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/login/_components/input.tsx
================================================
import { Input, type InputProps } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { type ReactNode } from "react";
export const AddonFieldPrefix = (props: { children?: ReactNode; prefix: string }) => {
return (
{props.prefix}
{props.children}
);
};
export const AddonFieldInput = (props: { className?: string } & InputProps) => {
const { className } = props;
return (
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/login/_components/login.tsx
================================================
"use client";
import { signInWithCredentials } from "@/app/_actions";
import { ButtonSubmit } from "@/app/_components/submit-button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { useFormState } from "react-dom";
export type LoginFormState =
| {
inputErrors: {
email?: string[] | undefined;
password?: string[] | undefined;
};
error?: undefined;
}
| {
error: null;
inputErrors?: undefined;
}
| {
error: string;
inputErrors?: undefined;
};
export function LoginForm() {
const [formState, dispatch] = useFormState(signInWithCredentials, {
error: null,
});
return (
Login
Enter your email below to login in to your account.
Email
{formState?.inputErrors?.email ? (
{formState.inputErrors.email[0]}
) : null}
Password
{formState?.inputErrors?.password ? (
{formState.inputErrors.password[0]}
) : null}
{!!formState?.error && {formState.error}
}
Log in
Don't have an account?{" "}
Sign up
{" "}
instead.
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/login/layout.tsx
================================================
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Home, Package2, PanelLeft } from "lucide-react";
import Link from "next/link";
import { type ReactNode } from "react";
export default function LoginLayout({ children }: { children?: ReactNode }) {
return (
<>
Toggle Menu
Home
Login
Home
Login
{children}
>
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/login/page.tsx
================================================
import { LoginForm } from "@/app/login/_components/login";
export default function LoginPage() {
return ;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/page.tsx
================================================
import { Results } from "./_components/home/results";
import SignupCard from "./_components/home/signup-card";
import { ButtonSubmit } from "./_components/submit-button";
import { Logo } from "./_components/universal/logo";
import { SignedIn, SignedOut, signOut } from "@/auth";
import { Button } from "@/components/ui/button";
import { LogIn } from "lucide-react";
import Link from "next/link";
import { db } from "prisma/client";
import React, { Suspense } from "react";
export default async function Home() {
const experts = await db.user.findMany({
where: { status: "APPROVED" },
include: { selectedFilterOptions: { include: { filterOption: true } } },
});
return (
{/*
Tip: Use this for your own navigation
*/}
{(_user) => (
{
"use server";
await signOut({ redirectTo: "/" });
}}>
Logout
Dashboard
)}
Login
Sign Up
}
/>
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/providers.tsx
================================================
"use client";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
{children}
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/_components/input.tsx
================================================
import { Input, type InputProps } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { type ReactNode } from "react";
export const AddonFieldPrefix = (props: { children?: ReactNode; prefix: string }) => {
return (
{props.prefix}
{props.children}
);
};
export const AddonFieldInput = (props: { className?: string } & InputProps) => {
const { className } = props;
return (
);
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/_components/signup.tsx
================================================
"use client";
import { signUpWithCredentials } from "@/app/_actions";
import { AddonFieldInput, AddonFieldPrefix } from "@/app/signup/_components/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import Link from "next/link";
import { useFormState } from "react-dom";
type TSignUpFormState = {
error?: string | null;
inputErrors?: {
name?: string[];
username?: string[];
email?: string[];
password?: string[];
bio?: string[];
categories?: string[];
capabilities?: string[];
frameworks?: string[];
budgets?: string[];
languages?: string[];
regions?: string[];
};
};
export const SignupForm = () => {
const [formState, dispatch] = useFormState(signUpWithCredentials, {
error: null,
});
return (
Sign Up
Enter your information to create an account
Name
{formState?.inputErrors?.name ? (
{formState.inputErrors.name[0]}
) : null}
Username
{formState?.inputErrors?.username ? (
{formState.inputErrors.username[0]}
) : null}
Email
{formState?.inputErrors?.email ? (
{formState.inputErrors.email[0]}
) : null}
Password
{formState?.inputErrors?.password ? (
{formState.inputErrors.password[0]}
) : null}
{formState?.error ? (
{formState.error}
) : null}
Create an account
Already have an account?{" "}
Log in
);
};
export default SignupForm;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/layout.tsx
================================================
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Home, Package2, PanelLeft } from "lucide-react";
import Link from "next/link";
import { type ReactNode } from "react";
export default function SignupLayout({ children }: { children?: ReactNode }) {
return (
<>
Toggle Menu
Home
Signup
Home
Signup
{children}
>
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/page.tsx
================================================
import { SignupForm } from "@/app/signup/_components/signup";
export default async function SignupPage() {
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/tailwind-indicator.tsx
================================================
import { IS_PRODUCTION } from "@/lib/constants";
export function TailwindIndicator() {
if (IS_PRODUCTION) return null;
return (
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/auth/config.edge.ts
================================================
import { signUp } from "@/cal/auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { type User } from "@prisma/client";
import { type NextAuthConfig, type DefaultSession } from "next-auth";
import { type DefaultJWT } from "next-auth/jwt";
import { db } from "prisma/client";
import "server-only";
import { z } from "zod";
declare module "next-auth" {
interface Session {
user: DefaultSession["user"] & {
username: string;
};
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
username?: string;
}
}
const SessionUpdateSchema = z.object({
user: z.object({
// default fields
name: z.string().optional(),
email: z.string().optional(),
picture: z.string().optional(),
// augmented fields
username: z.string().optional(),
}),
});
export const authConfig = {
logger: {
debug: (message, metadata) => console.debug(message, { metadata }),
error: (error) => console.error(error),
warn: (message) => console.warn(message),
},
adapter: PrismaAdapter(db),
session: { strategy: "jwt" },
pages: { signIn: "/login" },
callbacks: {
signIn: async ({ user }) => {
if (user.id) {
return true;
}
return false;
},
jwt: async ({ token, user, trigger, session, account }) => {
if (user) {
// update the token with the user's data
token.sub = user.id;
token.email = user.email;
token.username = (user as User).username ?? undefined;
token.name = user.name;
}
let dbUser: User | null = null;
// if this is an update, let's update the token with the provided user data
if (trigger === "update") {
const updateSessionValidation = SessionUpdateSchema.parse(session);
const keysToUpdate = Object.keys(updateSessionValidation.user) as Array<
keyof z.infer
>;
for (const key of keysToUpdate) {
console.info(
`
[NextAuth.callbacks.jwt] Update user's token (userId: '${token.sub}') with key '${key}' to the value: ${updateSessionValidation.user[key]}
The previous value was: ${String(token?.[key])}
`
);
token[key] = updateSessionValidation.user[key];
}
dbUser = await db.user.update({
where: { id: token.sub },
data: {
name: token.name,
email: token.email,
username: token.username,
// picture: token.picture,
},
});
// update the token with the user's data
token.sub = dbUser.id;
token.email = dbUser.email;
token.username = dbUser.username ?? undefined;
token.name = dbUser.name;
}
return token;
},
session: async ({ session, token }) => {
// make the token's user fields available on the session, so that we can call auth() to fetch it (no db call needed)
if (token?.sub) {
session.user.id = token.sub;
}
if (token?.email) {
session.user.email = token.email;
}
if (token?.username) {
session.user.username = token.username;
}
if (token?.name) {
session.user.name = token.name;
}
if (token?.picture) {
session.user.image = token.picture;
}
return session;
},
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
if (isOnDashboard) {
// so that we return to login page if isLoggedIn is false
return isLoggedIn;
}
// we explicitly allow our public pages to be accessed by anyone
return true;
},
},
// NB: we avoid the credentials provider definition here, so that we can use node-native apis (pw hash) but use this config in the Vercel Edge runtime (middleware)
providers: [],
} satisfies NextAuthConfig;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/auth/index.tsx
================================================
import { authConfig } from "./config.edge";
import { signUp } from "@/cal/auth";
import { env } from "@/env";
import { type Prisma, type User, type CalAccount } from "@prisma/client";
import NextAuth from "next-auth";
import type { Session } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
import { db } from "prisma/client";
import { cache } from "react";
import "server-only";
import { z } from "zod";
// would've loved to use webcrypto apis (supported on edge as well), but: TypeError: randomBytes is not a function
// globalThis.crypto ??= import("node:crypto").then((m) => m.webcrypto);
// const Crypto = globalThis.crypto;
async function hash(password: string) {
return new Promise((resolve, reject) => {
const salt = randomBytes(16).toString("hex");
scrypt(password, salt, 64, (err, derivedKey) => {
if (err) {
console.error("Error hashing password", err);
reject(err);
}
resolve(`${salt}.${derivedKey.toString("hex")}`);
});
});
}
async function compare(password: string, hash: string) {
return new Promise((resolve, reject) => {
const [salt, hashKey] = hash.split(".") as [string, string];
scrypt(password, salt, 64, (err, derivedKey) => {
if (err) {
console.error("Error comparing password", err);
reject(err);
}
resolve(timingSafeEqual(Buffer.from(hashKey, "hex"), derivedKey));
});
});
}
export const LoginSchema = z.object({
email: z.string().min(1).max(42),
password: z.string().min(6).max(32),
});
export const FiltersSchema = z.object({
categories: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
capabilities: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
frameworks: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
budgets: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
languages: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
regions: z.preprocess((val) => {
if (typeof val !== "string") return val; // should error
return JSON.parse(val);
}, z.array(z.string())),
});
export const SignupSchema = LoginSchema.merge(
z.object({
username: z.string().min(1).max(32),
name: z.string().min(1).max(32),
})
);
type UserAfterSignUp = User & { calAccount?: CalAccount };
const {
auth,
handlers: { GET, POST },
signIn,
signOut,
unstable_update,
} = NextAuth({
...authConfig,
/**
* [@calcom] 2️⃣ Attach the Credentials provider to NextAuth:
*/
// NB: `withCal` isn't edge ready as it uses node-native apis; we therefore avoid importing it in `confige.edge.ts`
providers: [
Credentials({
name: "Credentials",
authorize: async (c) => {
const credentials = LoginSchema.safeParse(c);
if (!credentials.success) {
console.error(
`[auth] Invalid sign in submission because of missing credentials: ${credentials.error.errors.map((e) => e.message).join(", ")}`
);
// return `null` to indicate that the credentials are invalid
return null;
}
let user: UserAfterSignUp | null = null;
try {
user = await db.user.findUnique({
where: { email: credentials.data.email },
});
if (user) {
// if user exists, this comes from our login page, let's check the password
console.info(`User ${user.id} attempted login with password`);
if (!user.hashedPassword) {
console.debug(`OAuth User ${user.id} attempted signin with password`);
return null;
}
const pwMatch = await compare(credentials.data.password, user.hashedPassword);
if (!pwMatch) {
console.debug(`User ${user.id} attempted login with bad password`);
return null;
}
return user;
} else {
// if user doesn't exist, this comes from our signup page w/ additional fields
const signupData = SignupSchema.safeParse(c);
if (!signupData.success) {
console.error(
`[auth] Invalid sign in submission because of missing signup data: ${signupData.error.errors
.map((e) => {
// return the path of the error with the message:
return `${e.path.join(".")} (${e.message}) -> '${e.code}'`;
})
.join(", ")}`
);
return null;
}
user = await db.user.create({
data: {
username: signupData.data.username,
name: signupData.data.name,
hashedPassword: await hash(credentials.data.password),
email: credentials.data.email,
},
});
if (!user) {
console.error(`[auth] Unable to create user with email ${credentials.data.email}`);
return null;
}
const toCreate = await signUp({
email: credentials.data.email,
name: signupData.data.name,
user: { id: user.id }, // we don't have the user id yet, so we'll use a placeholder
});
// update the user with the cal account info:
user = await db.user.update({ where: { id: user.id }, data: toCreate });
return user satisfies UserAfterSignUp;
}
} catch (e) {
console.error(e);
return null;
}
},
}),
],
});
export { signIn, signOut, GET, POST, unstable_update, auth };
export const currentUser = cache(async () => {
const sesh = await auth();
if (!sesh?.user.id) return null;
return db.user.findUnique({
where: { id: sesh.user.id },
});
});
export const currentUserWithCalAccount = cache(async () => {
const sesh = await auth();
if (!sesh?.user.id) throw new Error("somehting's wrong here");
return db.calAccount.findUnique({
where: { email: sesh?.user?.email?.replace("@", `+${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}@`) },
});
});
export async function SignedIn(props: { children: (props: { user: Session["user"] }) => React.ReactNode }) {
const sesh = await auth();
return sesh?.user ? <>{props.children({ user: sesh.user })}> : null;
}
export async function SignedOut(props: { children: React.ReactNode }) {
const sesh = await auth();
return sesh?.user ? null : <>{props.children}>;
}
export async function CurrentUser(props: { children: (props: User) => React.ReactNode }) {
const user = await currentUser();
return !!user ? <>{props.children(user)}> : null;
}
export async function CalAccount(props: { children: (props: CalAccount) => React.ReactNode }) {
const calAccount = await currentUserWithCalAccount();
return !!calAccount ? <>{props.children(calAccount)}> : null;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/cal/__generated/cal-sdk.ts
================================================
// @ts-nocheck generated file
import z from "zod";
export type ManagedUserOutput = z.infer;
export const ManagedUserOutput = z.object({
id: z.number(),
email: z.string(),
username: z.union([z.string(), z.null()]),
timeZone: z.string(),
weekStart: z.string(),
createdDate: z.string(),
timeFormat: z.union([z.number(), z.null()]),
defaultScheduleId: z.union([z.number(), z.null()]),
});
export type GetManagedUsersOutput = z.infer;
export const GetManagedUsersOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.array(ManagedUserOutput),
});
export type CreateManagedUserInput = z.infer;
export const CreateManagedUserInput = z.object({
email: z.string(),
timeFormat: z.union([z.literal(12), z.literal(24), z.undefined()]).optional(),
weekStart: z
.union([
z.literal("Monday"),
z.literal("Tuesday"),
z.literal("Wednesday"),
z.literal("Thursday"),
z.literal("Friday"),
z.literal("Saturday"),
z.literal("Sunday"),
z.undefined(),
])
.optional(),
timeZone: z.union([z.string(), z.undefined()]).optional(),
name: z.union([z.string(), z.undefined()]).optional(),
});
export type CreateManagedUserData = z.infer;
export const CreateManagedUserData = z.object({
user: ManagedUserOutput,
accessToken: z.string(),
refreshToken: z.string(),
});
export type CreateManagedUserOutput = z.infer;
export const CreateManagedUserOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: CreateManagedUserData,
});
export type GetManagedUserOutput = z.infer;
export const GetManagedUserOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: ManagedUserOutput,
});
export type UpdateManagedUserInput = z.infer;
export const UpdateManagedUserInput = z.object({
timeFormat: z.union([z.literal(12), z.literal(24)]).optional(),
weekStart: z
.union([
z.literal("Monday"),
z.literal("Tuesday"),
z.literal("Wednesday"),
z.literal("Thursday"),
z.literal("Friday"),
z.literal("Saturday"),
z.literal("Sunday"),
])
.optional(),
email: z.string().optional(),
name: z.string().optional(),
defaultScheduleId: z.number().optional(),
timeZone: z.string().optional(),
});
export type KeysDto = z.infer;
export const KeysDto = z.object({
accessToken: z.string(),
refreshToken: z.string(),
});
export type KeysResponseDto = z.infer;
export const KeysResponseDto = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: KeysDto,
});
export type CreateOAuthClientInput = z.infer;
export const CreateOAuthClientInput = z.object({});
export type DataDto = z.infer;
export const DataDto = z.object({
clientId: z.string(),
clientSecret: z.string(),
});
export type CreateOAuthClientResponseDto = z.infer;
export const CreateOAuthClientResponseDto = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: DataDto,
});
export type PlatformOAuthClientDto = z.infer;
export const PlatformOAuthClientDto = z.object({
id: z.string(),
name: z.string(),
secret: z.string(),
permissions: z.number(),
logo: z.union([z.string(), z.null(), z.undefined()]).optional(),
redirectUris: z.array(z.string()),
organizationId: z.number(),
createdAt: z.string(),
});
export type GetOAuthClientsResponseDto = z.infer;
export const GetOAuthClientsResponseDto = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.array(PlatformOAuthClientDto),
});
export type GetOAuthClientResponseDto = z.infer;
export const GetOAuthClientResponseDto = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: PlatformOAuthClientDto,
});
export type UpdateOAuthClientInput = z.infer;
export const UpdateOAuthClientInput = z.object({
logo: z.string().optional(),
name: z.string().optional(),
redirectUris: z.array(z.string()).optional(),
bookingRedirectUri: z.string().optional(),
bookingCancelRedirectUri: z.string().optional(),
bookingRescheduleRedirectUri: z.string().optional(),
areEmailsEnabled: z.boolean().optional(),
});
export type OAuthAuthorizeInput = z.infer;
export const OAuthAuthorizeInput = z.object({
redirectUri: z.string(),
});
export type ExchangeAuthorizationCodeInput = z.infer;
export const ExchangeAuthorizationCodeInput = z.object({
clientSecret: z.string(),
});
export type RefreshTokenInput = z.infer;
export const RefreshTokenInput = z.object({
refreshToken: z.string(),
});
export type EventTypeLocation = z.infer;
export const EventTypeLocation = z.object({
type: z.string(),
link: z.union([z.string(), z.undefined()]).optional(),
});
export type CreateEventTypeInput = z.infer;
export const CreateEventTypeInput = z.object({
length: z.number(),
slug: z.string(),
title: z.string(),
description: z.union([z.string(), z.undefined()]).optional(),
locations: z.union([z.array(EventTypeLocation), z.undefined()]).optional(),
disableGuests: z.union([z.boolean(), z.undefined()]).optional(),
});
export type EventTypeOutput = z.infer;
export const EventTypeOutput = z.object({
id: z.number(),
length: z.number(),
slug: z.string(),
title: z.string(),
description: z.union([z.string(), z.null()]),
locations: z.union([z.array(EventTypeLocation), z.null()]),
});
export type CreateEventTypeOutput = z.infer;
export const CreateEventTypeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: EventTypeOutput,
});
export type Data = z.infer;
export const Data = z.object({
eventType: EventTypeOutput,
});
export type GetEventTypeOutput = z.infer;
export const GetEventTypeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: Data,
});
export type EventTypeGroup = z.infer;
export const EventTypeGroup = z.object({
eventTypes: z.array(EventTypeOutput),
});
export type GetEventTypesData = z.infer;
export const GetEventTypesData = z.object({
eventTypeGroups: z.array(EventTypeGroup),
});
export type GetEventTypesOutput = z.infer;
export const GetEventTypesOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: GetEventTypesData,
});
export type Location = z.infer;
export const Location = z.object({
type: z.string(),
});
export type Source = z.infer;
export const Source = z.object({
id: z.string(),
type: z.string(),
label: z.string(),
});
export type BookingField = z.infer;
export const BookingField = z.object({
name: z.string(),
type: z.string(),
defaultLabel: z.union([z.string(), z.undefined()]).optional(),
label: z.union([z.string(), z.undefined()]).optional(),
placeholder: z.union([z.string(), z.undefined()]).optional(),
required: z.union([z.boolean(), z.undefined()]).optional(),
getOptionsAt: z.union([z.string(), z.undefined()]).optional(),
hideWhenJustOneOption: z.union([z.boolean(), z.undefined()]).optional(),
editable: z.union([z.string(), z.undefined()]).optional(),
sources: z.union([z.array(Source), z.undefined()]).optional(),
});
export type Organization = z.infer;
export const Organization = z.object({
id: z.number(),
slug: z.union([z.string(), z.null(), z.undefined()]).optional(),
name: z.string(),
metadata: z.unknown(),
});
export type Profile = z.infer;
export const Profile = z.object({
username: z.union([z.string(), z.null()]),
id: z.union([z.number(), z.null()]),
userId: z.union([z.number(), z.undefined()]).optional(),
uid: z.union([z.string(), z.undefined()]).optional(),
name: z.union([z.string(), z.undefined()]).optional(),
organizationId: z.union([z.number(), z.null()]),
organization: z.union([Organization, z.null(), z.undefined()]).optional(),
upId: z.string(),
image: z.union([z.string(), z.undefined()]).optional(),
brandColor: z.union([z.string(), z.undefined()]).optional(),
darkBrandColor: z.union([z.string(), z.undefined()]).optional(),
theme: z.union([z.string(), z.undefined()]).optional(),
bookerLayouts: z.union([z.unknown(), z.undefined()]).optional(),
});
export type Owner = z.infer;
export const Owner = z.object({
id: z.number(),
avatarUrl: z.union([z.string(), z.null(), z.undefined()]).optional(),
username: z.union([z.string(), z.null()]),
name: z.union([z.string(), z.null()]),
weekStart: z.string(),
brandColor: z.union([z.string(), z.null(), z.undefined()]).optional(),
darkBrandColor: z.union([z.string(), z.null(), z.undefined()]).optional(),
theme: z.union([z.string(), z.null(), z.undefined()]).optional(),
metadata: z.unknown(),
defaultScheduleId: z.union([z.number(), z.null(), z.undefined()]).optional(),
nonProfileUsername: z.union([z.string(), z.null()]),
profile: Profile,
});
export type Schedule = z.infer;
export const Schedule = z.object({
id: z.number(),
timeZone: z.union([z.string(), z.null()]),
});
export type User = z.infer;
export const User = z.object({
username: z.union([z.string(), z.null()]),
name: z.union([z.string(), z.null()]),
weekStart: z.string(),
organizationId: z.union([z.number(), z.undefined()]).optional(),
avatarUrl: z.union([z.string(), z.null(), z.undefined()]).optional(),
profile: Profile,
bookerUrl: z.string(),
});
export type PublicEventTypeOutput = z.infer;
export const PublicEventTypeOutput = z.object({
id: z.number(),
title: z.string(),
description: z.string(),
eventName: z.union([z.string(), z.null(), z.undefined()]).optional(),
slug: z.string(),
isInstantEvent: z.boolean(),
aiPhoneCallConfig: z.union([z.unknown(), z.undefined()]).optional(),
schedulingType: z.union([z.unknown(), z.undefined()]).optional(),
length: z.number(),
locations: z.array(Location),
customInputs: z.array(z.unknown()),
disableGuests: z.boolean(),
metadata: z.union([z.unknown(), z.null()]),
lockTimeZoneToggleOnBookingPage: z.boolean(),
requiresConfirmation: z.boolean(),
requiresBookerEmailVerification: z.boolean(),
recurringEvent: z.union([z.unknown(), z.undefined()]).optional(),
price: z.number(),
currency: z.string(),
seatsPerTimeSlot: z.union([z.number(), z.null(), z.undefined()]).optional(),
seatsShowAvailabilityCount: z.union([z.boolean(), z.null()]),
bookingFields: z.array(BookingField),
team: z.union([z.unknown(), z.undefined()]).optional(),
successRedirectUrl: z.union([z.string(), z.null(), z.undefined()]).optional(),
workflows: z.array(z.unknown()),
hosts: z.array(z.unknown()),
owner: z.union([Owner, z.null()]),
schedule: z.union([Schedule, z.null()]),
hidden: z.boolean(),
assignAllTeamMembers: z.boolean(),
bookerLayouts: z.union([z.unknown(), z.undefined()]).optional(),
users: z.array(User),
entity: z.unknown(),
isDynamic: z.boolean(),
});
export type GetEventTypePublicOutput = z.infer;
export const GetEventTypePublicOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.union([PublicEventTypeOutput, z.null()]),
});
export type PublicEventType = z.infer;
export const PublicEventType = z.object({
id: z.number(),
length: z.number(),
slug: z.string(),
title: z.string(),
description: z.union([z.string(), z.null(), z.undefined()]).optional(),
});
export type GetEventTypesPublicOutput = z.infer;
export const GetEventTypesPublicOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.array(PublicEventType),
});
export type UpdateEventTypeInput = z.infer;
export const UpdateEventTypeInput = z.object({
length: z.number().optional(),
slug: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
hidden: z.boolean().optional(),
locations: z.array(EventTypeLocation).optional(),
disableGuests: z.boolean().optional(),
});
export type UpdateEventTypeOutput = z.infer;
export const UpdateEventTypeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: EventTypeOutput,
});
export type DeleteData = z.infer;
export const DeleteData = z.object({
id: z.number(),
length: z.number(),
slug: z.string(),
title: z.string(),
});
export type DeleteEventTypeOutput = z.infer;
export const DeleteEventTypeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: DeleteData,
});
export type CreateAvailabilityInput = z.infer;
export const CreateAvailabilityInput = z.object({
days: z.array(z.number()),
startTime: z.string(),
endTime: z.string(),
});
export type CreateScheduleInput = z.infer;
export const CreateScheduleInput = z.object({
name: z.string(),
timeZone: z.string(),
availabilities: z.union([z.array(CreateAvailabilityInput), z.undefined()]).optional(),
isDefault: z.boolean(),
});
export type WorkingHours = z.infer;
export const WorkingHours = z.object({
days: z.array(z.number()),
startTime: z.number(),
endTime: z.number(),
userId: z.union([z.number(), z.null(), z.undefined()]).optional(),
});
export type AvailabilityModel = z.infer;
export const AvailabilityModel = z.object({
id: z.number(),
userId: z.union([z.number(), z.null(), z.undefined()]).optional(),
eventTypeId: z.union([z.number(), z.null(), z.undefined()]).optional(),
days: z.array(z.number()),
startTime: z.string(),
endTime: z.string(),
date: z.union([z.string(), z.null(), z.undefined()]).optional(),
scheduleId: z.union([z.number(), z.null(), z.undefined()]).optional(),
});
export type TimeRange = z.infer;
export const TimeRange = z.object({
userId: z.union([z.number(), z.null(), z.undefined()]).optional(),
start: z.string(),
end: z.string(),
});
export type ScheduleOutput = z.infer;
export const ScheduleOutput = z.object({
id: z.number(),
name: z.string(),
isManaged: z.boolean(),
workingHours: z.array(WorkingHours),
schedule: z.array(AvailabilityModel),
availability: z.array(z.array(TimeRange)),
timeZone: z.string(),
dateOverrides: z.array(z.unknown()),
isDefault: z.boolean(),
isLastSchedule: z.boolean(),
readOnly: z.boolean(),
});
export type CreateScheduleOutput = z.infer;
export const CreateScheduleOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: ScheduleOutput,
});
export type GetDefaultScheduleOutput = z.infer;
export const GetDefaultScheduleOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.union([ScheduleOutput, z.null()]),
});
export type GetScheduleOutput = z.infer;
export const GetScheduleOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: ScheduleOutput,
});
export type GetSchedulesOutput = z.infer;
export const GetSchedulesOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: ScheduleOutput,
});
export type UpdateScheduleInput = z.infer;
export const UpdateScheduleInput = z.object({
timeZone: z.string(),
name: z.string(),
isDefault: z.boolean(),
schedule: z.array(z.array(z.any())),
dateOverrides: z.union([z.array(z.array(z.any())), z.undefined()]).optional(),
});
export type EventTypeModel = z.infer;
export const EventTypeModel = z.object({
id: z.number(),
eventName: z.union([z.string(), z.null(), z.undefined()]).optional(),
});
export type ScheduleModel = z.infer;
export const ScheduleModel = z.object({
id: z.number(),
userId: z.number(),
name: z.string(),
timeZone: z.union([z.string(), z.null(), z.undefined()]).optional(),
eventType: z.union([z.array(EventTypeModel), z.undefined()]).optional(),
availability: z.union([z.array(AvailabilityModel), z.undefined()]).optional(),
});
export type UpdatedScheduleOutput = z.infer;
export const UpdatedScheduleOutput = z.object({
schedule: ScheduleModel,
isDefault: z.boolean(),
timeZone: z.union([z.string(), z.undefined()]).optional(),
prevDefaultId: z.union([z.number(), z.null(), z.undefined()]).optional(),
currentDefaultId: z.union([z.number(), z.null(), z.undefined()]).optional(),
});
export type UpdateScheduleOutput = z.infer;
export const UpdateScheduleOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: UpdatedScheduleOutput,
});
export type DeleteScheduleOutput = z.infer;
export const DeleteScheduleOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
});
export type AuthUrlData = z.infer;
export const AuthUrlData = z.object({
authUrl: z.string(),
});
export type GcalAuthUrlOutput = z.infer;
export const GcalAuthUrlOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: AuthUrlData,
});
export type GcalSaveRedirectOutput = z.infer;
export const GcalSaveRedirectOutput = z.object({
url: z.string(),
});
export type GcalCheckOutput = z.infer;
export const GcalCheckOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
});
export type ProviderVerifyClientOutput = z.infer;
export const ProviderVerifyClientOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
});
export type ProviderVerifyAccessTokenOutput = z.infer;
export const ProviderVerifyAccessTokenOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
});
export type MeOutput = z.infer;
export const MeOutput = z.object({
id: z.number(),
username: z.string(),
email: z.string(),
timeFormat: z.number(),
defaultScheduleId: z.union([z.number(), z.null()]),
weekStart: z.string(),
timeZone: z.string(),
});
export type GetMeOutput = z.infer;
export const GetMeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: MeOutput,
});
export type UpdateMeOutput = z.infer;
export const UpdateMeOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: MeOutput,
});
export type BusyTimesOutput = z.infer;
export const BusyTimesOutput = z.object({
start: z.string(),
end: z.string(),
source: z.union([z.string(), z.null(), z.undefined()]).optional(),
});
export type GetBusyTimesOutput = z.infer;
export const GetBusyTimesOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: z.array(BusyTimesOutput),
});
export type Integration = z.infer;
export const Integration = z.object({
appData: z.union([z.unknown(), z.null(), z.undefined()]).optional(),
dirName: z.union([z.string(), z.undefined()]).optional(),
__template: z.union([z.string(), z.undefined()]).optional(),
name: z.string(),
description: z.string(),
installed: z.union([z.boolean(), z.undefined()]).optional(),
type: z.string(),
title: z.union([z.string(), z.undefined()]).optional(),
variant: z.string(),
category: z.union([z.string(), z.undefined()]).optional(),
categories: z.array(z.string()),
logo: z.string(),
publisher: z.string(),
slug: z.string(),
url: z.string(),
email: z.string(),
locationOption: z.union([z.unknown(), z.null()]),
});
export type Primary = z.infer;
export const Primary = z.object({
externalId: z.string(),
integration: z.union([z.string(), z.undefined()]).optional(),
name: z.union([z.string(), z.undefined()]).optional(),
primary: z.union([z.boolean(), z.null()]),
readOnly: z.boolean(),
email: z.union([z.string(), z.undefined()]).optional(),
isSelected: z.boolean(),
credentialId: z.number(),
});
export type Calendar = z.infer;
export const Calendar = z.object({
externalId: z.string(),
integration: z.union([z.string(), z.undefined()]).optional(),
name: z.union([z.string(), z.undefined()]).optional(),
primary: z.union([z.boolean(), z.null(), z.undefined()]).optional(),
readOnly: z.boolean(),
email: z.union([z.string(), z.undefined()]).optional(),
isSelected: z.boolean(),
credentialId: z.number(),
});
export type ConnectedCalendar = z.infer;
export const ConnectedCalendar = z.object({
integration: Integration,
credentialId: z.number(),
primary: z.union([Primary, z.undefined()]).optional(),
calendars: z.union([z.array(Calendar), z.undefined()]).optional(),
});
export type DestinationCalendar = z.infer;
export const DestinationCalendar = z.object({
id: z.number(),
integration: z.string(),
externalId: z.string(),
primaryEmail: z.union([z.string(), z.null()]),
userId: z.union([z.number(), z.null()]),
eventTypeId: z.union([z.number(), z.null()]),
credentialId: z.union([z.number(), z.null()]),
name: z.union([z.string(), z.null(), z.undefined()]).optional(),
primary: z.union([z.boolean(), z.undefined()]).optional(),
readOnly: z.union([z.boolean(), z.undefined()]).optional(),
email: z.union([z.string(), z.undefined()]).optional(),
integrationTitle: z.union([z.string(), z.undefined()]).optional(),
});
export type ConnectedCalendarsData = z.infer;
export const ConnectedCalendarsData = z.object({
connectedCalendars: z.array(ConnectedCalendar),
destinationCalendar: DestinationCalendar,
});
export type ConnectedCalendarsOutput = z.infer;
export const ConnectedCalendarsOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: ConnectedCalendarsData,
});
export type Attendee = z.infer;
export const Attendee = z.object({
id: z.number(),
email: z.string(),
name: z.string(),
timeZone: z.string(),
locale: z.union([z.string(), z.null()]),
bookingId: z.union([z.number(), z.null()]),
});
export type EventType = z.infer;
export const EventType = z.object({
slug: z.union([z.string(), z.undefined()]).optional(),
id: z.union([z.number(), z.undefined()]).optional(),
eventName: z.union([z.string(), z.null(), z.undefined()]).optional(),
price: z.number(),
recurringEvent: z.union([z.unknown(), z.undefined()]).optional(),
currency: z.string(),
metadata: z.unknown(),
seatsShowAttendees: z.union([z.unknown(), z.undefined()]).optional(),
seatsShowAvailabilityCount: z.union([z.unknown(), z.undefined()]).optional(),
team: z.union([z.unknown(), z.null(), z.undefined()]).optional(),
});
export type Reference = z.infer;
export const Reference = z.object({
id: z.number(),
type: z.string(),
uid: z.string(),
meetingId: z.union([z.string(), z.null(), z.undefined()]).optional(),
thirdPartyRecurringEventId: z.union([z.string(), z.null(), z.undefined()]).optional(),
meetingPassword: z.union([z.string(), z.null()]),
meetingUrl: z.union([z.string(), z.null(), z.undefined()]).optional(),
bookingId: z.union([z.number(), z.null()]),
externalCalendarId: z.union([z.string(), z.null()]),
deleted: z.union([z.unknown(), z.undefined()]).optional(),
credentialId: z.union([z.number(), z.null()]),
});
export type GetBookingsDataEntry = z.infer;
export const GetBookingsDataEntry = z.object({
id: z.number(),
title: z.string(),
userPrimaryEmail: z.union([z.string(), z.null(), z.undefined()]).optional(),
description: z.union([z.string(), z.null()]),
customInputs: z.unknown(),
startTime: z.string(),
endTime: z.string(),
attendees: z.array(Attendee),
metadata: z.unknown(),
uid: z.string(),
recurringEventId: z.union([z.string(), z.null()]),
location: z.union([z.string(), z.null()]),
eventType: EventType,
status: z.unknown(),
paid: z.boolean(),
payment: z.array(z.unknown()),
references: z.array(Reference),
isRecorded: z.boolean(),
seatsReferences: z.array(z.unknown()),
user: z.union([User, z.null()]),
rescheduled: z.union([z.unknown(), z.undefined()]).optional(),
});
export type GetBookingsData = z.infer;
export const GetBookingsData = z.object({
bookings: z.array(GetBookingsDataEntry),
recurringInfo: z.array(z.unknown()),
nextCursor: z.union([z.number(), z.null()]),
});
export type GetBookingsOutput = z.infer;
export const GetBookingsOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: GetBookingsData,
});
export type GetBookingData = z.infer;
export const GetBookingData = z.object({
title: z.string(),
id: z.number(),
uid: z.string(),
description: z.union([z.string(), z.null()]),
customInputs: z.unknown(),
smsReminderNumber: z.union([z.string(), z.null()]),
recurringEventId: z.union([z.string(), z.null()]),
startTime: z.string(),
endTime: z.string(),
location: z.union([z.string(), z.null()]),
status: z.string(),
metadata: z.unknown(),
cancellationReason: z.union([z.string(), z.null()]),
responses: z.unknown(),
rejectionReason: z.union([z.string(), z.null()]),
userPrimaryEmail: z.union([z.string(), z.null()]),
user: z.union([User, z.null()]),
attendees: z.array(Attendee),
eventTypeId: z.union([z.number(), z.null()]),
eventType: z.union([EventType, z.null()]),
});
export type GetBookingOutput = z.infer;
export const GetBookingOutput = z.object({
status: z.union([z.literal("success"), z.literal("error")]),
data: GetBookingData,
});
export type Response = z.infer;
export const Response = z.object({
name: z.string(),
email: z.string(),
guests: z.array(z.string()),
location: z.union([Location, z.undefined()]).optional(),
notes: z.union([z.string(), z.undefined()]).optional(),
});
export type CreateBookingInput = z.infer;
export const CreateBookingInput = z.object({
end: z.union([z.string(), z.undefined()]).optional(),
start: z.string(),
eventTypeId: z.number(),
eventTypeSlug: z.union([z.string(), z.undefined()]).optional(),
rescheduleUid: z.union([z.string(), z.undefined()]).optional(),
recurringEventId: z.union([z.string(), z.undefined()]).optional(),
timeZone: z.string(),
user: z.union([z.array(z.string()), z.undefined()]).optional(),
language: z.string(),
bookingUid: z.union([z.string(), z.undefined()]).optional(),
metadata: z.unknown(),
hasHashedBookingLink: z.union([z.boolean(), z.undefined()]).optional(),
hashedLink: z.union([z.string(), z.null()]),
seatReferenceUid: z.union([z.string(), z.undefined()]).optional(),
responses: Response,
orgSlug: z.union([z.string(), z.undefined()]).optional(),
locationUrl: z.union([z.string(), z.undefined()]).optional(),
});
export type CancelBookingInput = z.infer;
export const CancelBookingInput = z.object({
id: z.number(),
uid: z.string(),
allRemainingBookings: z.boolean(),
cancellationReason: z.string(),
seatReferenceUid: z.string(),
});
export type ReserveSlotInput = z.infer;
export const ReserveSlotInput = z.object({});
export type get_OAuthClientUsersController_getManagedUsers =
typeof get_OAuthClientUsersController_getManagedUsers;
export const get_OAuthClientUsersController_getManagedUsers = {
method: z.literal("GET"),
path: z.literal("/v2/oauth-clients/{clientId}/users"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
}),
response: GetManagedUsersOutput,
};
export type post_OAuthClientUsersController_createUser = typeof post_OAuthClientUsersController_createUser;
export const post_OAuthClientUsersController_createUser = {
method: z.literal("POST"),
path: z.literal("/v2/oauth-clients/{clientId}/users"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
body: CreateManagedUserInput,
}),
response: CreateManagedUserOutput,
};
export type get_OAuthClientUsersController_getUserById = typeof get_OAuthClientUsersController_getUserById;
export const get_OAuthClientUsersController_getUserById = {
method: z.literal("GET"),
path: z.literal("/v2/oauth-clients/{clientId}/users/{userId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
userId: z.number(),
}),
}),
response: GetManagedUserOutput,
};
export type patch_OAuthClientUsersController_updateUser = typeof patch_OAuthClientUsersController_updateUser;
export const patch_OAuthClientUsersController_updateUser = {
method: z.literal("PATCH"),
path: z.literal("/v2/oauth-clients/{clientId}/users/{userId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
userId: z.number(),
}),
body: UpdateManagedUserInput,
}),
response: GetManagedUserOutput,
};
export type delete_OAuthClientUsersController_deleteUser =
typeof delete_OAuthClientUsersController_deleteUser;
export const delete_OAuthClientUsersController_deleteUser = {
method: z.literal("DELETE"),
path: z.literal("/v2/oauth-clients/{clientId}/users/{userId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
userId: z.number(),
}),
}),
response: GetManagedUserOutput,
};
export type post_OAuthClientUsersController_forceRefresh =
typeof post_OAuthClientUsersController_forceRefresh;
export const post_OAuthClientUsersController_forceRefresh = {
method: z.literal("POST"),
path: z.literal("/v2/oauth-clients/{clientId}/users/{userId}/force-refresh"),
parameters: z.object({
path: z.object({
userId: z.number(),
clientId: z.string(),
}),
}),
response: KeysResponseDto,
};
export type post_OAuthClientsController_createOAuthClient =
typeof post_OAuthClientsController_createOAuthClient;
export const post_OAuthClientsController_createOAuthClient = {
method: z.literal("POST"),
path: z.literal("/v2/oauth-clients"),
parameters: z.object({
body: CreateOAuthClientInput,
}),
response: CreateOAuthClientResponseDto,
};
export type get_OAuthClientsController_getOAuthClients = typeof get_OAuthClientsController_getOAuthClients;
export const get_OAuthClientsController_getOAuthClients = {
method: z.literal("GET"),
path: z.literal("/v2/oauth-clients"),
parameters: z.never(),
response: GetOAuthClientsResponseDto,
};
export type get_OAuthClientsController_getOAuthClientById =
typeof get_OAuthClientsController_getOAuthClientById;
export const get_OAuthClientsController_getOAuthClientById = {
method: z.literal("GET"),
path: z.literal("/v2/oauth-clients/{clientId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
}),
response: GetOAuthClientResponseDto,
};
export type patch_OAuthClientsController_updateOAuthClient =
typeof patch_OAuthClientsController_updateOAuthClient;
export const patch_OAuthClientsController_updateOAuthClient = {
method: z.literal("PATCH"),
path: z.literal("/v2/oauth-clients/{clientId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
body: UpdateOAuthClientInput,
}),
response: GetOAuthClientResponseDto,
};
export type delete_OAuthClientsController_deleteOAuthClient =
typeof delete_OAuthClientsController_deleteOAuthClient;
export const delete_OAuthClientsController_deleteOAuthClient = {
method: z.literal("DELETE"),
path: z.literal("/v2/oauth-clients/{clientId}"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
}),
response: GetOAuthClientResponseDto,
};
export type get_OAuthClientsController_getOAuthClientManagedUsersById =
typeof get_OAuthClientsController_getOAuthClientManagedUsersById;
export const get_OAuthClientsController_getOAuthClientManagedUsersById = z.object({
method: z.literal("GET"),
path: z.literal("/v2/oauth-clients/{clientId}/managed-users"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
}),
response: GetManagedUsersOutput,
});
export type post_OAuthFlowController_authorize = typeof post_OAuthFlowController_authorize;
export const post_OAuthFlowController_authorize = {
method: z.literal("POST"),
path: z.literal("/v2/oauth/{clientId}/authorize"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
body: OAuthAuthorizeInput,
}),
response: z.unknown(),
};
export type post_OAuthFlowController_exchange = typeof post_OAuthFlowController_exchange;
export const post_OAuthFlowController_exchange = {
method: z.literal("POST"),
path: z.literal("/v2/oauth/{clientId}/exchange"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
header: z.object({
Authorization: z.string(),
}),
body: ExchangeAuthorizationCodeInput,
}),
response: KeysResponseDto,
};
export type post_OAuthFlowController_refreshAccessToken = typeof post_OAuthFlowController_refreshAccessToken;
export const post_OAuthFlowController_refreshAccessToken = {
method: z.literal("POST"),
path: z.literal("/v2/oauth/{clientId}/refresh"),
parameters: z.object({
path: z.object({
clientId: z.string(),
}),
header: z.object({
"x-cal-secret-key": z.string(),
}),
body: RefreshTokenInput,
}),
response: KeysResponseDto,
};
export type post_EventTypesController_createEventType = typeof post_EventTypesController_createEventType;
export const post_EventTypesController_createEventType = {
method: z.literal("POST"),
path: z.literal("/v2/event-types"),
parameters: z.object({
body: CreateEventTypeInput,
}),
response: CreateEventTypeOutput,
};
export type get_EventTypesController_getEventTypes = typeof get_EventTypesController_getEventTypes;
export const get_EventTypesController_getEventTypes = {
method: z.literal("GET"),
path: z.literal("/v2/event-types"),
parameters: z.never(),
response: GetEventTypesOutput,
};
export type get_EventTypesController_getEventType = typeof get_EventTypesController_getEventType;
export const get_EventTypesController_getEventType = {
method: z.literal("GET"),
path: z.literal("/v2/event-types/{eventTypeId}"),
parameters: z.object({
path: z.object({
eventTypeId: z.string(),
}),
}),
response: GetEventTypeOutput,
};
export type patch_EventTypesController_updateEventType = typeof patch_EventTypesController_updateEventType;
export const patch_EventTypesController_updateEventType = {
method: z.literal("PATCH"),
path: z.literal("/v2/event-types/{eventTypeId}"),
parameters: z.object({
path: z.object({
eventTypeId: z.number(),
}),
body: UpdateEventTypeInput,
}),
response: UpdateEventTypeOutput,
};
export type delete_EventTypesController_deleteEventType = typeof delete_EventTypesController_deleteEventType;
export const delete_EventTypesController_deleteEventType = {
method: z.literal("DELETE"),
path: z.literal("/v2/event-types/{eventTypeId}"),
parameters: z.object({
path: z.object({
eventTypeId: z.number(),
}),
}),
response: DeleteEventTypeOutput,
};
export type get_EventTypesController_getPublicEventType = typeof get_EventTypesController_getPublicEventType;
export const get_EventTypesController_getPublicEventType = {
method: z.literal("GET"),
path: z.literal("/v2/event-types/{username}/{eventSlug}/public"),
parameters: z.object({
query: z.object({
isTeamEvent: z.boolean().optional(),
org: z.union([z.string(), z.null()]).optional(),
}),
path: z.object({
username: z.string(),
eventSlug: z.string(),
}),
}),
response: GetEventTypePublicOutput,
};
export type get_EventTypesController_getPublicEventTypes =
typeof get_EventTypesController_getPublicEventTypes;
export const get_EventTypesController_getPublicEventTypes = {
method: z.literal("GET"),
path: z.literal("/v2/event-types/{username}/public"),
parameters: z.object({
path: z.object({
username: z.string(),
}),
}),
response: GetEventTypesPublicOutput,
};
export type post_SchedulesController_createSchedule = typeof post_SchedulesController_createSchedule;
export const post_SchedulesController_createSchedule = {
method: z.literal("POST"),
path: z.literal("/v2/schedules"),
parameters: z.object({
body: CreateScheduleInput,
}),
response: CreateScheduleOutput,
};
export type get_SchedulesController_getSchedules = typeof get_SchedulesController_getSchedules;
export const get_SchedulesController_getSchedules = {
method: z.literal("GET"),
path: z.literal("/v2/schedules"),
parameters: z.never(),
response: GetSchedulesOutput,
};
export type get_SchedulesController_getDefaultSchedule = typeof get_SchedulesController_getDefaultSchedule;
export const get_SchedulesController_getDefaultSchedule = {
method: z.literal("GET"),
path: z.literal("/v2/schedules/default"),
parameters: z.never(),
response: GetDefaultScheduleOutput,
};
export type get_SchedulesController_getSchedule = typeof get_SchedulesController_getSchedule;
export const get_SchedulesController_getSchedule = {
method: z.literal("GET"),
path: z.literal("/v2/schedules/{scheduleId}"),
parameters: z.object({
path: z.object({
scheduleId: z.number(),
}),
}),
response: GetScheduleOutput,
};
export type patch_SchedulesController_updateSchedule = typeof patch_SchedulesController_updateSchedule;
export const patch_SchedulesController_updateSchedule = {
method: z.literal("PATCH"),
path: z.literal("/v2/schedules/{scheduleId}"),
parameters: z.object({
path: z.object({
scheduleId: z.string(),
}),
body: UpdateScheduleInput,
}),
response: UpdateScheduleOutput,
};
export type delete_SchedulesController_deleteSchedule = typeof delete_SchedulesController_deleteSchedule;
export const delete_SchedulesController_deleteSchedule = {
method: z.literal("DELETE"),
path: z.literal("/v2/schedules/{scheduleId}"),
parameters: z.object({
path: z.object({
scheduleId: z.number(),
}),
}),
response: DeleteScheduleOutput,
};
export type get_BookingsController_getBookings = typeof get_BookingsController_getBookings;
export const get_BookingsController_getBookings = {
method: z.literal("GET"),
path: z.literal("/v2/bookings"),
parameters: z.object({
query: z.object({
cursor: z.number(),
limit: z.number(),
"filters[status]": z.union([
z.literal("upcoming"),
z.literal("recurring"),
z.literal("past"),
z.literal("cancelled"),
z.literal("unconfirmed"),
]),
}),
}),
response: GetBookingsOutput,
};
export type post_BookingsController_createBooking = typeof post_BookingsController_createBooking;
export const post_BookingsController_createBooking = {
method: z.literal("POST"),
path: z.literal("/v2/bookings"),
parameters: z.object({
header: z.object({
"x-cal-client-id": z.string(),
}),
body: CreateBookingInput,
}),
response: z.unknown(),
};
export type get_BookingsController_getBooking = typeof get_BookingsController_getBooking;
export const get_BookingsController_getBooking = {
method: z.literal("GET"),
path: z.literal("/v2/bookings/{bookingUid}"),
parameters: z.object({
path: z.object({
bookingUid: z.string(),
}),
}),
response: GetBookingOutput,
};
export type get_BookingsController_getBookingForReschedule =
typeof get_BookingsController_getBookingForReschedule;
export const get_BookingsController_getBookingForReschedule = {
method: z.literal("GET"),
path: z.literal("/v2/bookings/{bookingUid}/reschedule"),
parameters: z.object({
path: z.object({
bookingUid: z.string(),
}),
}),
response: z.unknown(),
};
export type post_BookingsController_cancelBooking = typeof post_BookingsController_cancelBooking;
export const post_BookingsController_cancelBooking = {
method: z.literal("POST"),
path: z.literal("/v2/bookings/{bookingId}/cancel"),
parameters: z.object({
path: z.object({
bookingId: z.string(),
}),
header: z.object({
"x-cal-client-id": z.string(),
}),
body: CancelBookingInput,
}),
response: z.unknown(),
};
export type post_BookingsController_createRecurringBooking =
typeof post_BookingsController_createRecurringBooking;
export const post_BookingsController_createRecurringBooking = {
method: z.literal("POST"),
path: z.literal("/v2/bookings/recurring"),
parameters: z.object({
header: z.object({
"x-cal-client-id": z.string(),
}),
body: z.array(z.string()),
}),
response: z.unknown(),
};
export type post_BookingsController_createInstantBooking =
typeof post_BookingsController_createInstantBooking;
export const post_BookingsController_createInstantBooking = {
method: z.literal("POST"),
path: z.literal("/v2/bookings/instant"),
parameters: z.object({
header: z.object({
"x-cal-client-id": z.string(),
}),
body: CreateBookingInput,
}),
response: z.unknown(),
};
//
export const EndpointByMethod = {
get: {
"/v2/oauth-clients/{clientId}/users": get_OAuthClientUsersController_getManagedUsers,
"/v2/oauth-clients/{clientId}/users/{userId}": get_OAuthClientUsersController_getUserById,
"/v2/oauth-clients": get_OAuthClientsController_getOAuthClients,
"/v2/oauth-clients/{clientId}": get_OAuthClientsController_getOAuthClientById,
"/v2/oauth-clients/{clientId}/managed-users": get_OAuthClientsController_getOAuthClientManagedUsersById,
"/v2/event-types": get_EventTypesController_getEventTypes,
"/v2/event-types/{eventTypeId}": get_EventTypesController_getEventType,
"/v2/event-types/{username}/{eventSlug}/public": get_EventTypesController_getPublicEventType,
"/v2/event-types/{username}/public": get_EventTypesController_getPublicEventTypes,
"/v2/schedules": get_SchedulesController_getSchedules,
"/v2/schedules/default": get_SchedulesController_getDefaultSchedule,
"/v2/schedules/{scheduleId}": get_SchedulesController_getSchedule,
"/v2/bookings": get_BookingsController_getBookings,
"/v2/bookings/{bookingUid}": get_BookingsController_getBooking,
"/v2/bookings/{bookingUid}/reschedule": get_BookingsController_getBookingForReschedule,
},
post: {
"/v2/oauth-clients/{clientId}/users": post_OAuthClientUsersController_createUser,
"/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": post_OAuthClientUsersController_forceRefresh,
"/v2/oauth-clients": post_OAuthClientsController_createOAuthClient,
"/v2/oauth/{clientId}/authorize": post_OAuthFlowController_authorize,
"/v2/oauth/{clientId}/exchange": post_OAuthFlowController_exchange,
"/v2/oauth/{clientId}/refresh": post_OAuthFlowController_refreshAccessToken,
"/v2/event-types": post_EventTypesController_createEventType,
"/v2/schedules": post_SchedulesController_createSchedule,
"/v2/bookings": post_BookingsController_createBooking,
"/v2/bookings/{bookingId}/cancel": post_BookingsController_cancelBooking,
"/v2/bookings/recurring": post_BookingsController_createRecurringBooking,
"/v2/bookings/instant": post_BookingsController_createInstantBooking,
},
patch: {
"/v2/oauth-clients/{clientId}/users/{userId}": patch_OAuthClientUsersController_updateUser,
"/v2/oauth-clients/{clientId}": patch_OAuthClientsController_updateOAuthClient,
"/v2/event-types/{eventTypeId}": patch_EventTypesController_updateEventType,
"/v2/schedules/{scheduleId}": patch_SchedulesController_updateSchedule,
},
delete: {
"/v2/oauth-clients/{clientId}/users/{userId}": delete_OAuthClientUsersController_deleteUser,
"/v2/oauth-clients/{clientId}": delete_OAuthClientsController_deleteOAuthClient,
"/v2/event-types/{eventTypeId}": delete_EventTypesController_deleteEventType,
"/v2/schedules/{scheduleId}": delete_SchedulesController_deleteSchedule,
},
};
export type EndpointByMethod = typeof EndpointByMethod;
//
//
export type GetEndpoints = EndpointByMethod["get"];
export type PostEndpoints = EndpointByMethod["post"];
export type PatchEndpoints = EndpointByMethod["patch"];
export type DeleteEndpoints = EndpointByMethod["delete"];
export type AllEndpoints = EndpointByMethod[keyof EndpointByMethod];
//
//
export type EndpointParameters = {
body?: unknown;
query?: Record;
header?: Record;
path?: Record;
};
export type MutationMethod = "post" | "put" | "patch" | "delete";
export type Method = "get" | "head" | MutationMethod;
export type DefaultEndpoint = {
parameters?: EndpointParameters | undefined;
response: unknown;
};
export type Endpoint = {
operationId: string;
method: Method;
path: string;
parameters?: TConfig["parameters"];
meta: {
alias: string;
hasParameters: boolean;
areParametersRequired: boolean;
};
response: TConfig["response"];
};
type Fetcher = (
method: Method,
url: string,
parameters?: EndpointParameters | undefined
) => Promise;
type RequiredKeys = {
[P in keyof T]-?: undefined extends T[P] ? never : P;
}[keyof T];
type MaybeOptionalArg = RequiredKeys extends never ? [config?: T] : [config: T];
//
//
export class ApiClient {
baseUrl = "";
constructor(public fetcher: Fetcher) {}
setBaseUrl(baseUrl: string) {
this.baseUrl = baseUrl;
return this;
}
//
get(
path: Path,
...params: MaybeOptionalArg>
): Promise> {
return this.fetcher("get", this.baseUrl + path, params[0]) as Promise>;
}
//
//
post(
path: Path,
...params: MaybeOptionalArg>
): Promise> {
return this.fetcher("post", this.baseUrl + path, params[0]) as Promise>;
}
//
//
patch(
path: Path,
...params: MaybeOptionalArg>
): Promise> {
return this.fetcher("patch", this.baseUrl + path, params[0]) as Promise>;
}
//
//
delete(
path: Path,
...params: MaybeOptionalArg>
): Promise> {
return this.fetcher("delete", this.baseUrl + path, params[0]) as Promise>;
}
//
}
export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
return new ApiClient(fetcher).setBaseUrl(baseUrl ?? "");
}
/**
Example usage:
const api = createApiClient((method, url, params) =>
fetch(url, { method, body: JSON.stringify(params) }).then((res) => res.json()),
);
api.get("/users").then((users) => console.log(users));
api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
*/
// -
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOAuthClientInput'
responses:
'201':
description: Create an OAuth client
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOAuthClientResponseDto'
tags:
- OAuth - development only
get:
operationId: OAuthClientsController_getOAuthClients
summary: ''
description: >-
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetOAuthClientsResponseDto'
tags:
- OAuth - development only
/v2/oauth-clients/{clientId}:
get:
operationId: OAuthClientsController_getOAuthClientById
summary: ''
description: >-
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters:
- name: clientId
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetOAuthClientResponseDto'
tags:
- OAuth - development only
patch:
operationId: OAuthClientsController_updateOAuthClient
summary: ''
description: >-
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters:
- name: clientId
required: true
in: path
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateOAuthClientInput'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetOAuthClientResponseDto'
tags:
- OAuth - development only
delete:
operationId: OAuthClientsController_deleteOAuthClient
summary: ''
description: >-
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters:
- name: clientId
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetOAuthClientResponseDto'
tags:
- OAuth - development only
/v2/oauth-clients/{clientId}/managed-users:
get:
operationId: OAuthClientsController_getOAuthClientManagedUsersById
summary: ''
description: >-
⚠️ First, this endpoint requires `Cookie:
next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using
owner of organization that was created after visiting
`/settings/organizations/new`, refresh swagger docs, and the cookie will
be added to requests automatically to pass the NextAuthGuard.
Second, make sure that the logged in user has organizationId set to pass
the OrganizationRolesGuard guard.
parameters:
- name: clientId
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetManagedUsersOutput'
tags:
- OAuth - development only
/v2/oauth/{clientId}/authorize:
post:
operationId: OAuthFlowController_authorize
summary: Authorize an OAuth client
description: >-
Redirects the user to the specified 'redirect_uri' with an authorization
code in query parameter if the client is authorized successfully. The
code is then exchanged for access and refresh tokens via the `/exchange`
endpoint.
parameters:
- name: clientId
required: true
in: path
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthAuthorizeInput'
responses:
'200':
description: >-
The user is redirected to the 'redirect_uri' with an authorization
code in query parameter e.g. `redirectUri?code=secretcode.`
'400':
description: >-
Bad request if the OAuth client is not found, if the redirect URI is
invalid, or if the user has already authorized the client.
tags:
- OAuth - development only
/v2/oauth/{clientId}/exchange:
post:
operationId: OAuthFlowController_exchange
summary: Exchange authorization code for access tokens
description: >-
Exchanges the authorization code received from the `/authorize` endpoint
for access and refresh tokens. The authorization code should be provided
in the 'Authorization' header prefixed with 'Bearer '.
parameters:
- name: Authorization
required: true
in: header
schema:
type: string
- name: clientId
required: true
in: path
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExchangeAuthorizationCodeInput'
responses:
'200':
description: >-
Successfully exchanged authorization code for access and refresh
tokens.
content:
application/json:
schema:
$ref: '#/components/schemas/KeysResponseDto'
'400':
description: >-
Bad request if the authorization code is missing, invalid, or if the
client ID and secret do not match.
tags:
- OAuth - development only
/v2/oauth/{clientId}/refresh:
post:
operationId: OAuthFlowController_refreshAccessToken
parameters:
- name: clientId
required: true
in: path
schema:
type: string
- name: x-cal-secret-key
required: true
in: header
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshTokenInput'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/KeysResponseDto'
tags:
- OAuth - development only
/v2/event-types:
post:
operationId: EventTypesController_createEventType
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEventTypeInput'
responses:
'201':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEventTypeOutput'
tags:
- Event types
get:
operationId: EventTypesController_getEventTypes
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetEventTypesOutput'
tags:
- Event types
/v2/event-types/{eventTypeId}:
get:
operationId: EventTypesController_getEventType
parameters:
- name: eventTypeId
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetEventTypeOutput'
tags:
- Event types
patch:
operationId: EventTypesController_updateEventType
parameters:
- name: eventTypeId
required: true
in: path
schema:
type: number
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateEventTypeInput'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateEventTypeOutput'
tags:
- Event types
delete:
operationId: EventTypesController_deleteEventType
parameters:
- name: eventTypeId
required: true
in: path
schema:
type: number
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteEventTypeOutput'
tags:
- Event types
/v2/event-types/{username}/{eventSlug}/public:
get:
operationId: EventTypesController_getPublicEventType
parameters:
- name: username
required: true
in: path
schema:
type: string
- name: eventSlug
required: true
in: path
schema:
type: string
- name: isTeamEvent
required: false
in: query
schema:
type: boolean
- name: org
required: false
in: query
schema:
nullable: true
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetEventTypePublicOutput'
tags:
- Event types
/v2/event-types/{username}/public:
get:
operationId: EventTypesController_getPublicEventTypes
parameters:
- name: username
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetEventTypesPublicOutput'
tags:
- Event types
/v2/schedules:
post:
operationId: SchedulesController_createSchedule
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateScheduleInput'
responses:
'201':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/CreateScheduleOutput'
tags:
- Schedules
get:
operationId: SchedulesController_getSchedules
parameters: []
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetSchedulesOutput'
tags:
- Schedules
/v2/schedules/default:
get:
operationId: SchedulesController_getDefaultSchedule
parameters: []
responses:
'200':
description: Returns the default schedule
content:
application/json:
schema:
$ref: '#/components/schemas/GetDefaultScheduleOutput'
tags:
- Schedules
/v2/schedules/{scheduleId}:
get:
operationId: SchedulesController_getSchedule
parameters:
- name: scheduleId
required: true
in: path
schema:
type: number
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetScheduleOutput'
tags:
- Schedules
patch:
operationId: SchedulesController_updateSchedule
parameters:
- name: scheduleId
required: true
in: path
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateScheduleInput'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateScheduleOutput'
tags:
- Schedules
delete:
operationId: SchedulesController_deleteSchedule
parameters:
- name: scheduleId
required: true
in: path
schema:
type: number
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/DeleteScheduleOutput'
tags:
- Schedules
/v2/bookings:
get:
operationId: BookingsController_getBookings
parameters:
- name: cursor
required: false
in: query
schema:
type: number
- name: limit
required: false
in: query
schema:
type: number
- name: filters[status]
required: true
in: query
schema:
enum:
- upcoming
- recurring
- past
- cancelled
- unconfirmed
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetBookingsOutput'
tags:
- Bookings
post:
operationId: BookingsController_createBooking
parameters:
- name: x-cal-client-id
required: true
in: header
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateBookingInput'
responses:
'201':
description: ''
content:
application/json:
schema:
type: object
tags:
- Bookings
/v2/bookings/{bookingUid}:
get:
operationId: BookingsController_getBooking
parameters:
- name: bookingUid
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GetBookingOutput'
tags:
- Bookings
/v2/bookings/{bookingUid}/reschedule:
get:
operationId: BookingsController_getBookingForReschedule
parameters:
- name: bookingUid
required: true
in: path
schema:
type: string
responses:
'200':
description: ''
content:
application/json:
schema:
type: object
tags:
- Bookings
/v2/bookings/{bookingId}/cancel:
post:
operationId: BookingsController_cancelBooking
parameters:
- name: bookingId
required: true
in: path
schema:
type: string
- name: x-cal-client-id
required: true
in: header
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CancelBookingInput'
responses:
'201':
description: ''
content:
application/json:
schema:
type: object
tags:
- Bookings
/v2/bookings/recurring:
post:
operationId: BookingsController_createRecurringBooking
parameters:
- name: x-cal-client-id
required: true
in: header
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
responses:
'201':
description: ''
content:
application/json:
schema:
type: object
tags:
- Bookings
/v2/bookings/instant:
post:
operationId: BookingsController_createInstantBooking
parameters:
- name: x-cal-client-id
required: true
in: header
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateBookingInput'
responses:
'201':
description: ''
content:
application/json:
schema:
type: object
tags:
- Bookings
info:
title: Cal.com v2 API
description: ''
version: 1.0.0
contact: {}
tags: []
servers: []
components:
schemas:
ManagedUserOutput:
type: object
properties:
id:
type: number
example: 1
email:
type: string
example: alice+cluo37fwd0001khkzqqynkpj3@example.com
username:
type: string
nullable: true
example: alice
timeZone:
type: string
example: America/New_York
weekStart:
type: string
example: Sunday
createdDate:
type: string
example: '2024-04-01T00:00:00.000Z'
timeFormat:
type: number
nullable: true
example: 12
defaultScheduleId:
type: number
nullable: true
example: null
required:
- id
- email
- username
- timeZone
- weekStart
- createdDate
- timeFormat
- defaultScheduleId
GetManagedUsersOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
type: array
items:
$ref: '#/components/schemas/ManagedUserOutput'
required:
- status
- data
CreateManagedUserInput:
type: object
properties:
email:
type: string
example: alice@example.com
timeFormat:
type: number
example: 12
enum:
- 12
- 24
description: Must be 12 or 24
weekStart:
type: string
example: Monday
enum:
- Monday
- Tuesday
- Wednesday
- Thursday
- Friday
- Saturday
- Sunday
timeZone:
type: string
example: America/New_York
name:
type: string
required:
- email
CreateManagedUserData:
type: object
properties:
user:
$ref: '#/components/schemas/ManagedUserOutput'
accessToken:
type: string
refreshToken:
type: string
required:
- user
- accessToken
- refreshToken
CreateManagedUserOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/CreateManagedUserData'
required:
- status
- data
GetManagedUserOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/ManagedUserOutput'
required:
- status
- data
UpdateManagedUserInput:
type: object
properties:
timeFormat:
type: number
example: 12
enum:
- 12
- 24
description: Must be 12 or 24
weekStart:
type: string
example: Monday
enum:
- Monday
- Tuesday
- Wednesday
- Thursday
- Friday
- Saturday
- Sunday
email:
type: string
name:
type: string
defaultScheduleId:
type: number
timeZone:
type: string
KeysDto:
type: object
properties:
accessToken:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
refreshToken:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
required:
- accessToken
- refreshToken
KeysResponseDto:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/KeysDto'
required:
- status
- data
CreateOAuthClientInput:
type: object
properties: {}
DataDto:
type: object
properties:
clientId:
type: string
example: clsx38nbl0001vkhlwin9fmt0
clientSecret:
type: string
example: >-
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi
required:
- clientId
- clientSecret
CreateOAuthClientResponseDto:
type: object
properties:
status:
type: string
enum:
- success
- error
example: success
data:
example:
clientId: clsx38nbl0001vkhlwin9fmt0
clientSecret: >-
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi
allOf:
- $ref: '#/components/schemas/DataDto'
required:
- status
- data
PlatformOAuthClientDto:
type: object
properties:
id:
type: string
example: clsx38nbl0001vkhlwin9fmt0
name:
type: string
example: MyClient
secret:
type: string
example: secretValue
permissions:
type: number
example: 3
logo:
type: string
nullable: true
example: https://example.com/logo.png
redirectUris:
example:
- https://example.com/callback
type: array
items:
type: string
organizationId:
type: number
example: 1
createdAt:
format: date-time
type: string
example: '2024-03-23T08:33:21.851Z'
required:
- id
- name
- secret
- permissions
- redirectUris
- organizationId
- createdAt
GetOAuthClientsResponseDto:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
type: array
items:
$ref: '#/components/schemas/PlatformOAuthClientDto'
required:
- status
- data
GetOAuthClientResponseDto:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/PlatformOAuthClientDto'
required:
- status
- data
UpdateOAuthClientInput:
type: object
properties:
logo:
type: string
name:
type: string
redirectUris:
default: []
type: array
items:
type: string
bookingRedirectUri:
type: string
bookingCancelRedirectUri:
type: string
bookingRescheduleRedirectUri:
type: string
areEmailsEnabled:
type: boolean
OAuthAuthorizeInput:
type: object
properties:
redirectUri:
type: string
required:
- redirectUri
ExchangeAuthorizationCodeInput:
type: object
properties:
clientSecret:
type: string
required:
- clientSecret
RefreshTokenInput:
type: object
properties:
refreshToken:
type: string
required:
- refreshToken
EventTypeLocation:
type: object
properties:
type:
type: string
example: link
link:
type: string
example: https://masterchief.com/argentina/flan/video/9129412
required:
- type
CreateEventTypeInput:
type: object
properties:
length:
type: number
minimum: 1
example: 60
slug:
type: string
example: cooking-class
title:
type: string
example: Learn the secrets of masterchief!
description:
type: string
example: >-
Discover the culinary wonders of the Argentina by making the best
flan ever!
locations:
type: array
items:
$ref: '#/components/schemas/EventTypeLocation'
disableGuests:
type: boolean
required:
- length
- slug
- title
EventTypeOutput:
type: object
properties:
id:
type: number
example: 1
length:
type: number
example: 60
slug:
type: string
example: cooking-class
title:
type: string
example: Learn the secrets of masterchief!
description:
type: string
nullable: true
example: >-
Discover the culinary wonders of the Argentina by making the best
flan ever!
locations:
nullable: true
type: array
items:
$ref: '#/components/schemas/EventTypeLocation'
required:
- id
- length
- slug
- title
- description
- locations
CreateEventTypeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/EventTypeOutput'
required:
- status
- data
Data:
type: object
properties:
eventType:
$ref: '#/components/schemas/EventTypeOutput'
required:
- eventType
GetEventTypeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/Data'
required:
- status
- data
EventTypeGroup:
type: object
properties:
eventTypes:
type: array
items:
$ref: '#/components/schemas/EventTypeOutput'
required:
- eventTypes
GetEventTypesData:
type: object
properties:
eventTypeGroups:
type: array
items:
$ref: '#/components/schemas/EventTypeGroup'
required:
- eventTypeGroups
GetEventTypesOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/GetEventTypesData'
required:
- status
- data
Location:
type: object
properties:
type:
type: string
required:
- type
Source:
type: object
properties:
id:
type: string
type:
type: string
label:
type: string
required:
- id
- type
- label
BookingField:
type: object
properties:
name:
type: string
type:
type: string
defaultLabel:
type: string
label:
type: string
placeholder:
type: string
required:
type: boolean
getOptionsAt:
type: string
hideWhenJustOneOption:
type: boolean
editable:
type: string
sources:
type: array
items:
$ref: '#/components/schemas/Source'
required:
- name
- type
Organization:
type: object
properties:
id:
type: number
slug:
type: string
nullable: true
name:
type: string
metadata:
type: object
required:
- id
- name
- metadata
Profile:
type: object
properties:
username:
type: string
nullable: true
id:
type: number
nullable: true
userId:
type: number
uid:
type: string
name:
type: string
organizationId:
type: number
nullable: true
organization:
nullable: true
allOf:
- $ref: '#/components/schemas/Organization'
upId:
type: string
image:
type: string
brandColor:
type: string
darkBrandColor:
type: string
theme:
type: string
bookerLayouts:
type: object
required:
- username
- id
- organizationId
- upId
Owner:
type: object
properties:
id:
type: number
avatarUrl:
type: string
nullable: true
username:
type: string
nullable: true
name:
type: string
nullable: true
weekStart:
type: string
brandColor:
type: string
nullable: true
darkBrandColor:
type: string
nullable: true
theme:
type: string
nullable: true
metadata:
type: object
defaultScheduleId:
type: number
nullable: true
nonProfileUsername:
type: string
nullable: true
profile:
$ref: '#/components/schemas/Profile'
required:
- id
- username
- name
- weekStart
- metadata
- nonProfileUsername
- profile
Schedule:
type: object
properties:
id:
type: number
timeZone:
type: string
nullable: true
required:
- id
- timeZone
User:
type: object
properties:
username:
type: string
nullable: true
name:
type: string
nullable: true
weekStart:
type: string
organizationId:
type: number
avatarUrl:
type: string
nullable: true
profile:
$ref: '#/components/schemas/Profile'
bookerUrl:
type: string
required:
- username
- name
- weekStart
- profile
- bookerUrl
PublicEventTypeOutput:
type: object
properties:
id:
type: number
title:
type: string
description:
type: string
eventName:
type: string
nullable: true
slug:
type: string
isInstantEvent:
type: boolean
aiPhoneCallConfig:
type: object
schedulingType:
type: object
length:
type: number
locations:
type: array
items:
$ref: '#/components/schemas/Location'
customInputs:
type: array
items:
type: object
disableGuests:
type: boolean
metadata:
type: object
nullable: true
lockTimeZoneToggleOnBookingPage:
type: boolean
requiresConfirmation:
type: boolean
requiresBookerEmailVerification:
type: boolean
recurringEvent:
type: object
price:
type: number
currency:
type: string
seatsPerTimeSlot:
type: number
nullable: true
seatsShowAvailabilityCount:
type: boolean
nullable: true
bookingFields:
type: array
items:
$ref: '#/components/schemas/BookingField'
team:
type: object
successRedirectUrl:
type: string
nullable: true
workflows:
type: array
items:
type: object
hosts:
type: array
items:
type: object
owner:
nullable: true
allOf:
- $ref: '#/components/schemas/Owner'
schedule:
nullable: true
allOf:
- $ref: '#/components/schemas/Schedule'
hidden:
type: boolean
assignAllTeamMembers:
type: boolean
bookerLayouts:
type: object
users:
type: array
items:
$ref: '#/components/schemas/User'
entity:
type: object
isDynamic:
type: boolean
required:
- id
- title
- description
- slug
- isInstantEvent
- length
- locations
- customInputs
- disableGuests
- metadata
- lockTimeZoneToggleOnBookingPage
- requiresConfirmation
- requiresBookerEmailVerification
- price
- currency
- seatsShowAvailabilityCount
- bookingFields
- workflows
- hosts
- owner
- schedule
- hidden
- assignAllTeamMembers
- users
- entity
- isDynamic
GetEventTypePublicOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
nullable: true
allOf:
- $ref: '#/components/schemas/PublicEventTypeOutput'
required:
- status
- data
PublicEventType:
type: object
properties:
id:
type: number
example: 1
length:
type: number
example: 60
slug:
type: string
example: cooking-class
title:
type: string
example: Learn the secrets of masterchief!
description:
type: string
nullable: true
required:
- id
- length
- slug
- title
GetEventTypesPublicOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
type: array
items:
$ref: '#/components/schemas/PublicEventType'
required:
- status
- data
UpdateEventTypeInput:
type: object
properties:
length:
type: number
minimum: 1
slug:
type: string
title:
type: string
description:
type: string
hidden:
type: boolean
locations:
type: array
items:
$ref: '#/components/schemas/EventTypeLocation'
disableGuests:
type: boolean
UpdateEventTypeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/EventTypeOutput'
required:
- status
- data
DeleteData:
type: object
properties:
id:
type: number
example: 1
length:
type: number
example: 60
slug:
type: string
example: cooking-class
title:
type: string
example: Learn the secrets of masterchief!
required:
- id
- length
- slug
- title
DeleteEventTypeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/DeleteData'
required:
- status
- data
CreateAvailabilityInput:
type: object
properties:
days:
example:
- 1
- 2
type: array
items:
type: number
startTime:
format: date-time
type: string
endTime:
format: date-time
type: string
required:
- days
- startTime
- endTime
CreateScheduleInput:
type: object
properties:
name:
type: string
timeZone:
type: string
availabilities:
type: array
items:
$ref: '#/components/schemas/CreateAvailabilityInput'
isDefault:
type: boolean
required:
- name
- timeZone
- isDefault
WorkingHours:
type: object
properties:
days:
type: array
items:
type: number
startTime:
type: number
endTime:
type: number
userId:
type: number
nullable: true
required:
- days
- startTime
- endTime
AvailabilityModel:
type: object
properties:
id:
type: number
userId:
type: number
nullable: true
eventTypeId:
type: number
nullable: true
days:
type: array
items:
type: number
startTime:
format: date-time
type: string
endTime:
format: date-time
type: string
date:
format: date-time
type: string
nullable: true
scheduleId:
type: number
nullable: true
required:
- id
- days
- startTime
- endTime
TimeRange:
type: object
properties:
userId:
type: number
nullable: true
start:
format: date-time
type: string
end:
format: date-time
type: string
required:
- start
- end
ScheduleOutput:
type: object
properties:
id:
type: number
name:
type: string
isManaged:
type: boolean
workingHours:
type: array
items:
$ref: '#/components/schemas/WorkingHours'
schedule:
type: array
items:
$ref: '#/components/schemas/AvailabilityModel'
availability:
type: array
items:
required: true
type: array
items:
$ref: '#/components/schemas/TimeRange'
timeZone:
type: string
dateOverrides:
type: array
items:
type: object
isDefault:
type: boolean
isLastSchedule:
type: boolean
readOnly:
type: boolean
required:
- id
- name
- isManaged
- workingHours
- schedule
- availability
- timeZone
- dateOverrides
- isDefault
- isLastSchedule
- readOnly
CreateScheduleOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/ScheduleOutput'
required:
- status
- data
GetDefaultScheduleOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
nullable: true
allOf:
- $ref: '#/components/schemas/ScheduleOutput'
required:
- status
- data
GetScheduleOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/ScheduleOutput'
required:
- status
- data
GetSchedulesOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/ScheduleOutput'
required:
- status
- data
UpdateScheduleInput:
type: object
properties:
timeZone:
type: string
name:
type: string
isDefault:
type: boolean
schedule:
example:
- []
- - start: '2022-01-01T00:00:00.000Z'
end: '2022-01-02T00:00:00.000Z'
- []
- []
- []
- []
- []
items:
type: array
type: array
dateOverrides:
example:
- []
- - start: '2022-01-01T00:00:00.000Z'
end: '2022-01-02T00:00:00.000Z'
- []
- []
- []
- []
- []
items:
type: array
type: array
required:
- timeZone
- name
- isDefault
- schedule
EventTypeModel:
type: object
properties:
id:
type: number
eventName:
type: string
nullable: true
required:
- id
ScheduleModel:
type: object
properties:
id:
type: number
userId:
type: number
name:
type: string
timeZone:
type: string
nullable: true
eventType:
type: array
items:
$ref: '#/components/schemas/EventTypeModel'
availability:
type: array
items:
$ref: '#/components/schemas/AvailabilityModel'
required:
- id
- userId
- name
UpdatedScheduleOutput:
type: object
properties:
schedule:
$ref: '#/components/schemas/ScheduleModel'
isDefault:
type: boolean
timeZone:
type: string
prevDefaultId:
type: number
nullable: true
currentDefaultId:
type: number
nullable: true
required:
- schedule
- isDefault
UpdateScheduleOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/UpdatedScheduleOutput'
required:
- status
- data
DeleteScheduleOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
required:
- status
AuthUrlData:
type: object
properties:
authUrl:
type: string
required:
- authUrl
GcalAuthUrlOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/AuthUrlData'
required:
- status
- data
GcalSaveRedirectOutput:
type: object
properties:
url:
type: string
required:
- url
GcalCheckOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
required:
- status
ProviderVerifyClientOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
required:
- status
ProviderVerifyAccessTokenOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
required:
- status
MeOutput:
type: object
properties:
id:
type: number
username:
type: string
email:
type: string
timeFormat:
type: number
defaultScheduleId:
type: number
nullable: true
weekStart:
type: string
timeZone:
type: string
required:
- id
- username
- email
- timeFormat
- defaultScheduleId
- weekStart
- timeZone
GetMeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/MeOutput'
required:
- status
- data
UpdateMeOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/MeOutput'
required:
- status
- data
BusyTimesOutput:
type: object
properties:
start:
format: date-time
type: string
end:
format: date-time
type: string
source:
type: string
nullable: true
required:
- start
- end
GetBusyTimesOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
type: array
items:
$ref: '#/components/schemas/BusyTimesOutput'
required:
- status
- data
Integration:
type: object
properties:
appData:
type: object
nullable: true
dirName:
type: string
__template:
type: string
name:
type: string
description:
type: string
installed:
type: boolean
type:
type: string
title:
type: string
variant:
type: string
category:
type: string
categories:
type: array
items:
type: string
logo:
type: string
publisher:
type: string
slug:
type: string
url:
type: string
email:
type: string
locationOption:
type: object
nullable: true
required:
- name
- description
- type
- variant
- categories
- logo
- publisher
- slug
- url
- email
- locationOption
Primary:
type: object
properties:
externalId:
type: string
integration:
type: string
name:
type: string
primary:
type: boolean
nullable: true
readOnly:
type: boolean
email:
type: string
isSelected:
type: boolean
credentialId:
type: number
required:
- externalId
- primary
- readOnly
- isSelected
- credentialId
Calendar:
type: object
properties:
externalId:
type: string
integration:
type: string
name:
type: string
primary:
type: boolean
nullable: true
readOnly:
type: boolean
email:
type: string
isSelected:
type: boolean
credentialId:
type: number
required:
- externalId
- readOnly
- isSelected
- credentialId
ConnectedCalendar:
type: object
properties:
integration:
$ref: '#/components/schemas/Integration'
credentialId:
type: number
primary:
$ref: '#/components/schemas/Primary'
calendars:
type: array
items:
$ref: '#/components/schemas/Calendar'
required:
- integration
- credentialId
DestinationCalendar:
type: object
properties:
id:
type: number
integration:
type: string
externalId:
type: string
primaryEmail:
type: string
nullable: true
userId:
type: number
nullable: true
eventTypeId:
type: number
nullable: true
credentialId:
type: number
nullable: true
name:
type: string
nullable: true
primary:
type: boolean
readOnly:
type: boolean
email:
type: string
integrationTitle:
type: string
required:
- id
- integration
- externalId
- primaryEmail
- userId
- eventTypeId
- credentialId
ConnectedCalendarsData:
type: object
properties:
connectedCalendars:
type: array
items:
$ref: '#/components/schemas/ConnectedCalendar'
destinationCalendar:
$ref: '#/components/schemas/DestinationCalendar'
required:
- connectedCalendars
- destinationCalendar
ConnectedCalendarsOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/ConnectedCalendarsData'
required:
- status
- data
Attendee:
type: object
properties:
id:
type: number
email:
type: string
name:
type: string
timeZone:
type: string
locale:
type: string
nullable: true
bookingId:
type: number
nullable: true
required:
- id
- email
- name
- timeZone
- locale
- bookingId
EventType:
type: object
properties:
slug:
type: string
id:
type: number
eventName:
type: string
nullable: true
price:
type: number
recurringEvent:
type: object
currency:
type: string
metadata:
type: object
seatsShowAttendees:
type: object
seatsShowAvailabilityCount:
type: object
team:
type: object
nullable: true
required:
- price
- currency
- metadata
Reference:
type: object
properties:
id:
type: number
type:
type: string
uid:
type: string
meetingId:
type: string
nullable: true
thirdPartyRecurringEventId:
type: string
nullable: true
meetingPassword:
type: string
nullable: true
meetingUrl:
type: string
nullable: true
bookingId:
type: number
nullable: true
externalCalendarId:
type: string
nullable: true
deleted:
type: object
credentialId:
type: number
nullable: true
required:
- id
- type
- uid
- meetingPassword
- bookingId
- externalCalendarId
- credentialId
GetBookingsDataEntry:
type: object
properties:
id:
type: number
title:
type: string
userPrimaryEmail:
type: string
nullable: true
description:
type: string
nullable: true
customInputs:
type: object
startTime:
type: string
endTime:
type: string
attendees:
type: array
items:
$ref: '#/components/schemas/Attendee'
metadata:
type: object
uid:
type: string
recurringEventId:
type: string
nullable: true
location:
type: string
nullable: true
eventType:
$ref: '#/components/schemas/EventType'
status:
type: object
paid:
type: boolean
payment:
type: array
items:
type: object
references:
type: array
items:
$ref: '#/components/schemas/Reference'
isRecorded:
type: boolean
seatsReferences:
type: array
items:
type: object
user:
nullable: true
allOf:
- $ref: '#/components/schemas/User'
rescheduled:
type: object
required:
- id
- title
- description
- customInputs
- startTime
- endTime
- attendees
- metadata
- uid
- recurringEventId
- location
- eventType
- status
- paid
- payment
- references
- isRecorded
- seatsReferences
- user
GetBookingsData:
type: object
properties:
bookings:
type: array
items:
$ref: '#/components/schemas/GetBookingsDataEntry'
recurringInfo:
type: array
items:
type: object
nextCursor:
type: number
nullable: true
required:
- bookings
- recurringInfo
- nextCursor
GetBookingsOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/GetBookingsData'
required:
- status
- data
GetBookingData:
type: object
properties:
title:
type: string
id:
type: number
uid:
type: string
description:
type: string
nullable: true
customInputs:
type: object
smsReminderNumber:
type: string
nullable: true
recurringEventId:
type: string
nullable: true
startTime:
format: date-time
type: string
endTime:
format: date-time
type: string
location:
type: string
nullable: true
status:
type: string
metadata:
type: object
cancellationReason:
type: string
nullable: true
responses:
type: object
rejectionReason:
type: string
nullable: true
userPrimaryEmail:
type: string
nullable: true
user:
nullable: true
allOf:
- $ref: '#/components/schemas/User'
attendees:
type: array
items:
$ref: '#/components/schemas/Attendee'
eventTypeId:
type: number
nullable: true
eventType:
nullable: true
allOf:
- $ref: '#/components/schemas/EventType'
required:
- title
- id
- uid
- description
- customInputs
- smsReminderNumber
- recurringEventId
- startTime
- endTime
- location
- status
- metadata
- cancellationReason
- responses
- rejectionReason
- userPrimaryEmail
- user
- attendees
- eventTypeId
- eventType
GetBookingOutput:
type: object
properties:
status:
type: string
example: success
enum:
- success
- error
data:
$ref: '#/components/schemas/GetBookingData'
required:
- status
- data
Response:
type: object
properties:
name:
type: string
email:
type: string
guests:
type: array
items:
type: string
location:
$ref: '#/components/schemas/Location'
notes:
type: string
required:
- name
- email
- guests
CreateBookingInput:
type: object
properties:
end:
type: string
start:
type: string
eventTypeId:
type: number
eventTypeSlug:
type: string
rescheduleUid:
type: string
recurringEventId:
type: string
timeZone:
type: string
user:
type: array
items:
type: string
language:
type: string
bookingUid:
type: string
metadata:
type: object
hasHashedBookingLink:
type: boolean
hashedLink:
type: string
nullable: true
seatReferenceUid:
type: string
responses:
$ref: '#/components/schemas/Response'
orgSlug:
type: string
locationUrl:
type: string
required:
- start
- eventTypeId
- timeZone
- language
- metadata
- hashedLink
- responses
CancelBookingInput:
type: object
properties:
id:
type: number
uid:
type: string
allRemainingBookings:
type: boolean
cancellationReason:
type: string
seatReferenceUid:
type: string
required:
- id
- uid
- allRemainingBookings
- cancellationReason
- seatReferenceUid
ReserveSlotInput:
type: object
properties: {}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/cal/api.ts
================================================
import {
KeysResponseDto,
createApiClient,
RefreshTokenInput,
ManagedUserOutput,
type CreateManagedUserInput,
type CreateManagedUserOutput,
} from "./__generated/cal-sdk";
import { isCalSandbox } from "./utils";
import { env } from "@/env";
import { type Prisma, type User } from "@prisma/client";
import { unstable_noStore } from "next/cache";
import { db } from "prisma/client";
import "server-only";
import { type z } from "zod";
const calApiUrl = new URL(env.NEXT_PUBLIC_CAL_API_URL);
const baseUrl = `${calApiUrl.protocol}//${calApiUrl.hostname}`;
// create a class instance of the Cal SDK that requires a user id as an input & returns the createApiClient's return:
type SDKInput = Pick & Partial>;
export const cal = (input: { user: SDKInput }) =>
createApiClient(async (method, url, params) => {
unstable_noStore(); // TODO: whenever we upgrade to next15, replace this with `unstable_rethrow` to support react's throwing mechanism under the hood @link: https://nextjs.org/docs/messages/ppr-caught-error
const fullUrl = new URL(url);
// the pathname contains variables that need to be replaced with our params.path (if defined). e.g. `v2/oauth-clients/%7BclientId%7D/users` should be rpelaced with the params.path["clientId"]:
if (params?.path && Object.keys(params.path).length > 0)
fullUrl.pathname = fullUrl.pathname.replace(
// a regex, that takes the key inside `%7B` and `%7D` and replaces this with the value from the params.path object
/%7B([^%7D]+)%7D/g,
(match, key) => params.path?.[key as keyof typeof params.path] as string
);
try {
if (!input.user.id) {
console.warn(`[Cal SDK] Unable to use the Cal API: No user is authenticated'`);
throw new Error(`[Cal SDK] Unauthorized`);
}
// instantiate with 'let' so that we can re-assign this variable after a potential retry with a refreshed token (syncs the dbUser)
let dbUser: SDKInput | null = input.user;
if (!dbUser.calAccessToken || !dbUser.calRefreshToken || !dbUser.calAccountId) {
dbUser = await db.user.findUnique({
where: { id: input.user.id },
select: { id: true, calAccessToken: true, calAccountId: true, calRefreshToken: true },
});
}
if (!dbUser) {
console.warn(
`[Cal SDK] Unable to use the Cal API: No user found in the database with id '${input.user.id}'.`
);
throw new Error(`[Cal SDK] Unauthorized`);
}
if (!dbUser.calAccessToken && !isAdminEndpoint(fullUrl.pathname)) {
console.warn(
`[Cal SDK] Unable to use the Cal API on a non-admin endpoint ('${fullUrl.pathname}'): The user ${dbUser.id} has no access token for Cal set.`
);
throw new Error(`[Cal SDK] Unauthorized`);
}
/*
* [Validation] End.
**/
// compose the fetch parameters in a variable to re-use later (for logs & retries)
// @ts-expect-error - query has `unknown` values, but we're in a try/catch to handle that case
if (params?.query) fullUrl.search = new URLSearchParams(params.query).toString();
const options = {
headers: {
...calHeaders,
Authorization: `Bearer ${dbUser.calAccessToken}`,
},
method,
body: JSON.stringify(params?.body),
cache: "no-store",
} satisfies RequestInit;
const fetchParameters = [fullUrl.href, options] satisfies Parameters;
// instantiate response variables (assign with `let` so that we can re-assign after retries)
unstable_noStore(); // TODO: whenever we upgrade to next15, replace this with `unstable_rethrow` to support react's throwing mechanism under the hood @link: https://nextjs.org/docs/messages/ppr-caught-error
let res = await fetch(fetchParameters[0], fetchParameters[1]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let json = await res.json();
// [@calcom] This means the token has expired, we refresh it and retry the request (re-assigns to res & json)
if (res.status === 498) {
if (!dbUser.calRefreshToken || !dbUser.calAccountId) {
console.warn(
`[Cal SDK] Unable to use the Cal API: The user ${dbUser.id} has no refresh token for Cal set and the access token has expired.`
);
throw new Error(`[Cal SDK] Unauthorized`);
}
const refreshFlowData = await refreshTokens({
refreshToken: dbUser.calRefreshToken,
calAccountId: dbUser.calAccountId,
});
// if we were unable to refresh the tokens, we throw an error
if (refreshFlowData.status === "error") {
console.error(
`[Cal SDK] Unable to fetch the Cal API: Entire refresh flow for user with refreshToken '${dbUser.calRefreshToken}' (user id: '${dbUser.id}') failed.`
);
throw new Error(`[Cal SDK] Application Error`);
}
// with fresh tokens, let's update the database & retry the request in parallel:
const [_updatedUser, retryRes] = await updateDbAndRetryFetch({
fetch: fetchParameters,
update: {
where: { id: dbUser.id },
data: {
calAccessToken: refreshFlowData.data.accessToken,
calRefreshToken: refreshFlowData.data.refreshToken,
},
},
});
// if the retry with fresh tokens didn't work, we log the fetch debug info (not throwing, so that we return the error to the application)
if (!retryRes.res.ok) {
console.error(
`[Cal SDK] Unable to fetch cal api on endpoint '${fullUrl.pathname}' after the refreshFlow: Invalid response from Cal with fresh tokens.`
);
}
// re-assign the variables so that the outer closure can use it, we're done with 498 handling
dbUser = _updatedUser;
res = retryRes.res;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
json = retryRes.json;
}
// [@calcom] This means the used token has been rotated, but the token we used is outdated (ie our DB *could* be out-of-sync), let's force-refresh:
if (res.status === 403) {
if (!dbUser.calAccountId) {
console.warn(
`[Cal SDK] Unable to use the Cal API: The user ${dbUser.id} has no refresh token for Cal set and the access token has expired.`
);
throw new Error(`[Cal SDK] Unauthorized`);
}
const forceRefreshData = await forceRefresh({ calAccountId: dbUser.calAccountId });
// if the force-refresh doesn't work, we have to throw an error, it's a dead end
if (forceRefreshData.status === "error") throw new Error(`[Cal SDK] Application Error`);
const [_updatedUser, retryRes] = await updateDbAndRetryFetch({
fetch: [fullUrl, options],
update: {
where: { id: dbUser.id },
data: {
calAccessToken: forceRefreshData.data.accessToken,
calRefreshToken: forceRefreshData.data.refreshToken,
},
},
});
// if the retry with fresh tokens didn't work, we log the fetch debug info (not throwing, so that we return the error to the application)
if (!retryRes.res.ok) {
console.error(
`
[Cal SDK] Unable to fetch cal api on endpoint '${fullUrl.pathname}' after the forceRefresh: Invalid response from Cal with fresh tokens.
`
);
}
// re-assign the variables so that the outer closure can use it, we're done with 403 handling
dbUser = _updatedUser;
res = retryRes.res;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
json = retryRes.json;
}
// apart from 498 & 403 (requires token rotation), we return all other responses as-is to show the error to the application consumer and/or end-user
// but first, let's log the initial request if it failed
if (!res.ok) {
console.warn(
`[Cal SDK] Unable to fetch cal api on endpoint '${fullUrl.pathname}': Invalid response from Cal.
${composeFetchLogs({ fetch: fetchParameters, res, json, ...(dbUser.calAccountId && { cal: { id: dbUser.calAccountId } }) })}
`
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return json;
} catch (e) {
console.error("[Cal SDK] Unknown error encountered: ", e);
throw e;
}
}, baseUrl);
/**
* =================
* TOKEN REFRESH
* =================
*/
export const KeysSuccessDto = KeysResponseDto.shape.data;
type KeysSuccessData = z.infer;
export const refreshTokens = async (
input: z.infer & { calAccountId: User["calAccountId"] }
) => {
const inputValidation = RefreshTokenInput.extend({
calAccountId: ManagedUserOutput.shape.id,
}).safeParse(input);
if (!inputValidation.success) {
console.error(
`[Cal SDK] Invalid input provided to refreshTokens: ${inputValidation.error.errors.map((e) => e.message).join(", ")}`
);
return { status: "error" } as { status: "error" };
}
// first attempt is to use the /refresh endpoint on `/oauth/`:
const url = new URL(calApiUrl);
url.pathname = `/v2/oauth/${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}/refresh`;
const fetchParameters = [
url.href,
{
method: "POST",
headers: calHeaders,
cache: "no-store",
body: JSON.stringify({ refreshToken: input.refreshToken } satisfies RefreshTokenInput),
},
] satisfies Parameters;
unstable_noStore(); // TODO: whenever we upgrade to next15, replace this with `unstable_rethrow` to support react's throwing mechanism under the hood @link: https://nextjs.org/docs/messages/ppr-caught-error
const response = await fetch(fetchParameters[0], fetchParameters[1]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const json = await response.json();
// early return if that worked
if (response.ok) {
console.info(`[Cal SDK] 🔁 Refreshed the token for user with refreshToken '${input.refreshToken}'`);
return json as { status: "success"; data: KeysSuccessData };
}
// log for debugging if that didn't work:
console.warn(`
[Cal SDK] Unable to refresh the user token for user with refreshToken '${input.refreshToken}': Invalid response from Cal after attempting to refresh the token.
Resorting to force-refresh now.
${composeFetchLogs({ fetch: fetchParameters, res: response, json, cal: { refreshToken: input.refreshToken } })}
`);
// second attempt is to use the /force-refresh endpoint on `/oauth-clients/`. We need the accountId for this:
return forceRefresh({ calAccountId: input.calAccountId });
};
export const forceRefresh = async (input: { calAccountId: User["calAccountId"] }) => {
const url = new URL(calApiUrl);
url.pathname = `/v2/oauth-clients/${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}/users/${input.calAccountId}/force-refresh`;
const fetchParameters = [
url.href,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": env.CAL_SECRET,
origin: "http://localhost:3000",
},
},
] satisfies Parameters;
const response = await fetch(fetchParameters[0], fetchParameters[1]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const json = await response.json();
// if this doesn't work, log for debugging:
if (!response.ok) {
console.warn(
`[Cal SDK] Unable to force-refresh the user token for user with calAccountId '${input.calAccountId}': Invalid response from Cal after attempting to refresh the token.
${composeFetchLogs({ fetch: fetchParameters, res: response, json, cal: { id: input.calAccountId } })}
`
);
return { status: "error" } as { status: "error" };
}
// response.ok === true
console.info(`[Cal SDK] 🔁 Force-refreshed the token for user with calAccountId '${input.calAccountId}'`);
return json as Promise<{ status: "success"; data: KeysSuccessData }>;
};
export const updateDbAndRetryFetch = async (input: {
fetch: FetchParametersLike;
update: Pick;
}) => {
// with fresh tokens, let's update the database & retry the request in parallel:
const inputUrl = input.fetch[0];
const retryUrl = (typeof inputUrl === "string" ? new URL(inputUrl) : inputUrl) satisfies URL;
// overwrite the previous headers with the new access token
const retryOptions = {
...input.fetch[1],
headers: {
...input.fetch[1].headers,
Authorization: `Bearer ${input.update.data.calAccessToken as string}`,
},
} satisfies RequestInit;
const [updatedUser, retryRes] = await Promise.all([
db.user.update({
where: input.update.where,
data: input.update.data,
}),
fetch(retryUrl.href, retryOptions),
]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const retryJson = await retryRes.json();
// if the retry with fresh tokens didn't work, we log the fetch debug info (not throwing, so that we return the error to the application)
if (!retryRes.ok) {
console.error(
`
[Cal SDK] Unable to fetch cal api on endpoint '${retryUrl.pathname}' after the forceRefresh: Invalid response from Cal with fresh tokens.
${composeFetchLogs({ fetch: [retryUrl.href, retryOptions], res: retryRes, json: retryJson, ...(updatedUser.calAccountId && { cal: { id: updatedUser.calAccountId } }) })}
`
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return [updatedUser, { res: retryRes, json: retryJson }] as const;
};
/**
* =================
* USER MANAGEMENT
* =================
*/
export const createUser = async (input: CreateManagedUserInput) => {
const url = new URL(calApiUrl);
url.pathname = `/v2/oauth-clients/${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}/users`;
const fetchParameters = [
url.href,
{
method: "POST",
headers: calHeaders,
body: JSON.stringify(input),
},
] satisfies Parameters;
const response = await fetch(fetchParameters[0], fetchParameters[1]);
// if the initial response fails, there is a possibility that we attempted to create a duplicate user -- let's handle this
if (!response.ok) {
const text = await response.text();
// if the response is bad and anything but a duplicate response, we log for debugging & throw
if (!text.includes("already exists")) {
console.error(
`[Cal SDK] Unable to create Managed User '${input.email.slice(0, 4)}*****': Invalid response from Cal after attempting to create the user.
${composeFetchLogs({ fetch: fetchParameters, res: response, json: text })}
`
);
// TODO: should we return cal's error message here?
return { status: "error" } as { status: "error" };
}
// otherwise, it means that Cal already has this user signed up
if (isCalSandbox) {
// in the sandbox, we have to return an error as we there are potentially multiple apps using the same Cal OAuth sandbox client, have the user create a unique email:
console.warn(`
[Cal SDK] Unable to create Managed User '${input.email.slice(0, 4)}*****': User already exists in Cal.
ℹ️ In Cal's OAuth sandbox, the user must provide a unique email address. ℹ️
`);
return { status: "error" } as { status: "error" };
}
// let's try to reconcile our user from Cal's OAuth Managed users
const users = await getUsers();
if (users.status === "error") {
console.error(
`[Cal SDK] Unable to create user '${input.email.slice(0, 4)}*****': The user exists in Cal but we couldn't fetch the user list. Check logs above.`
);
return { status: "error" } as { status: "error" };
}
// [@calcom] 💡 Find our using by adding `+` before the @ in the email -- that's what Cal does internally.
const user = users.data.find((calUser) => {
const ourEmailAsCal = input.email.replace("@", `+${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}@`);
return calUser.email === ourEmailAsCal;
});
// if we couldn't find it, the signup fails
if (!user) {
console.error(
`[Cal SDK] Unable to create user '${input.email.slice(0, 4)}*****': The user exists in Cal but we couldn't reconcile it from the response. Is there anything wrong with the 'users.data.find' predicate?`
);
return { status: "error" } as { status: "error" };
}
// We have found the user, but are missing the accessToken, let's force-refresh them:
const refreshedTokens = await forceRefresh({ calAccountId: user.id });
if (refreshedTokens.status === "error") {
// this is somewhat of a weird state as we found the user but couldn't refresh the tokens, try to force-refresh manually.
console.error(
`[Cal SDK] Unable to create user '${input.email.slice(0, 4)}*****': You need to force-refresh tokens manually for the calAccountId§ '${user.id}'.`
);
return { status: "error" } as { status: "error" };
}
return { status: "success", data: { ...refreshedTokens.data, user } } satisfies {
status: "success";
data: CreateManagedUserOutput["data"];
};
}
return response.json() as Promise<{ status: "success"; data: CreateManagedUserOutput["data"] }>;
};
export const getUsers = async () => {
const url = new URL(calApiUrl);
url.pathname = `/v2/oauth-clients/${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}/users`;
const fetchParameters = [
url.href,
{
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": env.CAL_SECRET,
},
},
] satisfies Parameters;
const response = await fetch(fetchParameters[0], fetchParameters[1]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const json = response.json();
if (!response.ok) {
console.error(
`
[Cal SDK] Unable to fetch all users: Invalid response from Cal after attempting to fetch all users.
${composeFetchLogs({ fetch: fetchParameters, res: response, json })}
`
);
return { status: "error" } as { status: "error" };
}
return json as Promise<{ status: "success"; data: Array }>;
};
/**
* =================
* UTILITIES
* =================
*/
type FetchParametersLike = [string | URL, Parameters["1"] & { body?: string }];
export const composeFetchLogs = (ctx: {
fetch: FetchParametersLike;
res: Pick;
json: unknown;
cal?: { id: User["calAccountId"] } | { refreshToken: User["calRefreshToken"] };
}) => {
const { fetch, res, cal, json } = ctx;
const [input, init] = fetch;
const url = typeof input === "string" ? new URL(input) : input;
return `
-- REQUEST DETAILS --
URL: ${url.href}
Method: ${init?.method ?? "GET"}
Headers: ${JSON.stringify(init?.headers)}
Body: ${init?.body}
-- RESPONSE DETAILS --
Error Code: ${res.status}
Error Message: ${res.statusText}
Error Body: ${JSON.stringify(json)}
Timestamp: ${Date.now()}
-- CAL API DETAILS --
OAuthClient: ${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}
API Host: ${calApiUrl.host}
${!cal && "Manaded User id: Not available"}
${cal && ("id" in cal ? `Managed User id: ${cal.id}}` : `Managed User refresh token: ${cal.refreshToken}`)}
Endpoint: ${url.pathname}
`;
};
export const isCalError = (res: any): res is CalErrorResponse => res.status === "error";
export type CalErrorResponse = {
status: "error";
timestamp: string;
path: string;
error: {
code: string;
message: string;
details: {
message: string;
error: string;
statusCode: number;
};
};
};
export const calHeaders = {
"x-cal-secret-key": env.CAL_SECRET,
// "cal-api-version": "2024-05-21", 06-11 -> latest version & 07-15 is the previous
"cal-api-version": "2024-06-11", // 06-11 -> latest version & 07-15 is the previous
Origin: new URL(env.NEXT_PUBLIC_REFRESH_URL).origin ?? "http://localhost:3000",
// ⚠️ NestJS requires this header otherwise it won't consume the body
"Content-type": "application/json",
} as const;
export const isAdminEndpoint = (pathname: string) => pathname.startsWith("/v2/oauth");
================================================
FILE: with-platform-supabase-tailwind-prisma/src/cal/auth.ts
================================================
import { type CreateManagedUserInput } from "./__generated/cal-sdk";
import { cal } from "./api";
import { env } from "@/env";
import { type User, type Prisma } from "@prisma/client";
export async function signUp({ email, name, user }: CreateManagedUserInput & { user: Pick }) {
/** [@calcom] 1. Let's first create a managed user on the Cal Platform: */
const userCreation = await cal({ user }).post("/v2/oauth-clients/{clientId}/users", {
path: { clientId: env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID },
body: {
email,
name,
// [@calcom] If we supply the timeZone as wwell, we create a default schedule for the managed user
timeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone.toString(),
},
});
if (userCreation.status === "error") {
console.log(
`[Cal auth] Unable to create user '${email}' on Cal Platform. Check the logs above for more details`
);
throw new Error(`Unable to create user '${email}' on Cal Platform`);
}
/** [@calcom] 3. Finally, return the user as an object in the form of prisma's UserUpdateInput payload */
const toUpdate = {
calAccessToken: userCreation.data.accessToken,
calRefreshToken: userCreation.data.refreshToken,
// calAccessTokenExpiresAt: calUser.accessTokenExpiresAt,
calAccount: {
connectOrCreate: {
where: { id: userCreation.data.user.id },
create: {
id: userCreation.data.user.id,
email: userCreation.data.user.email,
username: userCreation.data.user.username,
timeZone: userCreation.data.user.timeZone,
weekStart: userCreation.data.user.weekStart,
createdDate: userCreation.data.user.createdDate,
timeFormat: userCreation.data.user.timeFormat,
defaultScheduleId: userCreation.data.user.defaultScheduleId,
},
},
},
} satisfies Prisma.UserUpdateInput;
return toUpdate;
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/cal/utils.ts
================================================
import { env } from "@/env";
export const stripCalOAuthClientIdFromText = (str: string) => {
if (str === "") return str;
return str.split(`-${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}`)?.[0]?.replace(".", " ");
};
export const stripCalOAuthClientIdFromEmail = (str: string) => {
if (str === "") return str;
return str.replace(`+${env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}`, "");
};
export const isCalSandbox =
env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID === "cluwyp9yb0001p61n2dkqdmo1" &&
"https://api.cal.dev/v2" === env.NEXT_PUBLIC_CAL_API_URL;
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/accordion.tsx
================================================
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
svg]:rotate-180",
className
)}
{...props}
>
{children}
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/avatar.tsx
================================================
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/badge.tsx
================================================
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
success: "border-transparent bg-success text-success-foreground hover:bg-success/80",
},
},
defaultVariants: {
variant: "default",
},
}
);
export interface BadgeProps
extends React.HTMLAttributes,
VariantProps {}
function Badge({ className, variant, ...props }: BadgeProps) {
return
;
}
export { Badge, badgeVariants };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/breadcrumb.tsx
================================================
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => );
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return ;
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
svg]:size-3.5", className)} {...props}>
{children ?? }
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
More
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/button.tsx
================================================
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return ;
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/card.tsx
================================================
import { cn } from "@/lib/utils";
import * as React from "react";
const Card = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
Card.displayName = "Card";
const CardHeader = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef>(
({ className, ...props }, ref) =>
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/checkbox.tsx
================================================
"use client";
import { cn } from "@/lib/utils";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import * as React from "react";
const Checkbox = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/collapsible.tsx
================================================
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/command.tsx
================================================
"use client";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
export type CommandProps = React.ElementRef &
React.ComponentPropsWithoutRef;
const Command = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
Command.displayName = CommandPrimitive.displayName;
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
{children}
);
};
const CommandInput = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>((props, ref) => );
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {
return (
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/dialog.tsx
================================================
"use client";
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
Close
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/dropdown-menu.tsx
================================================
"use client";
import { cn } from "@/lib/utils";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import * as React from "react";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
{children}
));
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, sideOffset = 4, ...props }, ref) => (
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, checked, ...props }, ref) => (
{children}
));
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
return ;
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/form.tsx
================================================
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import {
Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath,
> = {
name: TName;
};
const FormFieldContext = React.createContext({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath = FieldPath,
>({
...props
}: ControllerProps) => {
return (
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within ");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext({} as FormItemContextValue);
const FormItem = React.forwardRef>(
({ className, ...props }, ref) => {
const id = React.useId();
return (
);
}
);
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef>(
({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
);
}
);
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef>(
({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
{body}
);
}
);
FormMessage.displayName = "FormMessage";
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/input.tsx
================================================
import { cn } from "@/lib/utils";
import * as React from "react";
export type InputProps = React.InputHTMLAttributes;
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
);
});
Input.displayName = "Input";
export { Input };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/label.tsx
================================================
"use client";
import { cn } from "@/lib/utils";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
);
const Label = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & VariantProps
>(({ className, ...props }, ref) => (
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/navigation-menu.tsx
================================================
import { cn } from "@/lib/utils";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import * as React from "react";
const NavigationMenu = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center uppercase rounded-full px-4 py-2 text-sm font-medium hover:bg-black hover:text-white focus:bg-black focus:text-black-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active] data-[active]:bg-black data-[state=open]:bg-black data-[state=open]:text-white"
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
{children}{" "}
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
));
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};
================================================
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/pagination.tsx
================================================
import { type ButtonProps, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
);
Pagination.displayName = "Pagination";
const PaginationContent = React.forwardRef>(
({ className, ...props }, ref) => (
)
);
PaginationContent.displayName = "PaginationContent";
const PaginationItem = React.forwardRef>(
({ className, ...props }, ref) =>
);
PaginationItem.displayName = "PaginationItem";
type PaginationLinkProps = {
isActive?: boolean;
} & Pick &
React.ComponentProps<"a">;
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
);
PaginationLink.displayName = "PaginationLink";
const PaginationPrevious = ({ className, ...props }: React.ComponentProps) => (