Showing preview only (508K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<!-- PROJECT LOGO -->
<p align="center">
<a href="https://github.com/calcom/cal.com">
<img src="https://github.com/calcom/platform-starter-kit/assets/8019099/6f0a8337-6d18-42de-aa00-44a57764e19b" alt="Logo">
</a>
<h3 align="center">Cal.com Platform Starter Kit</h3>
<p align="center">
Build your pixel-perfect booking experience
<br />
<br />
<a href="https://experts.cal.com"><strong>Demo</strong></a>
·
<a href="https://www.youtube.com/watch?v=wwo07ghiNn4"><strong>Video Tutorial</strong></a>
·
<a href="https://cal.com/docs/platform"><strong>Docs</strong></a>
·
<a href="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"><strong>Deploy on Vercel</strong></a>
<br />
<br />
<a href="https://go.cal.com/discord">Discord</a>
·
<a href="https://cal.com/platform">Website</a>
·
<a href="https://github.com/calcom/cal.com/issues">Issues</a>
</p>
</p>
# 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**
<!-- note(richard): We require pnpm since we have this version deployed; if we separate example source from our deployed version, we free up the package manager choice. -->
> [!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
# <https://next-auth.js.org/configuration/options#secret>
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://<your-project>.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<typeof createPrismaClient> | 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 <div>Expert not found</div>;
}
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 <div>Event not found</div>;
}
const descriptionWithoutHtmlTags = eventType.data?.description.replace(/<[^>]*>?/gm, "");
return (
<div className="mb-4 flex flex-1 flex-col items-center gap-4 overflow-auto">
<header className="flex w-full flex-col justify-between gap-4 rounded-md bg-muted/50 px-8 py-4 sm:px-10 lg:flex-row lg:px-12 2xl:px-36">
<div className="flex items-center gap-x-6">
<Image
alt="Expert image"
className="aspect-square rounded-md object-cover"
src={`avatars/${expert.id}`}
height="64"
width="64"
/>
<div className="space-y-2">
<h1 className="text-2xl font-semibold capitalize leading-none tracking-tight">
{expert.name}: {eventType.data?.title}
</h1>
<p className="text-sm text-muted-foreground">{descriptionWithoutHtmlTags}</p>
</div>
</div>
</header>
<div className="mx-auto mt-4 grid w-full gap-2 px-8 sm:px-10 lg:px-12 2xl:px-36">
{Boolean(expert.calAccount) && (
<ExpertBooker
calAccount={{ username: expert.calAccount.username }}
expert={{
name: expert.name,
username: expert.username,
}}
eventSlug={eventType.data?.slug}
customClassNames={{ bookerContainer: "custom-grid border" }}
/>
)}
</div>
</div>
);
}
================================================
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 (
<section {...props}>
<h2 className="flex items-center font-mono text-sm font-medium leading-7 text-slate-900">
<Info
// colors={['fill-violet-300', 'fill-pink-300']}
className="h-2.5 w-2.5"
/>
<span className="ml-2.5">About</span>
</h2>
<p
className={clsx(
'mt-2 text-base leading-7 text-slate-700',
!isExpanded && 'lg:line-clamp-4',
)}
>
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.
</p>
{!isExpanded && (
<button
type="button"
className="mt-2 hidden text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900 lg:inline-block"
onClick={() => setIsExpanded(true)}
>
Show more
</button>
)}
</section>
)
}
================================================
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 (
<div className={cn("lg:px-8", className)} {...props}>
<div className="lg:max-w-4xl">
<div className="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0">{children}</div>
</div>
</div>
);
}
================================================
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<typeof Booker>[number];
export const ExpertBooker = (
props: {
className?: string;
calAccount: Pick<CalAccount, "username">;
expert: Pick<User, "name" | "username">;
} & Partial<BookerProps>
) => {
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 <div className="w-full text-center">Sorry. We couldn't find this experts' user.</div>;
}
if (isLoadingEvents) {
return (
<div className="flex items-center justify-center">
<Loader className="z-50 animate-spin" />
</div>
);
}
if (!eventTypes?.length) {
return (
<div className="w-full text-center">Sorry. Unable to load ${expert.name}'s availabilities.</div>
);
}
return (
<Booker
eventSlug={eventTypes[0]?.slug ?? ""}
username={calAccount.username}
onCreateBookingSuccess={(booking) => {
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 (
<div className="flex items-center justify-center py-20">
<Suspense>
<BookingResult />
</Suspense>
</div>
);
}
================================================
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 (
<div>
<header className="sticky top-0 z-50 flex h-14 items-center justify-between border-b border-border/40 bg-muted/90 px-4 py-2 backdrop-blur lg:h-[60px] lg:px-6">
<Logo />
{/*
Tip: Use this for your own navigation
<Navigation /> */}
<div>
<SignedIn>
{(_user) => (
<Link href="/dashboard">
<Button className="w-full">
Dashboard
<LogIn className="ml-1 size-4" />
</Button>
</Link>
)}
</SignedIn>
<SignedOut>
<Link href="/signup">
<Button className="w-full">Sign Up</Button>
</Link>
</SignedOut>
</div>
</header>
<main>{children}</main>
</div>
);
}
================================================
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 <div>Expert not found</div>;
}
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 (
<div className="mb-4 flex flex-1 flex-col items-center gap-4 overflow-auto">
<header className="flex w-full flex-col justify-between gap-4 rounded-md bg-muted/50 px-8 py-4 sm:px-10 lg:flex-row lg:px-12 2xl:px-36">
<div className="flex items-center gap-x-6">
<Image
alt="Expert image"
className="aspect-square rounded-md object-cover"
src={`avatars/${expert.id}`}
height="64"
width="64"
/>
<div>
<h1 className="text-2xl font-semibold capitalize leading-none tracking-tight">{expert.name}</h1>
</div>
</div>
</header>
<div className="mx-auto mt-4 grid w-full gap-2 px-8 sm:px-10 lg:px-12 2xl:px-36">
<Card className="sm:col-span-2">
<CardHeader className="pb-3">
<CardTitle>About Us</CardTitle>
<p className="max-w-lg text-balance leading-relaxed">{expert.bio}</p>
</CardHeader>
</Card>
</div>
<div className="mx-auto mt-4 grid w-full gap-2 px-8 sm:px-10 lg:px-12 2xl:px-36">
{eventTypes.status === "error" ? (
<div>User Events not found</div>
) : (
<Card>
<CardHeader>
<CardTitle>Book Us</CardTitle>
<CardDescription>Book us for any of the below events.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="hidden md:table-cell">Description</TableHead>
<TableHead>Duration (min)</TableHead>
<TableHead>
<span className="sr-only">Availability</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{eventTypes.data.map((eventType) => (
<TableRow key={eventType.id}>
<TableCell>
<Link href={`/${expert.username}/${eventType.slug}`}>
<div className="font-medium capitalize">{eventType.title}</div>
<div className="text-sm text-muted-foreground">/{eventType.slug}</div>
</Link>
</TableCell>
<TableCell>
<div className="hidden text-sm text-muted-foreground md:inline">
{eventType.description}
</div>
</TableCell>
<TableCell>{eventType.length}</TableCell>
<TableCell>
<Link href={`/${expert.username}/${eventType.slug}`}>
<Button variant="ghost" size="icon">
<ArrowRight className="size-5 hover:size-6" />
</Button>
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className="text-xs text-muted-foreground">
Showing{" "}
<strong>
{eventTypes.data.length > 0 ? 1 : 0}-
{eventTypes.data.length > 10 ? 10 : eventTypes.data.length}
</strong>{" "}
of <strong>{eventTypes.data.length}</strong> event types
</div>
</CardFooter>
</Card>
)}
</div>
</div>
);
}
================================================
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<Promise<any>> = [];
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<typeof Command> {
className?: string;
options: Array<Option>;
initialSearch?: string;
placement?: "header";
}
export const AutocompleteSearch = forwardRef<HTMLDivElement, AutocompleteSearchProps>(
({ 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 (
<div
className="relative z-10"
onBlur={(e) => {
if (e.currentTarget.contains(e.relatedTarget)) return;
if (value && !query) {
setQuery(options.find((option) => option.value === value)?.label ?? "");
}
setOpen(false);
}}>
<Command
data-open={open}
className={cn("data-[open=true]:rounded-b-none", placement === "header" && "border", className)}
ref={ref}
{...props}>
<CommandInput
value={query}
placeholder="Search an expert..."
onFocus={() => {
setOpen(true);
if (query === options.find((option) => option.value === value)?.label) {
setQuery("");
}
}}
onValueChange={(value) => setQuery(value)}
className="h-full justify-center leading-[2.75rem]"
/>
<CommandList>
{open && (
<div
data-open={open}
className={cn(
"absolute left-0 right-0 top-full rounded-b-md bg-background p-0 shadow !duration-150 data-[open=true]:animate-in data-[open=true]:fade-in",
placement === "header" && "border-x border-b"
)}>
<CommandEmpty>No expert found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(newValue) => {
setValue(newValue);
setQuery(options.find((option) => option.value === newValue)?.label ?? "");
setOpen(false);
router.push(`/experts?${new URLSearchParams({ q: newValue }).toString()}`, {
scroll: false,
});
}}>
<Check
className={cn("mr-2 h-4 w-4", value === option.value ? "opacity-100" : "opacity-0")}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</div>
)}
</CommandList>
</Command>
</div>
);
}
);
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 (
<div className="relative isolate z-50 flex items-center gap-x-6 overflow-hidden bg-gray-50 px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
<div
className="absolute left-[max(-7rem,calc(50%-52rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl"
aria-hidden="true">
<div
className="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] opacity-30"
style={{
clipPath:
"polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)",
}}
/>
</div>
<div
className="absolute left-[max(45rem,calc(50%+8rem))] top-1/2 -z-10 -translate-y-1/2 transform-gpu blur-2xl"
aria-hidden="true">
<div
className="aspect-[577/310] w-[36.0625rem] bg-gradient-to-r from-[#ff80b5] to-[#9089fc] opacity-30"
style={{
clipPath:
"polygon(74.8% 41.9%, 97.2% 73.2%, 100% 34.9%, 92.5% 0.4%, 87.5% 0%, 75% 28.6%, 58.5% 54.6%, 50.1% 56.8%, 46.9% 44%, 48.3% 17.4%, 24.7% 53.9%, 0% 27.9%, 11.9% 74.2%, 24.9% 54.1%, 68.6% 100%, 74.8% 41.9%)",
}}
/>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<p className="text-sm leading-6 text-gray-900">
<strong className="font-semibold">{title}</strong>
<svg viewBox="0 0 2 2" className="mx-2 inline h-0.5 w-0.5 fill-current" aria-hidden="true">
<circle cx={1} cy={1} r={1} />
</svg>
{description}
</p>
<a href="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">
<Button size="sm" className="flex h-[32px] w-[103px] gap-2" variant="default">
<svg
className="size-5"
// width="24"
// height="24"
viewBox="0 0 76 65"
fill="#fff"
xmlns="http://www.w3.org/2000/svg">
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="#fff" />
</svg>
Deploy
</Button>
</a>
<a href={ctaLink}>
<Button size="icon" className="flex h-[32px] gap-2" variant="ghost">
<svg
className="size-5 fill-black"
// width="24"
// height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017C2 16.442 4.865 20.197 8.839 21.521C9.339 21.613 9.521 21.304 9.521 21.038C9.521 20.801 9.513 20.17 9.508 19.335C6.726 19.94 6.139 17.992 6.139 17.992C5.685 16.834 5.029 16.526 5.029 16.526C4.121 15.906 5.098 15.918 5.098 15.918C6.101 15.988 6.629 16.95 6.629 16.95C7.521 18.48 8.97 18.038 9.539 17.782C9.631 17.135 9.889 16.694 10.175 16.444C7.955 16.191 5.62 15.331 5.62 11.493C5.62 10.4 6.01 9.505 6.649 8.805C6.546 8.552 6.203 7.533 6.747 6.155C6.747 6.155 7.587 5.885 9.497 7.181C10.3128 6.95851 11.1544 6.84519 12 6.844C12.85 6.848 13.705 6.959 14.504 7.181C16.413 5.885 17.251 6.154 17.251 6.154C17.797 7.533 17.453 8.552 17.351 8.805C17.991 9.505 18.379 10.4 18.379 11.493C18.379 15.341 16.04 16.188 13.813 16.436C14.172 16.745 14.491 17.356 14.491 18.291C14.491 19.629 14.479 20.71 14.479 21.038C14.479 21.306 14.659 21.618 15.167 21.52C17.1583 20.8521 18.8893 19.5753 20.1155 17.87C21.3416 16.1648 22.0009 14.1173 22 12.017C22 6.484 17.522 2 12 2Z"></path>
</svg>
</Button>
</a>
</div>
<div className="flex flex-1 justify-end">
<button type="button" className="-m-3 p-3 focus-visible:outline-offset-[-4px]">
<span className="sr-only">Dismiss</span>
</button>
</div>
</div>
);
}
================================================
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 <div>No Booking UID.</div>;
}
if (isLoading) {
return <Loader className="z-50 animate-spin place-self-center" />;
}
if (!booking) {
return <div>Booking not found</div>;
}
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 (
<Card className="w-full max-w-lg">
<CardHeader className="space-y-4 px-8">
<div className="flex items-center justify-center space-x-2">
{bookingStatus?.toLowerCase() === "cancelled" && (
<div className="flex flex-col items-center space-y-4">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-destructive/50">
<X className="size-6 text-destructive" />
</div>
<CardTitle className="text-2xl">Meeting Cancelled</CardTitle>
</div>
)}
{bookingStatus?.toLowerCase() === "accepted" && (
<div className="flex flex-col items-center space-y-4">
<div className="mx-auto flex size-12 items-center justify-center rounded-full bg-success">
<Check className="size-6 text-green-600" />
</div>
<CardTitle className="text-2xl">Meeting scheduled successfully</CardTitle>
</div>
)}
</div>
</CardHeader>
<CardContent className="p-6 px-8 pt-2 text-sm">
<Separator className="mb-8" />
<div className="grid gap-3">
<ul className="grid gap-3">
<li className="flex flex-col">
<span className="space-y-0.5 font-semibold">What</span>
{formerWhat !== what && (
<span className={cn("text-muted-foreground line-through")}>{formerWhat}</span>
)}
<span
className={cn(
"text-muted-foreground",
bookingStatus?.toLowerCase() === "cancelled" && "line-through"
)}>
{what}
</span>
</li>
<li className="flex flex-col">
<span className="space-y-0.5 font-semibold">When</span>
{formerWhen !== when && (
<span className={cn("text-muted-foreground line-through")}>{formerWhen}</span>
)}
<span
className={cn(
"text-muted-foreground",
bookingStatus?.toLowerCase() === "cancelled" && "line-through"
)}>
{when}
</span>
</li>
<li className="flex flex-col">
<span className="font-semibold">Who</span>
<ul className="space-y-0.5">
<li
className={cn(
"text-muted-foreground",
bookingStatus?.toLowerCase() === "cancelled" && "line-through"
)}>
{who.host}
</li>
{who.attendees.map((attendee, idx) => (
<li
key={idx}
className={cn(
"text-muted-foreground",
bookingStatus?.toLowerCase() === "cancelled" && "line-through"
// // if the attendee is not in the previous booking, we'll highlight them
// formerWho?.attendees?.findIndex((formerAttendee) => formerAttendee === attendee) ===
// -1 && "font-semibold italic"
)}>
{attendee}
{formerWho?.attendees?.findIndex((formerAttendee) => formerAttendee === attendee) ===
-1 && (
<Badge className="ml-2 px-1.5 py-[0.05rem] text-xs font-normal leading-none">New</Badge>
)}
</li>
))}
{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 && (
<li
key={idx}
className={cn(
"text-muted-foreground",
// if the attendee is not in the current booking, we'll strike them out
who.attendees.findIndex((attendee) => attendee === formerAttendee) === -1 &&
"line-through"
)}>
{formerAttendee}
</li>
)
)}
</ul>
</li>
<li className="flex flex-col">
<span className="space-y-0.5 font-semibold">Where</span>
{/* Display the previous location only if it's different from the current booking */}
{bookingPrevious.data?.location !== booking.location && (
<span className={cn("text-muted-foreground")}>
{bookingPrevious.data?.location === "integrations:daily" ? (
<span className="border-b-0 border-transparent hover:border-b hover:border-current">
<Link
className={cn("inline-flex items-center gap-1")}
href={
(bookingPrevious.data?.metadata as { videoCallUrl?: string })?.videoCallUrl ?? "#"
}>
Online (Cal Video)
</Link>
</span>
) : (
bookingPrevious.data?.location
)}
</span>
)}
{/* Display the location of the current booking */}
<span
className={cn(
"text-muted-foreground",
bookingPrevious.data?.location !== booking.location && "line-through"
)}>
{booking?.location === "integrations:daily" ? (
<span className="border-b-0 border-transparent hover:border-b hover:border-current">
<Link
className={cn(
"inline-flex items-center gap-1",
bookingStatus?.toLowerCase() === "cancelled" && "line-through",
bookingStatus?.toLowerCase() === "cancelled" && "cursor-not-allowed"
)}
href={
bookingStatus?.toLowerCase() === "cancelled"
? "#"
: (booking?.metadata as { videoCallUrl?: string })?.videoCallUrl ?? "#"
}>
Online (Cal Video)
<ExternalLinkIcon className="size-4" />
</Link>
</span>
) : (
booking.location
)}
</span>
</li>
{booking.description && (
<li className="flex flex-col">
<span className="space-y-0.5 font-semibold">Event Description</span>
{booking.description !== bookingPrevious.data?.description && (
<span className={cn("text-muted-foreground line-through")}>
{bookingPrevious?.data?.description}
</span>
)}
<span
className={cn(
"text-muted-foreground",
bookingStatus?.toLowerCase() === "cancelled" && "line-through"
)}>
{booking.description}
</span>
</li>
)}
</ul>
</div>
<Separator className="mt-8" />
</CardContent>
<CardFooter className="flex flex-col px-8">
{bookingStatus?.toLowerCase() === "cancelled" ? (
<div>
<span>Want to book {booking?.user?.name}?</span>
<span>
{" "}
See{" "}
<Link href={`/${expertUsername}`} className="underline">
availabilities
</Link>
</span>
</div>
) : (
<div>
<span>Need to make changes?</span>
<span>
{" "}
<Link href={`/${expertUsername}?rescheduleUid=${bookingUid}`} className="underline">
Reschedule
</Link>{" "}
or{" "}
<div
className="cursor-pointer underline"
onClick={() => {
return cancelBooking({
id: booking.id,
uid: booking.uid,
cancellationReason: "User request",
allRemainingBookings: true,
});
}}>
Cancel
</div>
</span>
</div>
)}
</CardFooter>
</Card>
);
};
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<SyntheticEvent<HTMLImageElement, Event> | null>(null);
const [isLoading, setIsLoading] = useState(true);
return (
<Link href={"/" + slug} className="col-span-1 flex">
<Card className="mx-auto overflow-hidden transition-all ease-in-out hover:rotate-1 hover:scale-105 hover:shadow-lg">
<div
className={cn(
"h-[265px] w-[380px] rounded-md",
error && "bg-muted",
isLoading && "animate-pulse bg-muted"
)}>
{!error && (
<Image
src={`avatars/${userId}?width=380&height=265`}
alt={title}
className="h-full w-full rounded-md object-cover"
height={265}
width={380}
objectFit="cover"
onLoadingComplete={() => setIsLoading(false)}
onError={setError}
/>
)}
</div>
<CardHeader>
<CardTitle className="text-xl">
{/* this highlights the search query for the title */}
{queryIndexTitle != undefined && query ? (
<>
{title.substring(0, queryIndexTitle)}
<span className="bg-yellow-300">
{title.substring(queryIndexTitle, queryIndexTitle + query.length)}
</span>
{title.substring(queryIndexTitle + query.length)}
</>
) : (
title
)}
</CardTitle>
<CardDescription>
{/* this highlights the search query for the title */}
{queryIndexDescription != undefined && query ? (
<>
{description.substring(0, queryIndexDescription)}
<span className="bg-yellow-300">
{description.substring(queryIndexDescription, queryIndexDescription + query.length)}
</span>
{description.substring(queryIndexDescription + query.length)}
</>
) : (
description
)}
</CardDescription>
</CardHeader>
</Card>
</Link>
);
}
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 (
<Fragment>
<div
className="flex min-h-96 flex-col justify-center bg-cover bg-center bg-no-repeat py-20"
style={{ backgroundImage: "url('/hero.jpg')" }}>
<div className="container mt-16 flex flex-col items-center justify-center gap-12 px-4 py-6">
<h1 className="font-display text-5xl font-extrabold tracking-tight text-white">
<Balancer>Find your Cal.com Expert</Balancer>
</h1>
<SearchBar />
</div>
</div>
<div className="flex-1">
<div className="sm:my-10">
<Suspense
fallback={
<div className="relative h-max w-full max-w-sm place-self-center">
<div className="absolute inset-0 z-40 grid rounded-2xl bg-slate-900 text-white">
<Loader className="z-50 animate-spin place-self-center" />
</div>
</div>
}>
<div className="block sm:flex">
<div className="flex items-center gap-2 p-4">
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="sm" className="h-8 gap-1 sm:hidden">
<span>Filter</span>
<ListFilter className="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="sm:max-w-xs">
{filtersByCategory.map((section) => (
<div
key={section.filterCategoryValue}
className="mb-8 space-y-4 border-b border-gray-200 pb-8">
<p className="text-base font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{section.filterCategoryLabel}
</p>
{filterOptions
.filter(
(filterOption) =>
filterOption.filterCategoryFieldId === section.filterCategoryFieldId
)
.map((filterOption) => (
<SidebarItem
category={section.filterCategoryFieldId}
key={filterOption.fieldId}
id={filterOption.fieldId}
label={filterOption.fieldLabel}
/>
))}
</div>
))}
</SheetContent>
</Sheet>
</div>
<aside className="hidden w-full overflow-scroll border-r border-gray-300 p-4 sm:max-h-full sm:w-72 sm:border-0 md:block">
{filtersByCategory.map((section) => (
<div
key={section.filterCategoryValue}
className="mb-8 space-y-4 border-b border-gray-200 pb-8">
<p className="text-base font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{section.filterCategoryLabel}
</p>
{filterOptions
.filter(
(filterOption) => filterOption.filterCategoryFieldId === section.filterCategoryFieldId
)
.map((filterOption) => (
<SidebarItem
category={section.filterCategoryValue}
key={filterOption.fieldId}
id={filterOption.fieldId}
label={filterOption.fieldLabel}
/>
))}
</div>
))}
</aside>
<main className="w-full p-4 pt-0">
<div className="grid grid-cols-1 gap-4 space-x-2 md:grid-cols-3 2xl:grid-cols-4">
{!query && props.signedOut}
{experts.length ? (
experts.map(({ username, name, bio, id }) => (
<ResultsCard
key={username}
slug={username ?? ""}
userId={id ?? ""}
title={name ?? "Your title"}
description={bio ?? "Your bio"}
query={query ?? undefined}
/>
))
) : (
<Card className="mx-auto flex items-center">
<div>
<CardHeader>
<CardTitle className="text-xl">No experts found</CardTitle>
<CardDescription>
We’ve filtered our experts based on your search query and selected filters:
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Query</Label>
<p className="col-span-3 max-w-lg text-balance text-sm leading-relaxed">
{query ?? "No search query provided"}
</p>
<Label className="text-right">Filters</Label>
<p className="col-span-3 max-w-lg text-balance text-sm capitalize leading-relaxed">
{Object.keys(filters ?? {}).length
? Object.entries(filters ?? {})
.map(([filterCategory, filterValues]) => {
return `${filterCategory}: ${filterValues.join(", ")}`;
})
.join(", ")
: "No filters selected"}
</p>
</div>
</div>
</CardContent>
</div>
</Card>
)}
</div>
</main>
</div>
</Suspense>
</div>
</div>
</Fragment>
);
}
================================================
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<typeof Checkbox>) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const [filters, setFilters] = useQueryState("f", parseAsJson(filterSearchParamSchema.parse));
const selectedIds = filters?.[category];
return (
<div className="flex items-center space-x-2">
<Checkbox
id={id}
className={cn(className)}
// @ts-expect-error id could be anything
defaultChecked={selectedIds?.includes(id)}
onCheckedChange={async (checked) => {
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
htmlFor={id}
className="text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{label}
</Label>
</div>
);
}
================================================
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 (
<Card className="mx-auto flex items-center">
<div>
<CardHeader>
<CardTitle className="text-xl">
Are you an expert of <span className="font-display">Cal.com</span>?
</CardTitle>
<CardDescription>
Sign up, connect your calendar and fill your schedule with exciting customers who need help with
anything Cal.com-related!
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<Link href="/signup">
<Button className="w-full">Sign Up</Button>
</Link>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{" "}
<Link href="/login" className="underline">
Log in
</Link>
</div>
</CardContent>
</div>
</Card>
);
};
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<Option>;
placeholder: string;
} & React.ComponentProps<typeof CommandPrimitive.Input>
) {
const { options, id, name, ...inputProps } = props;
const initiallySelected = options?.[0];
const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<Array<Option | null>>([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<HTMLDivElement>) => {
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 <input /> 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 (
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-transparent">
<div className="group rounded-md border border-input px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
<div className="flex flex-wrap gap-1">
{selected.map((option) => {
if (!option) return null;
return (
<Badge key={option.value} variant="secondary">
{option.label}
<button
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(option);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(option)}>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
onBlur={() => {
setOpen(false);
}}
onFocus={() => setOpen(true)}
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
{...inputProps}
/>
<input hidden id={id} name={name} value={JSON.stringify(selected.map((s) => s?.value))} readOnly />
</div>
</div>
<div className="relative mt-2">
{open && selectables.length > 0 ? (
<div className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandList>
<CommandGroup className="h-full overflow-auto">
{selectables.map((option) => {
return (
<CommandItem
key={option.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={(_value) => {
setInputValue("");
setSelected((prev) => [...prev, option]);
}}
className={"cursor-pointer"}>
{option.label}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</div>
) : null}
</div>
</Command>
);
}
================================================
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 (
<NavigationMenu className="hidden rounded-full border-2 border-black p-0.5 md:block">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Getting started</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li className="row-span-3">
<NavigationMenuLink asChild>
<a
className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
href="/">
{/*
<Icons.logo className="h-6 w-6" /> */}
<div className="mb-2 mt-4 text-lg font-medium">shadcn/ui</div>
<p className="text-sm leading-tight text-muted-foreground">
Beautifully designed components that you can copy and paste into your apps. Accessible.
Customizable. Open Source.
</p>
</a>
</NavigationMenuLink>
</li>
<ListItem href="/docs" title="Introduction">
Re-usable components built using Radix UI and Tailwind CSS.
</ListItem>
<ListItem href="/docs/installation" title="Installation">
How to install dependencies and structure your app.
</ListItem>
<ListItem href="/docs/primitives/typography" title="Typography">
Styles for headings, paragraphs, lists...etc
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Components</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
{components.map((component) => (
<ListItem key={component.title} title={component.title} href={component.href}>
{component.description}
</ListItem>
))}
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<Link href="/docs" legacyBehavior passHref>
<NavigationMenuLink className={navigationMenuTriggerStyle()}>Documentation</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
);
}
const ListItem = React.forwardRef<React.ElementRef<"a">, React.ComponentPropsWithoutRef<"a">>(
({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent/50 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className
)}
{...props}>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">{children}</p>
</a>
</NavigationMenuLink>
</li>
);
}
);
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 (
<div className="w-full max-w-2xl">
<Input
placeholder="Search for your expert, topic or more"
className="h-14 w-full shadow-md"
defaultValue={query ?? ""}
onChange={async (e) => {
// append the query to the URL
await setQuery(e.target.value);
}}
/>
{/* <AutocompleteSearch options={professions} /> */}
</div>
);
};
================================================
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 (
<>
<Button
type="submit"
variant="secondary"
disabled={pending}
className={cn("w-48 font-normal", className)}
{...props}>
{pending ? (
<div className="flex w-full flex-row justify-evenly">
<Loader
className="stroke-offset-foreground/25 h-5 w-5 animate-spin"
// 1s feels a bit fast
style={{ animationDuration: "2s" }}
/>
</div>
) : (
children
)}
</Button>
</>
);
};
================================================
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 (
<div
className="flex min-h-96 flex-col justify-center bg-cover bg-center bg-no-repeat py-20"
style={{ backgroundImage: "url('/hero.jpg')" }}>
<div className="container mt-16 flex flex-col items-center justify-center gap-12 px-4 py-6">
<h1 className="font-display text-5xl font-extrabold tracking-tight text-white">
<Balancer>{title}</Balancer>
</h1>
{children}
</div>
</div>
);
};
================================================
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 <aside className={cn("layout-aside min-w-[260px] p-4", className)}>{children}</aside>;
};
export const LayoutMain = ({ children, className }: BaseProps) => {
return <main className={cn("layout-main p-4", className)}>{children}</main>;
};
export const Layout = ({
children,
className,
flex = false,
fullWidth = true,
align,
}: BaseProps & LayoutProps) => {
return (
<div
className={cn(
"layout mx-auto my-0",
flex && "flex flex-1",
align && `justify-${align}`,
!fullWidth && `max-w-screen-2xl`,
className
)}>
{children}
</div>
);
};
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 (
<Link href={href ?? "/"} className={cn("flex font-display text-2xl", className)}>
Cal.com <span className="font-display text-sm">®</span>
</Link>
);
};
================================================
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 (
<Suspense>
{* note how we're passing the unresolved promise as props*}
<UseCalAtoms calAccessToken={currentUser().then((dbUser) => dbUser?.calAccessToken ?? undefined)}>
{props.children}
</UseCalAtoms>
</Suspense>
)
}
```
*/
export default function UseCalAtoms(props: {
children: React.ReactNode;
calAccessToken: Promise<string | null>;
}) {
const accessToken = use(props.calAccessToken);
return (
<CalProvider
clientId={env.NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID}
options={{
apiUrl: env.NEXT_PUBLIC_CAL_API_URL,
refreshUrl: env.NEXT_PUBLIC_REFRESH_URL,
}}
{...(accessToken && { accessToken: accessToken })}>
{props.children}
</CalProvider>
);
}
================================================
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<ElementType>(arr: Array<ElementType>): [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 (
<Breadcrumb>
<BreadcrumbList>
{dashboardSegments.map((segment, idx) => {
const parentSegments = dashboardSegments.slice(0, idx);
const parentPath = parentSegments.length > 0 ? `/${parentSegments.join("/")}` : "";
const href = `${parentPath}/${segment}`;
return (
<Fragment key={href}>
{idx > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link className="capitalize transition-colors hover:text-foreground" href={href}>
{segment}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
</Fragment>
);
})}
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage className="capitalize">{breadcrumbPage}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
}
================================================
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 (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>Dashboard</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
}
================================================
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 (
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
{dashboardNavigationData.map((navigationItem) => {
return (
<Link
key={navigationItem.href}
href={navigationItem.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:bg-muted/50 hover:text-primary",
pathname === navigationItem.href && "bg-muted text-primary"
)}
prefetch={false}>
<navigationItem.icon className="size-4" />
{navigationItem.label}
</Link>
);
})}
</nav>
);
}
================================================
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 (
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
{dashboardNavigationData.map((navigationItem) => {
return (
<Link
key={navigationItem.href}
href={navigationItem.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:bg-muted/50 hover:text-primary",
pathname === navigationItem.href && "bg-muted text-primary"
)}
prefetch={false}>
<navigationItem.icon className="size-4" />
{navigationItem.label}
</Link>
);
})}
</nav>
);
}
================================================
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 (
<nav className="grid gap-6 text-lg font-medium">
<Logo
href="/dashboard"
className="group h-10 shrink-0 font-display text-lg font-semibold md:text-base"
/>
{dashboardNavigationData.map((navigationItem) => {
return (
<Link
key={navigationItem.href}
href={navigationItem.href}
className={cn(
"flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground",
navigationItem.href === pathname && "text-muted-foreground"
)}>
<navigationItem.icon className="size-5 transition-all group-hover:scale-110" />
{navigationItem.label}
</Link>
);
})}
</nav>
);
}
================================================
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 (
<nav className="grid gap-6 text-lg font-medium">
<Logo
href="/dashboard"
className="group h-10 shrink-0 font-display text-lg font-semibold md:text-base"
/>
{dashboardNavigationData.map((navigationItem) => {
return (
<Link
key={navigationItem.href}
href={navigationItem.href}
className={cn(
"flex items-center gap-4 px-2.5 text-muted-foreground hover:text-foreground",
navigationItem.href === pathname && "text-muted-foreground"
)}>
<navigationItem.icon className="size-5 transition-all group-hover:scale-110" />
{navigationItem.label}
</Link>
);
})}
</nav>
);
}
================================================
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<GetBookingsDataEntry>;
currentWeek: Array<GetBookingsDataEntry>;
currentMonth: Array<GetBookingsDataEntry>;
currentYear: Array<GetBookingsDataEntry>;
};
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<GetBookingsDataEntry | null | undefined>(
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 (
<Fragment>
<Tabs
defaultValue="week"
className="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3"
onValueChange={(val) => setSelectedTab(val as "week" | "month" | "year")}>
<div className="flex items-center lg:col-span-2 xl:col-span-3">
<TabsList>
{tabs.map((tab, idx) => (
<TabsTrigger key={idx} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
</div>
{tabs.map((tab, idx) => {
return (
<TabsContent value={tab.value} key={idx} className="xl:col-span-2">
<Card>
<CardHeader className="px-7">
<CardTitle>Bookings</CardTitle>
<CardDescription>Bookings from potential customers.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Initiator</TableHead>
<TableHead className="hidden sm:table-cell">Event Type</TableHead>
<TableHead className="hidden sm:table-cell">Status</TableHead>
<TableHead className="hidden md:table-cell">Date</TableHead>
<TableHead className="text-right">Time</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{bookings.length ? (
bookings.map((booking, idx) => {
const initiator = booking.attendees[0];
const isEven = idx % 2 === 0;
return (
<TableRow
key={booking.id}
className={cn(
"data-[current=true]:bg-muted-foreground/30",
isEven && "bg-accent"
)}
data-current={
bookings.findIndex((booking) => booking.id === selectedElement?.id) === idx
}
onClick={() => setSelectedElement(booking)}>
<TableCell>
<div className="font-medium capitalize">
{stripCalOAuthClientIdFromText(initiator?.name ?? "")}
</div>
<div className="hidden text-sm text-muted-foreground md:inline">
{stripCalOAuthClientIdFromEmail(initiator?.email ?? "")}
</div>
</TableCell>
<TableCell className="hidden sm:table-cell">{booking.eventType.slug}</TableCell>
<TableCell className="hidden sm:table-cell">
<Badge
variant={
// so that we show the destructive badge for cancelled meetings, and success badge for confirmed meetings
(["CANCELLED", "REJECTED"] as Array<BookingStatus>).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<BookingStatus>)
// @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}
</Badge>
</TableCell>
<TableCell className="hidden md:table-cell">
{dayjs(booking.startTime).format("YYYY-MM-DD")}
</TableCell>
<TableCell className="text-right">
<div className="font-medium capitalize">
{dayjs(booking.startTime).format("h:mma")}
</div>
<div className="hidden text-sm text-muted-foreground md:inline">
{props.user.timeZone}
</div>
</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell>
<p className="text-sm text-muted-foreground">
In the current {tab.label.toLocaleLowerCase()}, you don’t have any
bookings.
</p>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
);
})}
<Card className="mt-2" x-chunk="dashboard-05-chunk-4">
<CardHeader className="flex flex-row items-start bg-muted/50">
<div className="grid gap-0.5">
<CardTitle className="group flex items-center gap-2 text-lg">
{stripCalOAuthClientIdFromText(selectedElement?.title ?? "No booking selected")}
<Button
size="icon"
variant="outline"
className="h-6 w-6 opacity-0 transition-opacity group-hover:opacity-100">
<Copy className="h-3 w-3" />
<span className="sr-only">Copy Booking ID</span>
</Button>
</CardTitle>
<CardDescription className="flex flex-col"></CardDescription>
</div>
</CardHeader>
<CardContent className="p-6 text-sm">
<div className="grid gap-3">
<div className="font-semibold">Booking Details</div>
<ul className="grid gap-3">
<li className="flex items-center justify-between text-muted-foreground">
<span>Booking Uid:</span>
<span>{selectedElement?.uid}</span>
</li>
<li className="flex items-center justify-between text-muted-foreground">
<span>Date:</span>
<span>
{selectedElement?.startTime
? dayjs(selectedElement?.startTime).format("MMMM DD, YYYY")
: "MMMM DD, YYYY"}
</span>
</li>
<li className="flex items-center justify-between text-muted-foreground">
<span>Status:</span>
<span>
<Badge
variant={
// so that we show the destructive badge for cancelled meetings, and success badge for confirmed meetings
(["CANCELLED", "REJECTED"] as Array<BookingStatus>).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<BookingStatus>)
// @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}
</Badge>
</span>
</li>
</ul>
<Separator className="my-2" />
<div className="font-semibold">Attendees</div>
<ul className="grid gap-3">
{who.attendees?.map((attendee, idx) => (
<li key={idx} className="flex items-center justify-between">
<span className="capitalize text-muted-foreground">{attendee.name}</span>
<span>{attendee.email}</span>
</li>
))}
</ul>
</div>
<Separator className="my-4" />
<div className="grid gap-3">
<div className="font-semibold">Customer Information</div>
<dl className="grid gap-3">
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Customer</dt>
<dd className="capitalize">
{stripCalOAuthClientIdFromText(selectedElement?.attendees[0]?.name ?? "")}
</dd>
</div>
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Email</dt>
<dd>
<a href="mailto:">
{stripCalOAuthClientIdFromEmail(selectedElement?.attendees[0]?.email ?? "")}
</a>
</dd>
</div>
<div className="flex items-center justify-between">
<dt className="text-muted-foreground">Language</dt>
<dd>
{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})`
: ""}
</dd>
</div>
</dl>
</div>
</CardContent>
<CardFooter className="flex flex-row items-center border-t bg-muted/50 px-6 py-3">
<div className="text-xs text-muted-foreground">
Updated <time dateTime={Date.now().toLocaleString()}>Today</time>
</div>
<Pagination className="ml-auto mr-0 w-auto">
<PaginationContent>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="h-6 w-6"
onClick={() =>
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
}>
<ChevronLeft className="h-3.5 w-3.5" />
<span className="sr-only">Previous Booking</span>
</Button>
</PaginationItem>
<PaginationItem>
<Button
size="icon"
variant="outline"
className="h-6 w-6"
onClick={() =>
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
}>
<ChevronRight className="h-3.5 w-3.5" />
<span className="sr-only">Next Booking</span>
</Button>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
</Tabs>
</Fragment>
);
};
================================================
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 (
<>
<Card className="mx-auto mt-10 w-full max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Getting Started</CardTitle>
<CardDescription>Connect your calendar to get started.</CardDescription>
</CardHeader>
<CardFooter className="[&>div]:w-full">
<Suspense
fallback={
<div className="relative h-max w-full max-w-sm place-self-center">
<div className=" absolute inset-0 z-40 grid rounded-2xl bg-slate-900 text-white">
<Loader className="z-50 animate-spin place-self-center" />
</div>
</div>
}>
<GcalConnect className="flex w-full items-center justify-center [&>svg]:mr-2" />
</Suspense>
</CardFooter>
</Card>
<div className="flex w-full justify-end">
<ButtonSubmit variant="default" onClick={nextStep} size="sm">
Next
</ButtonSubmit>
</div>
</>
);
};
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<FilterOption>;
}) => {
return (
<main className="flex-1 bg-muted/40">
<div className="flex items-center justify-center p-10">
<div className="w-3/4">
<Stepper initialStep={0} steps={steps}>
{steps.map(({ id, label }, index) => {
return (
<Step key={id} label={label}>
{index === 0 && (
<div className="flex h-full min-h-96 flex-col items-center justify-between">
<ConnectCalendarStep />
</div>
)}
{index === 1 && <UserFilters filterOptions={filterOptions} />}
{index === 2 && <UserDetailsStep userId={userId} />}
</Step>
);
})}
</Stepper>
</div>
</div>
</main>
);
};
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<UserDetailsFormState, FormData>(expertEdit, {
error: null,
});
const { isDisabledStep, prevStep } = useStepper();
useEffect(() => {
if ("success" in userDetailsFormState && userDetailsFormState?.success) {
router.push("/dashboard");
}
}, [userDetailsFormState]);
return (
<form className="mt-10" action={dispatch}>
<SupabaseReactDropzone userId={userId ?? "clxj4quka0000gebuthdxi1cp"} />
<div>
<Label htmlFor="bio">Bio</Label>
<Textarea
placeholder="Tell us a little bit about yourself"
className="resize-none"
id="bio"
name="bio"
maxLength={500}
/>
</div>
<div className="mt-4 flex justify-end gap-2">
<Button disabled={isDisabledStep} onClick={prevStep} size="sm" variant="secondary">
Prev
</Button>
<ButtonSubmit variant="default" size="sm">
Finish
</ButtonSubmit>
</div>
</form>
);
};
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<FilterOption> }) => {
const [formState, dispatch] = useFormState<TUserFiltersFormState, FormData>(addUserFilters, {
error: null,
});
const { isDisabledStep, prevStep, nextStep } = useStepper();
const filtersByCategory = uniqueBy(filterOptions, prop("filterCategoryFieldId"));
useEffect(() => {
if ("success" in formState && !!formState?.success) {
nextStep();
}
}, [formState]);
return (
<form action={dispatch} className="mt-10">
{filtersByCategory.map(({ filterCategoryFieldId, filterCategoryLabel }) => (
<div className="grid gap-2" key={filterCategoryFieldId}>
<Label htmlFor="email">{filterCategoryLabel}</Label>
<FancyMultiSelect
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
options={filterOptions
.filter((filterOption) => 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"]] ? (
<div className="text-sm font-medium text-red-700" aria-live="polite">
{
formState.inputErrors?.[
filterCategoryFieldId as keyof TUserFiltersFormState["inputErrors"]
]?.[0]
}
</div>
) : null}
</div>
))}
<div className="mt-4 flex justify-end gap-2">
<Button disabled={isDisabledStep} onClick={prevStep} size="sm" variant="secondary">
Prev
</Button>
<ButtonSubmit variant="default" size="sm">
Next
</ButtonSubmit>
</div>
</form>
);
};
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 }) => <Home className={props.className} />,
},
{
label: "Booking Events",
href: "/dashboard/settings/booking-events",
icon: (props: { className?: string }) => <Calendar className={props.className} />,
},
{
label: "Profile",
href: "/dashboard/settings/profile",
icon: (props: { className?: string }) => <User className={props.className} />,
},
{
label: "Availability",
href: "/dashboard/settings/availability",
icon: (props: { className?: string }) => <Clock className={props.className} />,
},
];
================================================
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 <div>Not logged in</div>;
}
return <GettingStarted userId={sesh.user.id} filterOptions={filterOptions} />;
}
================================================
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 (
<SignedIn>
{({ user }) => (
<div className="grid min-h-screen w-full lg:grid-cols-[220px_1fr] 2xl:grid-cols-[280px_1fr]">
<div className="hidden border-r bg-muted/40 md:block">
<div className="sticky top-0 z-40 flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
<Logo />
</div>
<div className="flex-1">{dashboardNavigationDesktop}</div>
</div>
</div>
<div className="flex flex-col">
<header className="sticky top-0 z-40 flex h-14 items-center gap-4 border-b border-border/40 bg-muted/40 px-4 backdrop-blur-[2px] lg:h-[60px] lg:px-6">
<Sheet>
<SheetTrigger asChild>
<Button size="icon" variant="outline" className="sm:hidden">
<PanelLeft className="h-5 w-5" />
<span className="sr-only">Toggle Menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="sm:max-w-xs">
{dashboardNavigationMobile}
</SheetContent>
</Sheet>
{breadcrumbs}
<div className="relative ml-auto flex-1 md:grow-0">
<div className="flex flex-row items-center justify-end gap-4">
<div className="flex flex-row items-center gap-2">
<span className="hidden text-sm text-muted-foreground [width:max-content] md:block">
Logged in as <b>{user?.username}</b>
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="overflow-hidden rounded-full">
<User className="" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link href="/dashboard/settings">Settings</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/" });
}}>
<ButtonSubmit className="w-full">Logout</ButtonSubmit>
</form>
</DropdownMenuLabel>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</header>
{children}
</div>
</div>
)}
</SignedIn>
);
}
================================================
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 <div>Not logged in</div>;
}
/** [@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 (
<main className="flex flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-8">
<div className="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<Card className="sm:col-span-2" x-chunk="dashboard-05-chunk-0">
<CardHeader className="pb-3">
<CardTitle>Your Bookings</CardTitle>
<CardDescription className="max-w-lg text-balance leading-relaxed">
See all your bookings for your services.
</CardDescription>
</CardHeader>
<CardFooter className="pt-6">
<Link href="/dashboard/settings/booking-events">
<Button>
Manage booking events
<ArrowRight className="ml-1 size-4" />
</Button>
</Link>
</CardFooter>
</Card>
<Card x-chunk="dashboard-05-chunk-1">
<CardHeader className="pb-2">
<CardDescription>This Week</CardDescription>
<CardTitle className="text-4xl">{thisWeekBookings.length}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xs text-muted-foreground">
{lastWeekBookings.length > 0
? `${changeFromPrevious(thisWeekBookings.length, lastWeekBookings.length)}% from last week`
: "From 0 last week"}
</div>
</CardContent>
<CardFooter>
<Progress
value={Math.abs(changeFromPrevious(thisWeekBookings.length, lastWeekBookings.length))}
aria-label={
lastWeekBookings.length > 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"
)}
/>
</CardFooter>
</Card>
<Card x-chunk="dashboard-05-chunk-2">
<CardHeader className="pb-2">
<CardDescription>This Month</CardDescription>
<CardTitle className="text-4xl">{thisMonthBookings.length}</CardTitle>
</CardHeader>
<CardContent>
<div className="text-xs text-muted-foreground">
{lastMonthBookings.length > 0
? `${changeFromPrevious(thisMonthBookings.length, lastMonthBookings.length)}% from last week`
: "From 0 last month"}
</div>
</CardContent>
<CardFooter>
<Progress
value={Math.abs(changeFromPrevious(thisMonthBookings.length, lastMonthBookings.length))}
aria-label={
lastMonthBookings.length > 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"
)}
/>
</CardFooter>
</Card>
</div>
<Suspense>
<CalAccount>
{(calAccount) => (
<BookingsTable
bookings={{
all: bookings,
currentWeek: thisWeekBookings,
currentMonth: thisMonthBookings,
currentYear: thisYearBookings,
}}
user={{
timeZone: calAccount.timeZone,
username: calAccount.username,
email: stripCalOAuthClientIdFromEmail(calAccount.email),
}}
/>
)}
</CalAccount>
</Suspense>
</main>
);
}
================================================
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 (
<form action={submitAction} className="flex flex-col gap-4">
{props.name === "bio" ? (
<Textarea
{...(props as TextareaProps)}
className="min-w-72 text-balance text-sm leading-relaxed text-muted-foreground"
disabled={isPendingAction}
/>
) : (
<Input {...(props as InputProps)} disabled={isPendingAction} />
)}
{/* display action states (pending, idle, success & error) */}
{isPendingAction ? (
<CardDescription>Saving...</CardDescription>
) : "success" in state && state.success ? (
<CardDescription>{state.success}</CardDescription>
) : "error" in state && state.error ? (
<CardDescription className="text-red-900">{state.error}</CardDescription>
) : (
<CardDescription>
Provide a new {props.name} and hit save to reflect the changes on your public page.
</CardDescription>
)}
<ButtonSubmit size="sm" variant="default">
Save
</ButtonSubmit>
</form>
);
}
================================================
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 (
<div className="grid gap-6 [&>div]:rounded-lg [&>div]:border [&>div]:bg-card [&>div]:text-card-foreground [&>div]:shadow-sm">
<AvailabilitySettings
customClassNames={{
// this is to avoid layout shift when toggling days
scheduleClassNames: {
scheduleDay: "min-w-[480px]",
},
containerClassName: "!font-sans !p-6",
editableHeadingClassName:
"!text-2xl !font-semibold !leading-none !tracking-tight !pr-4 !text-foreground min-w-[20rem]",
subtitlesClassName: "!text-sm !leading-relaxed !max-w-lg !text-balance !text-muted-foreground",
}}
onUpdateSuccess={() => {
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");
}}
/>
</div>
);
};
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<SupabaseStorage["uploadToSignedUrl"]>[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<string | null>(`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) => (
// <li key={file.path}>
// {file.path} - {file.size} bytes
// </li>
// ));
// const fileRejectionItems = fileRejections.map(({ file, errors }) => (
// <li key={file.path}>
// {file.path} - {file.size} bytes
// <ul>
// {errors.map((e) => (
// <li key={e.code}>{e.message}</li>
// ))}
// </ul>
// </li>
// ));
return (
<div className="mx-auto grid w-full gap-4">
{status === "error" ? (
<div className="aspect-square size-16 rounded-md bg-muted" />
) : status === "loading" || !avatar ? (
<Skeleton className="aspect-square size-16 rounded-md" />
) : (
<Image
alt="Expert image"
className="aspect-square rounded-md object-cover"
src={avatar}
height="64"
width="64"
onLoadingComplete={() => setStatus("success")}
onLoad={() => setStatus("loading")}
onError={() => setStatus("error")}
/>
)}
<div
{...getRootProps({
className: cn(
"dropzone border-dashed border px-3 py-8 rounded-md hover:border-foreground/40 cursor-pointer"
),
})}>
<input {...getInputProps()} />
<p className="text-sm text-muted-foreground">Only *.jpeg, *.png and *.avif images will be accepted</p>
</div>
</div>
);
}
================================================
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 <SettingsContent />;
}
================================================
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<typeof post_EventTypesController_createEventType.parameters>;
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<Parameters<typeof useActionState>["2"]> }) {
const [state, submitAction, isPendingAction] = useActionState<
{ error: string | null } | { success: string | null },
FormData
>(createEventType, { error: null }, props.permalink);
return (
<form action={submitAction} className={cn(className)}>
{isPendingAction ? (
<DialogDescription>Saving...</DialogDescription>
) : "success" in state && state.success ? (
<DialogDescription>{state.success} You can close the dialog now.</DialogDescription>
) : "error" in state && state.error ? (
<DialogDescription>{state.error.replace("'", "’")}</DialogDescription>
) : (
<DialogDescription>
Create your new booking event here. Click save when you’re done.
</DialogDescription>
)}
{children}
</form>
);
}
================================================
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 (
<DropdownMenuItem className="cursor-pointer" onClick={handleDelete}>
{isPendingAction ? "Deleting..." : "Delete"}
</DropdownMenuItem>
);
}
================================================
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 <div>Not logged in</div>;
}
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 (
<Fragment>
<div className="flex items-center">
<div className="mr-auto flex items-center gap-2">
{/* TODO: add filter logic via url params */}
{/* <DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8 gap-1">
<ListFilter className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu> */}
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="h-8 gap-1">
<PlusCircle className="size-3.5" />
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">Add Event Type</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create a new Booking Event</DialogTitle>
</DialogHeader>
<EventTypeCreateForm permalink="/dashboard/settings/booking-events">
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
{(
[
{
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 }) => (
<Fragment key={name}>
<Label htmlFor={name} className="text-right">
{label}
</Label>
{name === "description" ? (
<Textarea id={name} name={name} {...inputAttributes} className="col-span-3" />
) : (
<Input id={name} name={name} {...inputAttributes} className="col-span-3" />
)}
</Fragment>
))}
</div>
</div>
<DialogFooter className="sm:justify-content justify-center">
<ButtonSubmit variant="default">Save</ButtonSubmit>
</DialogFooter>
</EventTypeCreateForm>
</DialogContent>
</Dialog>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Event Types</CardTitle>
<CardDescription>Manage your event type and view their sales performance.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Locations</TableHead>
<TableHead className="hidden md:table-cell">Duration (min)</TableHead>
<TableHead>
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{eventTypes.map((eventType) => (
<TableRow key={eventType.id}>
<TableCell>
<div className="font-medium capitalize">{eventType.title}</div>
<div className="hidden text-sm text-muted-foreground md:inline">/{eventType.slug}</div>
</TableCell>
<TableCell>
<div className="hidden text-sm text-muted-foreground md:inline">
{eventType.description}
</div>
</TableCell>
<TableCell>
{eventType.locations?.map((location, idx) => (
<Badge key={idx} variant="default">
{location.type === "integrations:daily" && (
<div className="text-emphasis inline-flex items-center justify-center gap-x-1 text-xs font-medium leading-3">
<Video className="size-3" />
Cal Video
</div>
)}
</Badge>
))}
</TableCell>
<TableCell className="hidden md:table-cell">{eventType.length}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup="true" size="icon" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<EventTypeDelete eventTypeId={eventType.id} />
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className="text-xs text-muted-foreground">
Showing{" "}
<strong>
{eventTypes.length > 0 ? 1 : 0}-{eventTypes.length > 10 ? 10 : eventTypes.length}
</strong>{" "}
of <strong>{eventTypes.length}</strong> event types
</div>
</CardFooter>
</Card>
</Fragment>
);
}
================================================
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/layout.tsx
================================================
export default function SettingsLayout(props: { children: React.ReactNode }) {
return (
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-8">
{props.children}
</main>
);
}
================================================
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 <div>Not logged in</div>;
}
return (
<div className="grid gap-6">
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Image</CardTitle>
<CardDescription>Used on your public profile, once it is approved.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
<SupabaseReactDropzone userId={expert.id} />
</div>
</CardContent>
<CardFooter className="border-t px-6 py-6">
<CardDescription className="flex items-center gap-1">
<Info className="size-3.5" />
The Image upload auto-saves.
</CardDescription>
</CardFooter>
</Card>
<Card x-chunk="dashboard-04-chunk-1">
<CardHeader>
<CardTitle>Name</CardTitle>
<CardDescription>Used on your public profile, once it is approved.</CardDescription>
</CardHeader>
<CardContent>
<ExpertEditForm id="name" name="name" placeholder={expert.name ?? "Your name"} />
</CardContent>
</Card>
<Card x-chunk="dashboard-04-chunk-2">
<CardHeader>
<CardTitle>Bio</CardTitle>
<CardDescription>
A couple of sentences about yourself. This will be displayed on your public profile.
</CardDescription>
</CardHeader>
<CardContent>
<ExpertEditForm id="bio" name="bio" placeholder={expert.bio ?? "Your Bio"} />
</CardContent>
</Card>
</div>
);
}
================================================
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 */
<html lang="en" dir="ltr">
<head />
<AxiomWebVitals />
<body className={cn("antialiased", calFont.variable, interFont.variable)}>
<Providers defaultTheme="system" enableSystem attribute="class">
<div className="flex min-h-screen flex-col">
<Banner
title="Build your own marketplace"
description="Use our Platform Starter Kit to go live in 15 minutes."
ctaLink="https://go.cal.com/starter-kit"
/>
<UseCalAtoms
calAccessToken={currentUser().then((dbUser) => dbUser?.calAccessToken ?? null) ?? null}>
{children}
</UseCalAtoms>
</div>
<TailwindIndicator />
</Providers>
<Toaster />
</body>
<Analytics />
</html>
);
}
================================================
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 (
<div className="flex rounded-md bg-muted shadow-sm ring-1 ring-inset ring-input ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring focus-within:ring-offset-2 sm:max-w-md">
<span className="flex h-10 select-none items-center border-none pl-3 text-muted-foreground sm:text-sm">
{props.prefix}
</span>
{props.children}
</div>
);
};
export const AddonFieldInput = (props: { className?: string } & InputProps) => {
const { className } = props;
return (
<Input
{...props}
className={cn(
"ml-1 rounded-l-none border-0 ring-0 ring-input focus:ring-0 focus-visible:outline-0 focus-visible:ring-0 focus-visible:ring-inset",
className
)}
/>
);
};
================================================
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<LoginFormState, FormData>(signInWithCredentials, {
error: null,
});
return (
<form action={dispatch}>
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login in to your account.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" placeholder="m@example.com" required />
{formState?.inputErrors?.email ? (
<div className="text-sm font-medium text-red-700" aria-live="polite">
{formState.inputErrors.email[0]}
</div>
) : null}
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input id="password" name="password" type="password" required />
{formState?.inputErrors?.password ? (
<div className="text-sm font-medium text-red-700" aria-live="polite">
{formState.inputErrors.password[0]}
</div>
) : null}
</div>
<input hidden name="redirectTo" value="/dashboard/getting-started" readOnly />
{!!formState?.error && <p className="text-sm font-medium text-red-900">{formState.error}</p>}
</CardContent>
<CardFooter>
<div className="flex w-full flex-col">
<ButtonSubmit variant="default" className="w-full">
Log in
</ButtonSubmit>
<div className="mt-4 text-center text-sm">
Don't have an account?{" "}
<Link href="/signup" className="underline">
Sign up
</Link>{" "}
instead.
</div>
</div>
</CardFooter>
</Card>
</form>
);
}
================================================
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 (
<>
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 max-w-screen-2xl item
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
SYMBOL INDEX (324 symbols across 75 files)
FILE: with-platform-supabase-tailwind-prisma/prisma/migrations/0_init/migration.sql
type "prisma" (line 8) | CREATE TABLE "prisma"."Account" (
type "prisma" (line 27) | CREATE TABLE "prisma"."Session" (
type "prisma" (line 38) | CREATE TABLE "prisma"."User" (
type "prisma" (line 57) | CREATE TABLE "prisma"."CalAccount" (
type "prisma" (line 72) | CREATE TABLE "prisma"."VerificationToken" (
type "prisma" (line 80) | CREATE TABLE "prisma"."FilterOption" (
type "prisma" (line 91) | CREATE TABLE "prisma"."FilterOptionsOnUser" (
type "prisma" (line 99) | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "prisma"...
type "prisma" (line 102) | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "prisma"."Session"("se...
type "prisma" (line 105) | CREATE UNIQUE INDEX "User_username_key" ON "prisma"."User"("username")
type "prisma" (line 108) | CREATE UNIQUE INDEX "User_email_key" ON "prisma"."User"("email")
type "prisma" (line 111) | CREATE UNIQUE INDEX "User_calAccountId_key" ON "prisma"."User"("calAccou...
type "prisma" (line 114) | CREATE UNIQUE INDEX "User_calAccessToken_key" ON "prisma"."User"("calAcc...
type "prisma" (line 117) | CREATE UNIQUE INDEX "User_calRefreshToken_key" ON "prisma"."User"("calRe...
type "prisma" (line 120) | CREATE UNIQUE INDEX "CalAccount_username_key" ON "prisma"."CalAccount"("...
type "prisma" (line 123) | CREATE UNIQUE INDEX "CalAccount_email_key" ON "prisma"."CalAccount"("ema...
type "prisma" (line 126) | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "prisma"."Verificat...
type "prisma" (line 129) | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "prisma"...
type "prisma" (line 132) | CREATE UNIQUE INDEX "FilterOption_fieldId_key" ON "prisma"."FilterOption...
type "prisma" (line 135) | CREATE INDEX "FilterOption_fieldId_filterCategoryFieldId_idx" ON "prisma...
type "prisma" (line 138) | CREATE UNIQUE INDEX "FilterOption_fieldId_filterCategoryFieldId_key" ON ...
type "prisma" (line 141) | CREATE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCateg...
type "prisma" (line 144) | CREATE UNIQUE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filt...
FILE: with-platform-supabase-tailwind-prisma/prisma/seed.ts
function main (line 6) | async function main() {
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/[eventSlug]/page.tsx
function BookerPage (line 8) | async function BookerPage({
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/AboutSection.tsx
function AboutSection (line 9) | function AboutSection(props: React.ComponentPropsWithoutRef<'section'>) {
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/Container.tsx
function Container (line 3) | function Container({ className, children, ...props }: React.ComponentPro...
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/expert-booker.tsx
type BookerProps (line 13) | type BookerProps = Parameters<typeof Booker>[number];
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/booking/[bookingUid]/page.tsx
function Booking (line 4) | function Booking() {
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/layout.tsx
function ExpertLayout (line 8) | function ExpertLayout({ children }: { children?: ReactNode }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/page.tsx
function ExpertDetails (line 12) | async function ExpertDetails({ params }: { params: { expertUsername: str...
FILE: with-platform-supabase-tailwind-prisma/src/app/_actions.tsx
function signInWithCredentials (line 13) | async function signInWithCredentials(_prevState: LoginFormState, formDat...
function addUserFilters (line 45) | async function addUserFilters(_prevState: { error?: string | null }, for...
function signUpWithCredentials (line 113) | async function signUpWithCredentials(_prevState: { error?: string | null...
function expertEdit (line 147) | async function expertEdit(
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/autocomplete.tsx
type Option (line 32) | type Option = { value: string; label: string };
type AutocompleteSearchProps (line 34) | interface AutocompleteSearchProps extends React.ComponentPropsWithoutRef...
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/banner.tsx
function Banner (line 3) | function Banner({
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/home/results.tsx
function ResultsCard (line 23) | function ResultsCard({
type UsersWithFilterOptions (line 97) | type UsersWithFilterOptions = Awaited<
function Results (line 104) | function Results(props: { experts: UsersWithFilterOptions; signedOut: JS...
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/home/sidebar-item.tsx
function SidebarItem (line 10) | function SidebarItem({
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/multi-select.tsx
type Option (line 9) | type Option = Record<"value" | "label", string>;
function FancyMultiSelect (line 11) | function FancyMultiSelect(
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/navigation.tsx
function Navigation (line 51) | function Navigation() {
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/universal/hero.tsx
type HeroProps (line 3) | interface HeroProps {
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/universal/layout.tsx
type BaseProps (line 4) | type BaseProps = {
type LayoutProps (line 9) | type LayoutProps = BaseProps & {
FILE: with-platform-supabase-tailwind-prisma/src/app/_components/use-cal.tsx
function UseCalAtoms (line 26) | function UseCalAtoms(props: {
FILE: with-platform-supabase-tailwind-prisma/src/app/_searchParams.ts
type RenderingOptions (line 12) | type RenderingOptions = (typeof renderingOptions)[number];
function nonEmptyArray (line 13) | function nonEmptyArray<ElementType>(arr: Array<ElementType>): [ElementTy...
FILE: with-platform-supabase-tailwind-prisma/src/app/api/cal/refresh/route.ts
constant GET (line 8) | const GET = NextAuth(authConfig).auth(async function GET(request) {
FILE: with-platform-supabase-tailwind-prisma/src/app/api/supabase/storage/route.ts
function GET (line 6) | async function GET(request: Request) {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/[...dashboardSegments]/page.tsx
function BreadcrumbsSlot (line 12) | function BreadcrumbsSlot(props: {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/page.tsx
function BreadcrumbsSlot (line 4) | function BreadcrumbsSlot() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/[...dashboardSegments]/page.tsx
function DashboardNavigationDesktopSlot (line 5) | function DashboardNavigationDesktopSlot(props: {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/page.tsx
function DashboardNavigationDesktopDefault (line 6) | function DashboardNavigationDesktopDefault() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/[...dashboardSegments]/page.tsx
function DashboardNavigationMobileSlot (line 6) | function DashboardNavigationMobileSlot(props: {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/page.tsx
function DashboardNavigationMobileDefault (line 7) | function DashboardNavigationMobileDefault() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-details-step.tsx
type UserDetailsFormState (line 12) | type UserDetailsFormState = { error: null | string } | { success: null |...
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-filters-step.tsx
type TUserFiltersFormState (line 12) | type TUserFiltersFormState = {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/getting-started/page.tsx
function Dashboard (line 5) | async function Dashboard() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/layout.tsx
function Layout (line 18) | async function Layout({
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/page.tsx
function Dashboard (line 17) | async function Dashboard() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/expert-edit.tsx
function ExpertEditForm (line 10) | function ExpertEditForm(props: InputProps | TextareaProps) {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/supabase-react-dropzone.tsx
type SupabaseStorage (line 12) | type SupabaseStorage = (typeof StorageFileApi)["prototype"];
type FileBody (line 13) | type FileBody = Parameters<SupabaseStorage["uploadToSignedUrl"]>[2];
function SupabaseReactDropzone (line 14) | function SupabaseReactDropzone({ userId }: { userId: string; userInitial...
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/availability/page.tsx
function DashboardSettingsAvailability (line 3) | async function DashboardSettingsAvailability() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/_actions.ts
function createEventType (line 9) | async function createEventType(
function deleteEventType (line 66) | async function deleteEventType(
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-create.tsx
function EventTypeCreateForm (line 8) | function EventTypeCreateForm({
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-delete.tsx
function EventTypeDelete (line 8) | function EventTypeDelete({ eventTypeId }: { eventTypeId: number }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/page.tsx
function DashboardSettingsBookingEvents (line 30) | async function DashboardSettingsBookingEvents() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/layout.tsx
function SettingsLayout (line 1) | function SettingsLayout(props: { children: React.ReactNode }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/page.tsx
function SettingsOutlet (line 1) | function SettingsOutlet() {
FILE: with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/profile/page.tsx
function DashboardSettingsProfile (line 9) | async function DashboardSettingsProfile() {
FILE: with-platform-supabase-tailwind-prisma/src/app/layout.tsx
function RootLayout (line 57) | async function RootLayout({ children }: { children: React.ReactNode }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/login/_components/login.tsx
type LoginFormState (line 11) | type LoginFormState =
function LoginForm (line 28) | function LoginForm() {
FILE: with-platform-supabase-tailwind-prisma/src/app/login/layout.tsx
function LoginLayout (line 15) | function LoginLayout({ children }: { children?: ReactNode }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/login/page.tsx
function LoginPage (line 3) | function LoginPage() {
FILE: with-platform-supabase-tailwind-prisma/src/app/page.tsx
function Home (line 12) | async function Home() {
FILE: with-platform-supabase-tailwind-prisma/src/app/providers.tsx
function Providers (line 8) | function Providers({ children, ...props }: ThemeProviderProps) {
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/_components/signup.tsx
type TSignUpFormState (line 12) | type TSignUpFormState = {
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/layout.tsx
function SignupLayout (line 15) | function SignupLayout({ children }: { children?: ReactNode }) {
FILE: with-platform-supabase-tailwind-prisma/src/app/signup/page.tsx
function SignupPage (line 3) | async function SignupPage() {
FILE: with-platform-supabase-tailwind-prisma/src/app/tailwind-indicator.tsx
function TailwindIndicator (line 3) | function TailwindIndicator() {
FILE: with-platform-supabase-tailwind-prisma/src/auth/config.edge.ts
type Session (line 11) | interface Session {
type JWT (line 18) | interface JWT extends DefaultJWT {
method authorized (line 115) | authorized({ auth, request: { nextUrl } }) {
FILE: with-platform-supabase-tailwind-prisma/src/auth/index.tsx
function hash (line 17) | async function hash(password: string) {
function compare (line 30) | async function compare(password: string, hash: string) {
type UserAfterSignUp (line 82) | type UserAfterSignUp = User & { calAccount?: CalAccount };
function SignedIn (line 192) | async function SignedIn(props: { children: (props: { user: Session["user...
function SignedOut (line 197) | async function SignedOut(props: { children: React.ReactNode }) {
function CurrentUser (line 202) | async function CurrentUser(props: { children: (props: User) => React.Rea...
function CalAccount (line 207) | async function CalAccount(props: { children: (props: CalAccount) => Reac...
FILE: with-platform-supabase-tailwind-prisma/src/cal/__generated/cal-sdk.ts
type ManagedUserOutput (line 4) | type ManagedUserOutput = z.infer<typeof ManagedUserOutput>;
type GetManagedUsersOutput (line 16) | type GetManagedUsersOutput = z.infer<typeof GetManagedUsersOutput>;
type CreateManagedUserInput (line 22) | type CreateManagedUserInput = z.infer<typeof CreateManagedUserInput>;
type CreateManagedUserData (line 42) | type CreateManagedUserData = z.infer<typeof CreateManagedUserData>;
type CreateManagedUserOutput (line 49) | type CreateManagedUserOutput = z.infer<typeof CreateManagedUserOutput>;
type GetManagedUserOutput (line 55) | type GetManagedUserOutput = z.infer<typeof GetManagedUserOutput>;
type UpdateManagedUserInput (line 61) | type UpdateManagedUserInput = z.infer<typeof UpdateManagedUserInput>;
type KeysDto (line 81) | type KeysDto = z.infer<typeof KeysDto>;
type KeysResponseDto (line 87) | type KeysResponseDto = z.infer<typeof KeysResponseDto>;
type CreateOAuthClientInput (line 93) | type CreateOAuthClientInput = z.infer<typeof CreateOAuthClientInput>;
type DataDto (line 96) | type DataDto = z.infer<typeof DataDto>;
type CreateOAuthClientResponseDto (line 102) | type CreateOAuthClientResponseDto = z.infer<typeof CreateOAuthClientResp...
type PlatformOAuthClientDto (line 108) | type PlatformOAuthClientDto = z.infer<typeof PlatformOAuthClientDto>;
type GetOAuthClientsResponseDto (line 120) | type GetOAuthClientsResponseDto = z.infer<typeof GetOAuthClientsResponse...
type GetOAuthClientResponseDto (line 126) | type GetOAuthClientResponseDto = z.infer<typeof GetOAuthClientResponseDto>;
type UpdateOAuthClientInput (line 132) | type UpdateOAuthClientInput = z.infer<typeof UpdateOAuthClientInput>;
type OAuthAuthorizeInput (line 143) | type OAuthAuthorizeInput = z.infer<typeof OAuthAuthorizeInput>;
type ExchangeAuthorizationCodeInput (line 148) | type ExchangeAuthorizationCodeInput = z.infer<typeof ExchangeAuthorizati...
type RefreshTokenInput (line 153) | type RefreshTokenInput = z.infer<typeof RefreshTokenInput>;
type EventTypeLocation (line 158) | type EventTypeLocation = z.infer<typeof EventTypeLocation>;
type CreateEventTypeInput (line 164) | type CreateEventTypeInput = z.infer<typeof CreateEventTypeInput>;
type EventTypeOutput (line 174) | type EventTypeOutput = z.infer<typeof EventTypeOutput>;
type CreateEventTypeOutput (line 184) | type CreateEventTypeOutput = z.infer<typeof CreateEventTypeOutput>;
type Data (line 190) | type Data = z.infer<typeof Data>;
type GetEventTypeOutput (line 195) | type GetEventTypeOutput = z.infer<typeof GetEventTypeOutput>;
type EventTypeGroup (line 201) | type EventTypeGroup = z.infer<typeof EventTypeGroup>;
type GetEventTypesData (line 206) | type GetEventTypesData = z.infer<typeof GetEventTypesData>;
type GetEventTypesOutput (line 211) | type GetEventTypesOutput = z.infer<typeof GetEventTypesOutput>;
type Location (line 217) | type Location = z.infer<typeof Location>;
type Source (line 222) | type Source = z.infer<typeof Source>;
type BookingField (line 229) | type BookingField = z.infer<typeof BookingField>;
type Organization (line 243) | type Organization = z.infer<typeof Organization>;
type Profile (line 251) | type Profile = z.infer<typeof Profile>;
type Owner (line 268) | type Owner = z.infer<typeof Owner>;
type Schedule (line 284) | type Schedule = z.infer<typeof Schedule>;
type User (line 290) | type User = z.infer<typeof User>;
type PublicEventTypeOutput (line 301) | type PublicEventTypeOutput = z.infer<typeof PublicEventTypeOutput>;
type GetEventTypePublicOutput (line 339) | type GetEventTypePublicOutput = z.infer<typeof GetEventTypePublicOutput>;
type PublicEventType (line 345) | type PublicEventType = z.infer<typeof PublicEventType>;
type GetEventTypesPublicOutput (line 354) | type GetEventTypesPublicOutput = z.infer<typeof GetEventTypesPublicOutput>;
type UpdateEventTypeInput (line 360) | type UpdateEventTypeInput = z.infer<typeof UpdateEventTypeInput>;
type UpdateEventTypeOutput (line 371) | type UpdateEventTypeOutput = z.infer<typeof UpdateEventTypeOutput>;
type DeleteData (line 377) | type DeleteData = z.infer<typeof DeleteData>;
type DeleteEventTypeOutput (line 385) | type DeleteEventTypeOutput = z.infer<typeof DeleteEventTypeOutput>;
type CreateAvailabilityInput (line 391) | type CreateAvailabilityInput = z.infer<typeof CreateAvailabilityInput>;
type CreateScheduleInput (line 398) | type CreateScheduleInput = z.infer<typeof CreateScheduleInput>;
type WorkingHours (line 406) | type WorkingHours = z.infer<typeof WorkingHours>;
type AvailabilityModel (line 414) | type AvailabilityModel = z.infer<typeof AvailabilityModel>;
type TimeRange (line 426) | type TimeRange = z.infer<typeof TimeRange>;
type ScheduleOutput (line 433) | type ScheduleOutput = z.infer<typeof ScheduleOutput>;
type CreateScheduleOutput (line 448) | type CreateScheduleOutput = z.infer<typeof CreateScheduleOutput>;
type GetDefaultScheduleOutput (line 454) | type GetDefaultScheduleOutput = z.infer<typeof GetDefaultScheduleOutput>;
type GetScheduleOutput (line 460) | type GetScheduleOutput = z.infer<typeof GetScheduleOutput>;
type GetSchedulesOutput (line 466) | type GetSchedulesOutput = z.infer<typeof GetSchedulesOutput>;
type UpdateScheduleInput (line 472) | type UpdateScheduleInput = z.infer<typeof UpdateScheduleInput>;
type EventTypeModel (line 481) | type EventTypeModel = z.infer<typeof EventTypeModel>;
type ScheduleModel (line 487) | type ScheduleModel = z.infer<typeof ScheduleModel>;
type UpdatedScheduleOutput (line 497) | type UpdatedScheduleOutput = z.infer<typeof UpdatedScheduleOutput>;
type UpdateScheduleOutput (line 506) | type UpdateScheduleOutput = z.infer<typeof UpdateScheduleOutput>;
type DeleteScheduleOutput (line 512) | type DeleteScheduleOutput = z.infer<typeof DeleteScheduleOutput>;
type AuthUrlData (line 517) | type AuthUrlData = z.infer<typeof AuthUrlData>;
type GcalAuthUrlOutput (line 522) | type GcalAuthUrlOutput = z.infer<typeof GcalAuthUrlOutput>;
type GcalSaveRedirectOutput (line 528) | type GcalSaveRedirectOutput = z.infer<typeof GcalSaveRedirectOutput>;
type GcalCheckOutput (line 533) | type GcalCheckOutput = z.infer<typeof GcalCheckOutput>;
type ProviderVerifyClientOutput (line 538) | type ProviderVerifyClientOutput = z.infer<typeof ProviderVerifyClientOut...
type ProviderVerifyAccessTokenOutput (line 543) | type ProviderVerifyAccessTokenOutput = z.infer<typeof ProviderVerifyAcce...
type MeOutput (line 548) | type MeOutput = z.infer<typeof MeOutput>;
type GetMeOutput (line 559) | type GetMeOutput = z.infer<typeof GetMeOutput>;
type UpdateMeOutput (line 565) | type UpdateMeOutput = z.infer<typeof UpdateMeOutput>;
type BusyTimesOutput (line 571) | type BusyTimesOutput = z.infer<typeof BusyTimesOutput>;
type GetBusyTimesOutput (line 578) | type GetBusyTimesOutput = z.infer<typeof GetBusyTimesOutput>;
type Integration (line 584) | type Integration = z.infer<typeof Integration>;
type Primary (line 605) | type Primary = z.infer<typeof Primary>;
type Calendar (line 617) | type Calendar = z.infer<typeof Calendar>;
type ConnectedCalendar (line 629) | type ConnectedCalendar = z.infer<typeof ConnectedCalendar>;
type DestinationCalendar (line 637) | type DestinationCalendar = z.infer<typeof DestinationCalendar>;
type ConnectedCalendarsData (line 653) | type ConnectedCalendarsData = z.infer<typeof ConnectedCalendarsData>;
type ConnectedCalendarsOutput (line 659) | type ConnectedCalendarsOutput = z.infer<typeof ConnectedCalendarsOutput>;
type Attendee (line 665) | type Attendee = z.infer<typeof Attendee>;
type EventType (line 675) | type EventType = z.infer<typeof EventType>;
type Reference (line 689) | type Reference = z.infer<typeof Reference>;
type GetBookingsDataEntry (line 704) | type GetBookingsDataEntry = z.infer<typeof GetBookingsDataEntry>;
type GetBookingsData (line 729) | type GetBookingsData = z.infer<typeof GetBookingsData>;
type GetBookingsOutput (line 736) | type GetBookingsOutput = z.infer<typeof GetBookingsOutput>;
type GetBookingData (line 742) | type GetBookingData = z.infer<typeof GetBookingData>;
type GetBookingOutput (line 766) | type GetBookingOutput = z.infer<typeof GetBookingOutput>;
type Response (line 772) | type Response = z.infer<typeof Response>;
type CreateBookingInput (line 781) | type CreateBookingInput = z.infer<typeof CreateBookingInput>;
type CancelBookingInput (line 802) | type CancelBookingInput = z.infer<typeof CancelBookingInput>;
type ReserveSlotInput (line 811) | type ReserveSlotInput = z.infer<typeof ReserveSlotInput>;
type get_OAuthClientUsersController_getManagedUsers (line 814) | type get_OAuthClientUsersController_getManagedUsers =
type post_OAuthClientUsersController_createUser (line 827) | type post_OAuthClientUsersController_createUser = typeof post_OAuthClien...
type get_OAuthClientUsersController_getUserById (line 840) | type get_OAuthClientUsersController_getUserById = typeof get_OAuthClient...
type patch_OAuthClientUsersController_updateUser (line 853) | type patch_OAuthClientUsersController_updateUser = typeof patch_OAuthCli...
type delete_OAuthClientUsersController_deleteUser (line 867) | type delete_OAuthClientUsersController_deleteUser =
type post_OAuthClientUsersController_forceRefresh (line 881) | type post_OAuthClientUsersController_forceRefresh =
type post_OAuthClientsController_createOAuthClient (line 895) | type post_OAuthClientsController_createOAuthClient =
type get_OAuthClientsController_getOAuthClients (line 906) | type get_OAuthClientsController_getOAuthClients = typeof get_OAuthClient...
type get_OAuthClientsController_getOAuthClientById (line 914) | type get_OAuthClientsController_getOAuthClientById =
type patch_OAuthClientsController_updateOAuthClient (line 927) | type patch_OAuthClientsController_updateOAuthClient =
type delete_OAuthClientsController_deleteOAuthClient (line 941) | type delete_OAuthClientsController_deleteOAuthClient =
type get_OAuthClientsController_getOAuthClientManagedUsersById (line 954) | type get_OAuthClientsController_getOAuthClientManagedUsersById =
type post_OAuthFlowController_authorize (line 967) | type post_OAuthFlowController_authorize = typeof post_OAuthFlowControlle...
type post_OAuthFlowController_exchange (line 980) | type post_OAuthFlowController_exchange = typeof post_OAuthFlowController...
type post_OAuthFlowController_refreshAccessToken (line 996) | type post_OAuthFlowController_refreshAccessToken = typeof post_OAuthFlow...
type post_EventTypesController_createEventType (line 1012) | type post_EventTypesController_createEventType = typeof post_EventTypesC...
type get_EventTypesController_getEventTypes (line 1022) | type get_EventTypesController_getEventTypes = typeof get_EventTypesContr...
type get_EventTypesController_getEventType (line 1030) | type get_EventTypesController_getEventType = typeof get_EventTypesContro...
type patch_EventTypesController_updateEventType (line 1042) | type patch_EventTypesController_updateEventType = typeof patch_EventType...
type delete_EventTypesController_deleteEventType (line 1055) | type delete_EventTypesController_deleteEventType = typeof delete_EventTy...
type get_EventTypesController_getPublicEventType (line 1067) | type get_EventTypesController_getPublicEventType = typeof get_EventTypes...
type get_EventTypesController_getPublicEventTypes (line 1084) | type get_EventTypesController_getPublicEventTypes =
type post_SchedulesController_createSchedule (line 1097) | type post_SchedulesController_createSchedule = typeof post_SchedulesCont...
type get_SchedulesController_getSchedules (line 1107) | type get_SchedulesController_getSchedules = typeof get_SchedulesControll...
type get_SchedulesController_getDefaultSchedule (line 1115) | type get_SchedulesController_getDefaultSchedule = typeof get_SchedulesCo...
type get_SchedulesController_getSchedule (line 1123) | type get_SchedulesController_getSchedule = typeof get_SchedulesControlle...
type patch_SchedulesController_updateSchedule (line 1135) | type patch_SchedulesController_updateSchedule = typeof patch_SchedulesCo...
type delete_SchedulesController_deleteSchedule (line 1148) | type delete_SchedulesController_deleteSchedule = typeof delete_Schedules...
type get_BookingsController_getBookings (line 1160) | type get_BookingsController_getBookings = typeof get_BookingsController_...
type post_BookingsController_createBooking (line 1180) | type post_BookingsController_createBooking = typeof post_BookingsControl...
type get_BookingsController_getBooking (line 1193) | type get_BookingsController_getBooking = typeof get_BookingsController_g...
type get_BookingsController_getBookingForReschedule (line 1205) | type get_BookingsController_getBookingForReschedule =
type post_BookingsController_cancelBooking (line 1218) | type post_BookingsController_cancelBooking = typeof post_BookingsControl...
type post_BookingsController_createRecurringBooking (line 1234) | type post_BookingsController_createRecurringBooking =
type post_BookingsController_createInstantBooking (line 1248) | type post_BookingsController_createInstantBooking =
type EndpointByMethod (line 1308) | type EndpointByMethod = typeof EndpointByMethod;
type GetEndpoints (line 1312) | type GetEndpoints = EndpointByMethod["get"];
type PostEndpoints (line 1313) | type PostEndpoints = EndpointByMethod["post"];
type PatchEndpoints (line 1314) | type PatchEndpoints = EndpointByMethod["patch"];
type DeleteEndpoints (line 1315) | type DeleteEndpoints = EndpointByMethod["delete"];
type AllEndpoints (line 1316) | type AllEndpoints = EndpointByMethod[keyof EndpointByMethod];
type EndpointParameters (line 1320) | type EndpointParameters = {
type MutationMethod (line 1327) | type MutationMethod = "post" | "put" | "patch" | "delete";
type Method (line 1328) | type Method = "get" | "head" | MutationMethod;
type DefaultEndpoint (line 1330) | type DefaultEndpoint = {
type Endpoint (line 1335) | type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
type Fetcher (line 1348) | type Fetcher = (
type RequiredKeys (line 1354) | type RequiredKeys<T> = {
type MaybeOptionalArg (line 1358) | type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] ...
class ApiClient (line 1363) | class ApiClient {
method constructor (line 1366) | constructor(public fetcher: Fetcher) {}
method setBaseUrl (line 1368) | setBaseUrl(baseUrl: string) {
method get (line 1374) | get<Path extends keyof GetEndpoints, TEndpoint extends GetEndpoints[Pa...
method post (line 1383) | post<Path extends keyof PostEndpoints, TEndpoint extends PostEndpoints...
method patch (line 1392) | patch<Path extends keyof PatchEndpoints, TEndpoint extends PatchEndpoi...
method delete (line 1401) | delete<Path extends keyof DeleteEndpoints, TEndpoint extends DeleteEnd...
function createApiClient (line 1410) | function createApiClient(fetcher: Fetcher, baseUrl?: string) {
FILE: with-platform-supabase-tailwind-prisma/src/cal/api.ts
type SDKInput (line 20) | type SDKInput = Pick<User, "id"> & Partial<Pick<User, "calAccessToken" |...
type KeysSuccessData (line 195) | type KeysSuccessData = z.infer<typeof KeysSuccessDto>;
type FetchParametersLike (line 445) | type FetchParametersLike = [string | URL, Parameters<typeof fetch>["1"] ...
type CalErrorResponse (line 486) | type CalErrorResponse = {
FILE: with-platform-supabase-tailwind-prisma/src/cal/auth.ts
function signUp (line 6) | async function signUp({ email, name, user }: CreateManagedUserInput & { ...
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/badge.tsx
type BadgeProps (line 23) | interface BadgeProps
function Badge (line 27) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/button.tsx
type ButtonProps (line 32) | interface ButtonProps
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/command.tsx
type CommandProps (line 10) | type CommandProps = React.ElementRef<typeof CommandPrimitive> &
type CommandDialogProps (line 27) | type CommandDialogProps = DialogProps;
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/form.tsx
type FormFieldContextValue (line 17) | type FormFieldContextValue<
type FormItemContextValue (line 62) | type FormItemContextValue = {
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/input.tsx
type InputProps (line 4) | type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/pagination.tsx
type PaginationLinkProps (line 28) | type PaginationLinkProps = {
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/sheet.tsx
type SheetContentProps (line 51) | interface SheetContentProps
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/skeleton.tsx
function Skeleton (line 3) | function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivE...
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/stepper.tsx
type StepperContextValue (line 13) | interface StepperContextValue extends StepperProps {
type StepperContextProviderProps (line 41) | type StepperContextProviderProps = {
function usePrevious (line 87) | function usePrevious<T>(value: T): T | undefined {
function useStepper (line 97) | function useStepper() {
function useMediaQuery (line 127) | function useMediaQuery(query: string) {
type StepItem (line 147) | type StepItem = {
type StepOptions (line 156) | interface StepOptions {
type StepperProps (line 195) | interface StepperProps extends StepOptions {
constant VARIABLE_SIZES (line 202) | const VARIABLE_SIZES = {
type StepProps (line 361) | interface StepProps extends React.HTMLAttributes<HTMLLIElement> {
type StepSharedProps (line 373) | interface StepSharedProps extends StepProps {
type StepInternalConfig (line 383) | interface StepInternalConfig {
type FullStepProps (line 390) | interface FullStepProps extends StepProps, StepInternalConfig {}
type VerticalStepProps (line 452) | type VerticalStepProps = StepSharedProps & {
type StepButtonContainerProps (line 712) | type StepButtonContainerProps = StepSharedProps & {
type IconType (line 762) | type IconType = LucideIcon | React.ComponentType<any> | undefined;
type StepIconProps (line 777) | interface StepIconProps {
type StepLabelProps (line 875) | interface StepLabelProps {
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/textarea.tsx
type TextareaProps (line 4) | type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/toast.tsx
type ToastProps (line 97) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement (line 99) | type ToastActionElement = React.ReactElement<typeof ToastAction>;
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: with-platform-supabase-tailwind-prisma/src/components/ui/use-toast.ts
constant TOAST_LIMIT (line 11) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 12) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 14) | type ToasterToast = ToastProps & {
function genId (line 30) | function genId() {
type ActionType (line 35) | type ActionType = typeof actionTypes
type Action (line 37) | type Action =
type State (line 55) | interface State {
function dispatch (line 136) | function dispatch(action: Action) {
type Toast (line 143) | type Toast = Omit<ToasterToast, "id">
function toast (line 145) | function toast({ ...props }: Toast) {
function useToast (line 174) | function useToast() {
FILE: with-platform-supabase-tailwind-prisma/src/lib/constants.ts
constant IS_PRODUCTION (line 35) | const IS_PRODUCTION = process.env.NODE_ENV === "production";
constant IS_SANDBOX (line 36) | const IS_SANDBOX = process.env.NEXT_PUBLIC_CAL_API_URL === "https://api....
constant IS_CALCOM (line 37) | const IS_CALCOM = process.env.VERCEL_URL === "experts.cal.com";
FILE: with-platform-supabase-tailwind-prisma/src/lib/supabase-image-loader.ts
function supabaseLoader (line 3) | function supabaseLoader({
FILE: with-platform-supabase-tailwind-prisma/src/lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
type Unit (line 19) | type Unit = keyof typeof units;
FILE: with-platform-supabase-tailwind-prisma/supabase/migrations/20240615093934_init.sql
type "prisma" (line 8) | CREATE TABLE "prisma"."Account" (
type "prisma" (line 27) | CREATE TABLE "prisma"."Session" (
type "prisma" (line 38) | CREATE TABLE "prisma"."User" (
type "prisma" (line 57) | CREATE TABLE "prisma"."CalAccount" (
type "prisma" (line 72) | CREATE TABLE "prisma"."VerificationToken" (
type "prisma" (line 80) | CREATE TABLE "prisma"."FilterOption" (
type "prisma" (line 91) | CREATE TABLE "prisma"."FilterOptionsOnUser" (
type "prisma" (line 99) | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "prisma"...
type "prisma" (line 102) | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "prisma"."Session"("se...
type "prisma" (line 105) | CREATE UNIQUE INDEX "User_username_key" ON "prisma"."User"("username")
type "prisma" (line 108) | CREATE UNIQUE INDEX "User_email_key" ON "prisma"."User"("email")
type "prisma" (line 111) | CREATE UNIQUE INDEX "User_calAccountId_key" ON "prisma"."User"("calAccou...
type "prisma" (line 114) | CREATE UNIQUE INDEX "User_calAccessToken_key" ON "prisma"."User"("calAcc...
type "prisma" (line 117) | CREATE UNIQUE INDEX "User_calRefreshToken_key" ON "prisma"."User"("calRe...
type "prisma" (line 120) | CREATE UNIQUE INDEX "CalAccount_username_key" ON "prisma"."CalAccount"("...
type "prisma" (line 123) | CREATE UNIQUE INDEX "CalAccount_email_key" ON "prisma"."CalAccount"("ema...
type "prisma" (line 126) | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "prisma"."Verificat...
type "prisma" (line 129) | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "prisma"...
type "prisma" (line 132) | CREATE UNIQUE INDEX "FilterOption_fieldId_key" ON "prisma"."FilterOption...
type "prisma" (line 135) | CREATE INDEX "FilterOption_fieldId_filterCategoryFieldId_idx" ON "prisma...
type "prisma" (line 138) | CREATE UNIQUE INDEX "FilterOption_fieldId_filterCategoryFieldId_key" ON ...
type "prisma" (line 141) | CREATE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCateg...
type "prisma" (line 144) | CREATE UNIQUE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filt...
type "public" (line 163) | CREATE TABLE IF NOT EXISTS "public"."_prisma_migrations" (
Condensed preview — 128 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (512K chars).
[
{
"path": ".gitignore",
"chars": 603,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "README.md",
"chars": 142,
"preview": "The platform starter kit has been archived and is no longer maintained.\n\nFind more, smaller examples here: https://githu"
},
{
"path": "with-platform-supabase-tailwind-prisma/.eslintrc.cjs",
"chars": 1686,
"preview": "/** @type {import(\"eslint\").Linter.Config} */\nconst config = {\n root: true,\n parser: \"@typescript-eslint/parser\",\n ig"
},
{
"path": "with-platform-supabase-tailwind-prisma/LICENSE",
"chars": 1055,
"preview": "Copyright (c) 2024 Cal.com, Inc\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this so"
},
{
"path": "with-platform-supabase-tailwind-prisma/README.md",
"chars": 8371,
"preview": "<!-- PROJECT LOGO -->\n<p align=\"center\">\n <a href=\"https://github.com/calcom/cal.com\">\n <img src=\"https://github.com/"
},
{
"path": "with-platform-supabase-tailwind-prisma/components.json",
"chars": 349,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/next.config.js",
"chars": 562,
"preview": "import { resolve } from \"path\";\n\n/**\n * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is "
},
{
"path": "with-platform-supabase-tailwind-prisma/package.json",
"chars": 3809,
"preview": "{\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"next build\",\n \"db:studio\": \"prisma studio\",\n "
},
{
"path": "with-platform-supabase-tailwind-prisma/postcss.config.cjs",
"chars": 85,
"preview": "const config = {\n plugins: {\n tailwindcss: {},\n },\n};\n\nmodule.exports = config;\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/prettier.config.mjs",
"chars": 423,
"preview": "/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */\nconst config = {\n bracke"
},
{
"path": "with-platform-supabase-tailwind-prisma/prisma/client.ts",
"chars": 467,
"preview": "import { env } from \"@/env\";\nimport { PrismaClient } from \"@prisma/client\";\n\nconst createPrismaClient = () =>\n new Pris"
},
{
"path": "with-platform-supabase-tailwind-prisma/prisma/migrations/0_init/migration.sql",
"chars": 5512,
"preview": "-- CreateSchema\nCREATE SCHEMA IF NOT EXISTS \"prisma\";\n\n-- CreateEnum\nCREATE TYPE \"prisma\".\"UserStatus\" AS ENUM ('APPROVE"
},
{
"path": "with-platform-supabase-tailwind-prisma/prisma/schema.prisma",
"chars": 3670,
"preview": "generator client {\n provider = \"prisma-client-js\"\n previewFeatures = [\"multiSchema\"]\n}\n\ndatasource db {\n provi"
},
{
"path": "with-platform-supabase-tailwind-prisma/prisma/seed.ts",
"chars": 648,
"preview": "import { filterOptions } from \"@/app/_hardcoded\";\nimport { PrismaClient } from \"@prisma/client\";\n\nconst devDb = new Pris"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/[eventSlug]/page.tsx",
"chars": 2887,
"preview": "import ExpertBooker from \"../_components/expert-booker\";\nimport { cal } from \"@/cal/api\";\nimport Image from \"next/image\""
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/AboutSection.tsx",
"chars": 1379,
"preview": "'use client'\n\nimport { useState } from 'react'\nimport clsx from 'clsx'\nimport { Info } from 'lucide-react'\n\n\n\nexport fun"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/Container.tsx",
"chars": 366,
"preview": "import { cn } from \"@/lib/utils\";\n\nexport function Container({ className, children, ...props }: React.ComponentPropsWith"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/expert-booker.tsx",
"chars": 1990,
"preview": "\"use client\";\n\nimport { Booker, useEventTypesPublic } from \"@calcom/atoms\";\nimport type { CalAccount, User } from \"@pris"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/booking/[bookingUid]/page.tsx",
"chars": 288,
"preview": "import { BookingResult } from \"@/app/_components/booking-result\";\nimport { Suspense } from \"react\";\n\nexport default func"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/layout.tsx",
"chars": 1173,
"preview": "import { Logo } from \"../_components/universal/logo\";\nimport { SignedIn, SignedOut } from \"@/auth\";\nimport { Button } fr"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/page.tsx",
"chars": 5333,
"preview": "import { cal } from \"@/cal/api\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_actions.tsx",
"chars": 6345,
"preview": "\"use server\";\n\nimport { type LoginFormState } from \"./login/_components/login\";\nimport { LoginSchema, SignupSchema, auth"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/autocomplete.tsx",
"chars": 3803,
"preview": "\"use client\";\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/banner.tsx",
"chars": 5005,
"preview": "import { Button } from \"@/components/ui/button\";\n\nexport default function Banner({\n title,\n description,\n ctaLink,\n}:"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/booking-result.tsx",
"chars": 12139,
"preview": "\"use client\";\n\nimport { stripCalOAuthClientIdFromEmail, stripCalOAuthClientIdFromText } from \"@/cal/utils\";\nimport { Bad"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/home/results.tsx",
"chars": 11384,
"preview": "\"use client\";\n\nimport { SearchBar } from \"../search-bar\";\nimport SidebarItem from \"./sidebar-item\";\nimport { filterOptio"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/home/sidebar-item.tsx",
"chars": 1990,
"preview": "\"use client\";\n\nimport { type filterOptions } from \"@/app/_hardcoded\";\nimport { filterSearchParamSchema } from \"@/app/_se"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/home/signup-card.tsx",
"chars": 1157,
"preview": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } fr"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/multi-select.tsx",
"chars": 4599,
"preview": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Command, CommandGroup, CommandItem, CommandList }"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/navigation.tsx",
"chars": 4948,
"preview": "\"use client\";\n\nimport {\n NavigationMenu,\n NavigationMenuContent,\n NavigationMenuItem,\n NavigationMenuLink,\n Navigat"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/search-bar.tsx",
"chars": 628,
"preview": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { useQueryState, parseAsString } from \"nuqs\";\n\nexpo"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/submit-button.tsx",
"chars": 976,
"preview": "\"use client\";\n\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimpo"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/universal/hero.tsx",
"chars": 639,
"preview": "import { Balancer } from \"react-wrap-balancer\";\n\ninterface HeroProps {\n title: string;\n children?: React.ReactNode;\n}\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/universal/layout.tsx",
"chars": 1025,
"preview": "import { cn } from \"@/lib/utils\";\nimport React from \"react\";\n\ntype BaseProps = {\n children: React.ReactNode;\n classNam"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/universal/logo.tsx",
"chars": 330,
"preview": "import { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\nexport const Logo = ({ href, className }: { href?: stri"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_components/use-cal.tsx",
"chars": 1308,
"preview": "\"use client\";\n\nimport { env } from \"@/env\";\nimport { CalProvider } from \"@calcom/atoms\";\nimport { use } from \"react\";\n\n/"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_hardcoded.ts",
"chars": 10241,
"preview": "import type { FilterOption } from \"@prisma/client\";\n\n// TODO: move to database after signup\nexport const categoryOptions"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/_searchParams.ts",
"chars": 1944,
"preview": "import {\n capabilityOptions,\n categoryOptions,\n budgetOptions,\n frameworkOptions,\n languageOptions,\n regions,\n} fr"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/api/auth/[...nextauth]/route.ts",
"chars": 36,
"preview": "export { GET, POST } from \"@/auth\";\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/api/cal/refresh/route.ts",
"chars": 2936,
"preview": "import { authConfig } from \"@/auth/config.edge\";\nimport { type KeysResponseDto } from \"@/cal/__generated/cal-sdk\";\nimpor"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/api/supabase/storage/route.ts",
"chars": 931,
"preview": "import { auth } from \"@/auth\";\nimport { env } from \"@/env\";\nimport { createClient } from \"@supabase/supabase-js\";\n\nexpor"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/[...dashboardSegments]/page.tsx",
"chars": 1501,
"preview": "import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@breadcrumbs/page.tsx",
"chars": 473,
"preview": "import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from \"@/components/ui/breadcrumb\";\n\n// Note: The r"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/[...dashboardSegments]/page.tsx",
"chars": 995,
"preview": "import { dashboardNavigationData } from \"../../data\";\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\ne"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationDesktop/page.tsx",
"chars": 987,
"preview": "import { dashboardNavigationData } from \"../data\";\nimport { cn } from \"@/lib/utils\";\nimport Link from \"next/link\";\n\n// N"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/[...dashboardSegments]/page.tsx",
"chars": 1153,
"preview": "import { dashboardNavigationData } from \"../../data\";\nimport { Logo } from \"@/app/_components/universal/logo\";\nimport { "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/@dashboardNavigationMobile/page.tsx",
"chars": 1145,
"preview": "import { dashboardNavigationData } from \"../data\";\nimport { Logo } from \"@/app/_components/universal/logo\";\nimport { cn "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/bookings-table.tsx",
"chars": 16522,
"preview": "\"use client\";\n\nimport { type GetBookingsDataEntry } from \"@/cal/__generated/cal-sdk\";\nimport { stripCalOAuthClientIdFrom"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/connect-calendar-step.tsx",
"chars": 1457,
"preview": "import { ButtonSubmit } from \"@/app/_components/submit-button\";\nimport { Card, CardDescription, CardFooter, CardHeader, "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/getting-started-steps.tsx",
"chars": 1444,
"preview": "\"use client\";\n\nimport ConnectCalendarStep from \"./connect-calendar-step\";\nimport UserFilters from \"./user-filters-step\";"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-details-step.tsx",
"chars": 1772,
"preview": "import SupabaseReactDropzone from \"../settings/_components/supabase-react-dropzone\";\nimport { expertEdit } from \"@/app/_"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/_components/user-filters-step.tsx",
"chars": 3098,
"preview": "import { addUserFilters } from \"@/app/_actions\";\nimport { FancyMultiSelect, type Option } from \"@/app/_components/multi-"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/data.tsx",
"chars": 739,
"preview": "import { Calendar, Clock, Home, User } from \"lucide-react\";\n\nexport const dashboardNavigationData = [\n {\n label: \"Da"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/getting-started/page.tsx",
"chars": 418,
"preview": "import GettingStarted from \"../_components/getting-started-steps\";\nimport { auth } from \"@/auth\";\nimport { db } from \"pr"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/layout.tsx",
"chars": 3925,
"preview": "import { ButtonSubmit } from \"@/app/_components/submit-button\";\nimport { Logo } from \"@/app/_components/universal/logo\";"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/page.tsx",
"chars": 7537,
"preview": "/* eslint-disable */\n// @ts-nocheck\nimport { BookingsTable } from \"./_components/bookings-table\";\nimport { CalAccount, a"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/expert-edit.tsx",
"chars": 1691,
"preview": "\"use client\";\n\nimport { expertEdit } from \"@/app/_actions\";\nimport { ButtonSubmit } from \"@/app/_components/submit-butto"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/settings-content.tsx",
"chars": 1420,
"preview": "\"use client\";\n\nimport { AvailabilitySettings } from \"@calcom/atoms\";\n\n/**\n * [@calcom] Make sure to wrap your app with o"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/_components/supabase-react-dropzone.tsx",
"chars": 3375,
"preview": "\"use client\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { env } from \"@/env\";\nimport slugify, { cn } "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/availability/page.tsx",
"chars": 160,
"preview": "import SettingsContent from \"../_components/settings-content\";\n\nexport default async function DashboardSettingsAvailabil"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/_actions.ts",
"chars": 3216,
"preview": "\"use server\";\n\nimport { auth } from \"@/auth\";\nimport { post_EventTypesController_createEventType } from \"@/cal/__generat"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-create.tsx",
"chars": 1226,
"preview": "\"use client\";\n\nimport { createEventType } from \"./_actions\";\nimport { DialogDescription } from \"@/components/ui/dialog\";"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/event-type-delete.tsx",
"chars": 752,
"preview": "\"use client\";\n\nimport { deleteEventType } from \"./_actions\";\nimport { DropdownMenuItem } from \"@/components/ui/dropdown-"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/booking-events/page.tsx",
"chars": 9306,
"preview": "import EventTypeCreateForm from \"./event-type-create\";\nimport { EventTypeDelete } from \"./event-type-delete\";\nimport { B"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/layout.tsx",
"chars": 254,
"preview": "export default function SettingsLayout(props: { children: React.ReactNode }) {\n return (\n <main className=\"flex min-"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/page.tsx",
"chars": 60,
"preview": "export default function SettingsOutlet() {\n return null;\n}\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/dashboard/settings/profile/page.tsx",
"chars": 2025,
"preview": "import ExpertEditForm from \"../_components/expert-edit\";\nimport SupabaseReactDropzone from \"../_components/supabase-reac"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/layout.tsx",
"chars": 2755,
"preview": "import Banner from \"./_components/banner\";\nimport UseCalAtoms from \"./_components/use-cal\";\nimport { Providers } from \"."
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/login/_components/input.tsx",
"chars": 1022,
"preview": "import { Input, type InputProps } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { type ReactNod"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/login/_components/login.tsx",
"chars": 2756,
"preview": "\"use client\";\n\nimport { signInWithCredentials } from \"@/app/_actions\";\nimport { ButtonSubmit } from \"@/app/_components/s"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/login/layout.tsx",
"chars": 2470,
"preview": "import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/login/page.tsx",
"chars": 124,
"preview": "import { LoginForm } from \"@/app/login/_components/login\";\n\nexport default function LoginPage() {\n return <LoginForm />"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/page.tsx",
"chars": 2411,
"preview": "import { Results } from \"./_components/home/results\";\nimport SignupCard from \"./_components/home/signup-card\";\nimport { "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/providers.tsx",
"chars": 444,
"preview": "\"use client\";\n\nimport { TooltipProvider } from \"@radix-ui/react-tooltip\";\nimport { ThemeProvider as NextThemesProvider }"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/signup/_components/input.tsx",
"chars": 1022,
"preview": "import { Input, type InputProps } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { type ReactNod"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/signup/_components/signup.tsx",
"chars": 3846,
"preview": "\"use client\";\n\nimport { signUpWithCredentials } from \"@/app/_actions\";\nimport { AddonFieldInput, AddonFieldPrefix } from"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/signup/layout.tsx",
"chars": 2433,
"preview": "import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/signup/page.tsx",
"chars": 219,
"preview": "import { SignupForm } from \"@/app/signup/_components/signup\";\n\nexport default async function SignupPage() {\n return (\n "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/app/tailwind-indicator.tsx",
"chars": 694,
"preview": "import { IS_PRODUCTION } from \"@/lib/constants\";\n\nexport function TailwindIndicator() {\n if (IS_PRODUCTION) return null"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/auth/config.edge.ts",
"chars": 4008,
"preview": "import { signUp } from \"@/cal/auth\";\nimport { PrismaAdapter } from \"@auth/prisma-adapter\";\nimport { type User } from \"@p"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/auth/index.tsx",
"chars": 7375,
"preview": "import { authConfig } from \"./config.edge\";\nimport { signUp } from \"@/cal/auth\";\nimport { env } from \"@/env\";\nimport { t"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/cal/__generated/cal-sdk.ts",
"chars": 49057,
"preview": "// @ts-nocheck generated file\nimport z from \"zod\";\n\nexport type ManagedUserOutput = z.infer<typeof ManagedUserOutput>;\ne"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/cal/__generated/cal-sdk.yml",
"chars": 65485,
"preview": "openapi: 3.0.0\npaths:\n /v2/oauth-clients/{clientId}/users:\n get:\n operationId: OAuthClientUsersController_getMa"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/cal/api.ts",
"chars": 20643,
"preview": "import {\n KeysResponseDto,\n createApiClient,\n RefreshTokenInput,\n ManagedUserOutput,\n type CreateManagedUserInput,\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/cal/auth.ts",
"chars": 1933,
"preview": "import { type CreateManagedUserInput } from \"./__generated/cal-sdk\";\nimport { cal } from \"./api\";\nimport { env } from \"@"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/cal/utils.ts",
"chars": 544,
"preview": "import { env } from \"@/env\";\n\nexport const stripCalOAuthClientIdFromText = (str: string) => {\n if (str === \"\") return s"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/accordion.tsx",
"chars": 1975,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/avatar.tsx",
"chars": 1419,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } fr"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/badge.tsx",
"chars": 1187,
"preview": "import { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React f"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/breadcrumb.tsx",
"chars": 2679,
"preview": "import { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } f"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/button.tsx",
"chars": 1771,
"preview": "import { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"c"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/card.tsx",
"chars": 1831,
"preview": "import { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nconst Card = React.forwardRef<HTMLDivElement, React.H"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/checkbox.tsx",
"chars": 1064,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport {"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/collapsible.tsx",
"chars": 329,
"preview": "\"use client\"\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nconst Collapsible = CollapsiblePrimit"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/command.tsx",
"chars": 4942,
"preview": "\"use client\";\n\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/dialog.tsx",
"chars": 3792,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X }"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/dropdown-menu.tsx",
"chars": 7272,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/form.tsx",
"chars": 4116,
"preview": "import { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport type * as LabelPrimitive from \"@"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/input.tsx",
"chars": 786,
"preview": "import { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nexport type InputProps = React.InputHTMLAttributes<HT"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/label.tsx",
"chars": 714,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/navigation-menu.tsx",
"chars": 5017,
"preview": "import { cn } from \"@/lib/utils\";\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { c"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/pagination.tsx",
"chars": 2715,
"preview": "import { type ButtonProps, buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Ch"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/popover.tsx",
"chars": 1252,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport * a"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/progress.tsx",
"chars": 776,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\nimport *"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/select.tsx",
"chars": 5575,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Che"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/separator.tsx",
"chars": 736,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\nimport"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/sheet.tsx",
"chars": 4246,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\";\nimport { cva,"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/skeleton.tsx",
"chars": 234,
"preview": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/stepper.tsx",
"chars": 25337,
"preview": "\"use client\";\n\n/* eslint-disable @typescript-eslint/no-empty-function */\nimport { Button } from \"./button\";\nimport { Col"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/table.tsx",
"chars": 2663,
"preview": "import { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nconst Table = React.forwardRef<HTMLTableElement, Reac"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/tabs.tsx",
"chars": 1908,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as Reac"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/textarea.tsx",
"chars": 736,
"preview": "import { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nexport type TextareaProps = React.TextareaHTMLAttribu"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/toast.tsx",
"chars": 4805,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva,"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/toaster.tsx",
"chars": 766,
"preview": "\"use client\";\n\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport,\n} fro"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/tooltip.tsx",
"chars": 1168,
"preview": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport * a"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/components/ui/use-toast.ts",
"chars": 3948,
"preview": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n ToastActionElement,"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/env.js",
"chars": 3747,
"preview": "import { createEnv } from \"@t3-oss/env-nextjs\";\nimport { vercel } from \"@t3-oss/env-nextjs/presets\";\nimport { z } from \""
},
{
"path": "with-platform-supabase-tailwind-prisma/src/lib/constants.ts",
"chars": 1315,
"preview": "import { type Option } from \"@/app/_components/autocomplete\";\n\nexport const professions = [\n { label: \"Hair dresser\", v"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/lib/supabase-image-loader.ts",
"chars": 278,
"preview": "import { env } from \"@/env\";\n\nexport default function supabaseLoader({\n src,\n width,\n quality,\n}: {\n src: string;\n "
},
{
"path": "with-platform-supabase-tailwind-prisma/src/lib/utils.ts",
"chars": 2013,
"preview": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/middleware.ts",
"chars": 546,
"preview": "import { authConfig } from \"./auth/config.edge\";\nimport NextAuth from \"next-auth\";\nimport { NextResponse, type Middlewar"
},
{
"path": "with-platform-supabase-tailwind-prisma/src/styles/globals.css",
"chars": 2516,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --f"
},
{
"path": "with-platform-supabase-tailwind-prisma/supabase/.gitignore",
"chars": 32,
"preview": "# Supabase\n.branches\n.temp\n.env\n"
},
{
"path": "with-platform-supabase-tailwind-prisma/supabase/config.toml",
"chars": 6825,
"preview": "# A string used to distinguish different Supabase projects on the same host. Defaults to the\n# working directory name wh"
},
{
"path": "with-platform-supabase-tailwind-prisma/supabase/migrations/20240615093934_init.sql",
"chars": 6248,
"preview": "-- CreateSchema\nCREATE SCHEMA IF NOT EXISTS \"prisma\";\n\n-- CreateEnum\nCREATE TYPE \"prisma\".\"UserStatus\" AS ENUM ('APPROVE"
},
{
"path": "with-platform-supabase-tailwind-prisma/supabase/seed.sql",
"chars": 5651,
"preview": "--\n-- Data for Name: FilterOption; Type: TABLE DATA; Schema: public; Owner: postgres\n--\n\nINSERT INTO \"prisma\".\"FilterOpt"
},
{
"path": "with-platform-supabase-tailwind-prisma/tailwind.config.ts",
"chars": 2833,
"preview": "import type { Config } from \"tailwindcss\";\n\nconst config = {\n darkMode: [\"class\"],\n content: [\n \"./pages/**/*.{ts,t"
},
{
"path": "with-platform-supabase-tailwind-prisma/tsconfig.json",
"chars": 857,
"preview": "{\n \"compilerOptions\": {\n /* Base Options: */\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"target\": \"e"
}
]
About this extraction
This page contains the full source code of the calcom/platform-starter-kit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 128 files (466.5 KB), approximately 119.4k tokens, and a symbol index with 324 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.