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 ================================================

Logo

Cal.com Platform Starter Kit

Build your pixel-perfect booking experience

Demo · Video Tutorial · Docs · Deploy on Vercel

Discord · Website · Issues

# Platform Starter Kit Example Cal.com Platform Starter Kit showcases the new Cal.com Platform API and Cal.com Atoms. It was built using the [T3 Stack](https://create.t3.gg/) with [Supabase](https://supabase.com/) as the Postgres Database and Image Storage host. ## Deploy your own [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain&env=NEXT_PUBLIC_REFRESH_URL,AUTH_SECRET,AUTH_TRUST_HOST,NEXT_PUBLIC_CAL_OAUTH_CLIENT_ID,NEXT_PUBLIC_CAL_API_URL,CAL_SECRET&envDescription=You%20can%20see%20how%20to%20populate%20the%20environment%20variables%20in%20our%20starter%20example%20→&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain%2F.env.example&project-name=cal-platform-starter&repository-name=cal-platform-starter&demo-title=Cal.com%20Experts&demo-description=A%20marketplace%20to%20book%20appointments%20with%20experts&demo-url=https%3A%2F%2Fexperts.cal.com&demo-image=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Fassets%2F8019099%2F2e58f8da-a110-4a45-b9a4-dcffb45f9baa&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6&external-id=https%3A%2F%2Fgithub.com%2Fcalcom%2Fplatform-starter-kit%2Ftree%2Fmain) ## How to use ```bash npx @calcom/starter-kit my-platform ``` OR **1. Clone the repository** HTTPS: ```bash git clone https://github.com/calcom/platform-starter-kit.git ``` GitHub CLI: ```bash gh repo clone calcom/platform-starter-kit ``` **2. Move into the Starter** ```bash cd platform-starter-kit/ ``` **3. Install dependencies** > [!IMPORTANT] > **Package Manager:** This repository is deployed as-is and therefore contains a `pnpm-lock.yaml` file. As a result, you currently have to use `pnpm` as your package manager to ensure that the dependencies are installed correctly. ```bash pnpm install ``` **4. Set Environment Variables** We provide most environment variables out of the box (including Cal-related variables). So get started by copying the `.env.example`: ```bash cp .env.example .env ``` _4.1 Database_ This project uses Postgres with Supabase. You can create a free project at [database.new](https://database.new/). Then, get the Database URL from the [Supabase dashboard](https://supabase.com/dashboard/project/_/settings/database) and update the respective values in your `.env` file: ```.env POSTGRES_PRISMA_URL="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1" # Transaction Mode POSTGRES_URL_NON_POOLING="postgres://postgres.YOUR-PROJECT-REF:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres" # Session Mode ``` When working locally you can use the DB URL: `postgresql://postgres:postgres@127.0.0.1:54322/postgres` outputted by the `supabase start` command for both vairables. [Only needed when deploying manually] Initialize the database: Note that if you used the Vercel Deploy link from above, the Supabase Vercel integration sets this up automatically for you! ```bash pnpm db:init pnpm db:seed # Will throw an error if DB is already seeded, which you can ignore. ``` Prisma will create a `_prisma_migrations` table on the `public` database schema. In Supabase, the public schema is exposed via the API by default. To secure the table, navigate to the [Table Editor](https://supabase.com/dashboard/project/_/editor), click on "RLS diasbaled" > "Enable RLS for this table". Alternatively, you can run the follow SQL statement on your database, e.g. via the [SQL Editor](https://supabase.com/dashboard/project/_/sql/new) in the Supabase Dashboard: ```sql ALTER TABLE "public"."_prisma_migrations" ENABLE ROW LEVEL SECURITY; ``` Lastly, in your [Supabase Dashboard](https://supabase.com/dashboard/project/_/storage/buckets) create a public `avatars` bucket to store the profile pictures. _4.2 Authentication_ Generate a NextAuth secret and add it to your `.env` file: ```bash openssl rand -hex 32 ``` ```.env # Next Auth # You can generate a new secret on the command line with # openssl rand -base64 32 # AUTH_SECRET="SQhGk****" ``` _4.3 Cal_ For **development**, you're all set! We've provided you with our sandbox keys that you can find the `.env.example` file. For **production**, keep in mind that you'll have to update the `NEXT_PUBLIC_REFRESH_URL` variable to make it point to your deployment, e.g.: ```.env # 3/ *REFRESH URL.* You have to expose an endpoint that will be used from calcom: https://cal.com/docs/platform/quick-start#4.-backend:-setting-up-refresh-token-endpoint NEXT_PUBLIC_REFRESH_URL="https://.vercel.app/api/cal/refresh" ``` **5. Development Server** From here, you're all set. Just start the development server & get going. ```bash pnpm dev ``` ## What's next? How do I make an app with this? We try to keep this project as simple as possible, so you can start with Cal.com Platform and the scaffolding we set up for you, and add additional things later when they become necessary. If you are not familiar with the different technologies used in this project, please refer to the respective docs. - [Cal.com Platform](https://cal.com/platform) - [Next.js](https://nextjs.org) - [Supabase](https://supabase.com) - [NextAuth.js](https://next-auth.js.org) - [Prisma](https://prisma.io) - [Tailwind CSS](https://tailwindcss.com) - [tRPC](https://trpc.io) ## Learn More about Cal.com Platform Visit our documentation at [cal.com/docs/platform](https://cal.com/docs/platform) or join our [Discord](https://go.cal.com/discord). Contact sales to purchase a commercial API key here: [cal.com/sales](https://cal.com/sales). ## Learn More about T3 To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: - [Documentation](https://create.t3.gg/) - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! ## Learn More about Supabase Supabase is the fastest way to get up and running with Next.js and Postgres. Check out [this video](https://youtu.be/WdA6b0jPNv4?si=eeWpu03PI3W-t5pC) to learn more! ================================================ FILE: with-platform-supabase-tailwind-prisma/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/styles/globals.css", "baseColor": "stone", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ================================================ FILE: with-platform-supabase-tailwind-prisma/next.config.js ================================================ import { resolve } from "path"; /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * for Docker builds. */ await import("./src/env.js"); /** @type {import("next").NextConfig} */ const config = { experimental: { ppr: true, }, images: { formats: ["image/avif", "image/webp"], remotePatterns: [ { protocol: "https", hostname: "picsum.photos", }, ], loader: "custom", loaderFile: "./src/lib/supabase-image-loader.ts", }, }; export default config; ================================================ FILE: with-platform-supabase-tailwind-prisma/package.json ================================================ { "private": true, "type": "module", "scripts": { "build": "next build", "db:studio": "prisma studio", "db:seed": "NODE_ENV=development prisma db seed", "db:init": "pnpm prisma migrate dev --name=init", "dev": "next dev", "postinstall": "prisma generate", "lint": "NODE_OPTIONS='--max-old-space-size=4096' next lint --fix", "start": "next start", "cal:generate": "openapi-endpoint-trimmer -u https://raw.githubusercontent.com/calcom/cal.com/51428087ef0a20f4d775fccbd3a34c2d885aed2b/apps/api/v2/swagger/documentation.json -p /v2/bookings,/v2/event-types,/v2/schedules,/v2/oauth-clients,/v2/oauth -o ./src/cal/__generated/cal-sdk.yml && typed-openapi ./src/cal/__generated/cal-sdk.yml --r zod -o ./src/cal/__generated/cal-sdk.ts", "typecheck": "tsc --noEmit" }, "prisma": { "seed": "tsx prisma/seed.ts" }, "dependencies": { "@auth/prisma-adapter": "^1.4.0", "@calcom/atoms": "^1.0.44", "@hookform/resolvers": "^3.3.4", "@libsql/client": "^0.6.0", "@opentelemetry/api": "^1.8.0", "@prisma/adapter-libsql": "^5.12.1", "@prisma/client": "^5.15.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "2.0.2", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@supabase/supabase-js": "^2.43.5", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.25.0", "@trpc/client": "next", "@trpc/next": "next", "@trpc/react-query": "next", "@trpc/server": "next", "@vercel/analytics": "^1.2.2", "@zodios/core": "^10.9.6", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", "dayjs": "^1.11.10", "lucide-react": "^0.364.0", "next": "14.3.0-canary.37", "next-auth": "5.0.0-beta.16", "next-axiom": "^1.1.1", "next-themes": "^0.3.0", "nuqs": "^1.17.4", "pino": "^8.20.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.51.4", "react-wrap-balancer": "^1.1.0", "remeda": "^2.0.3", "server-only": "^0.0.1", "sonner": "^1.4.41", "superjson": "^2.2.1", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "typed-openapi": "^0.4.1", "usehooks-ts": "^3.1.0", "zod": "^3.23.7" }, "devDependencies": { "@next/eslint-plugin-next": "^14.2.3", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/eslint": "^8.56.2", "@types/node": "^20.11.20", "@types/react": "^18.2.75", "@types/react-dom": "^18.2.24", "@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/parser": "^7.1.1", "eslint": "^8.57.0", "eslint-config-next": "^14.1.3", "eslint-config-turbo": "^1.13.3", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unused-imports": "^3.1.0", "openapi-endpoint-trimmer": "^2.0.0", "postcss": "^8.4.34", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11", "prisma": "^5.15.0", "tailwindcss": "^3.4.1", "tsx": "^4.7.2", "typescript": "^5.4.5", "typescript-eslint": "^7.8.0" }, "ct3aMetadata": { "initVersion": "7.30.0" } } ================================================ FILE: with-platform-supabase-tailwind-prisma/postcss.config.cjs ================================================ const config = { plugins: { tailwindcss: {}, }, }; module.exports = config; ================================================ FILE: with-platform-supabase-tailwind-prisma/prettier.config.mjs ================================================ /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ const config = { bracketSpacing: true, bracketSameLine: true, singleQuote: false, jsxSingleQuote: false, trailingComma: "es5", semi: true, printWidth: 110, arrowParens: "always", endOfLine: "auto", plugins: ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], }; export default config; ================================================ FILE: with-platform-supabase-tailwind-prisma/prisma/client.ts ================================================ import { env } from "@/env"; import { PrismaClient } from "@prisma/client"; const createPrismaClient = () => new PrismaClient({ log: env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], }); const globalForPrisma = globalThis as unknown as { prisma: ReturnType | undefined; }; export const db = globalForPrisma.prisma ?? createPrismaClient(); if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; ================================================ FILE: with-platform-supabase-tailwind-prisma/prisma/migrations/0_init/migration.sql ================================================ -- CreateSchema CREATE SCHEMA IF NOT EXISTS "prisma"; -- CreateEnum CREATE TYPE "prisma"."UserStatus" AS ENUM ('APPROVED', 'PENDING'); -- CreateTable CREATE TABLE "prisma"."Account" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "type" TEXT NOT NULL, "provider" TEXT NOT NULL, "providerAccountId" TEXT NOT NULL, "refresh_token" TEXT, "access_token" TEXT, "expires_at" INTEGER, "token_type" TEXT, "scope" TEXT, "id_token" TEXT, "session_state" TEXT, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Account_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "prisma"."Session" ( "id" TEXT NOT NULL, "sessionToken" TEXT NOT NULL, "userId" TEXT NOT NULL, "expires" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "Session_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "prisma"."User" ( "id" TEXT NOT NULL, "name" TEXT, "username" TEXT, "bio" TEXT, "email" TEXT, "emailVerified" TIMESTAMP(3), "hashedPassword" TEXT, "image" TEXT, "calAccountId" INTEGER, "calAccessToken" TEXT, "calRefreshToken" TEXT, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, "status" "prisma"."UserStatus" NOT NULL DEFAULT 'PENDING', CONSTRAINT "User_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "prisma"."CalAccount" ( "id" INTEGER NOT NULL, "username" TEXT, "email" TEXT NOT NULL, "timeZone" TEXT NOT NULL, "weekStart" TEXT NOT NULL, "createdDate" TEXT NOT NULL, "timeFormat" INTEGER, "defaultScheduleId" INTEGER, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "CalAccount_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "prisma"."VerificationToken" ( "identifier" TEXT NOT NULL, "token" TEXT NOT NULL, "expires" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP ); -- CreateTable CREATE TABLE "prisma"."FilterOption" ( "fieldId" TEXT NOT NULL, "fieldValue" TEXT NOT NULL, "fieldLabel" TEXT NOT NULL, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, "filterCategoryFieldId" TEXT NOT NULL, "filterCategoryValue" TEXT NOT NULL, "filterCategoryLabel" TEXT NOT NULL ); -- CreateTable CREATE TABLE "prisma"."FilterOptionsOnUser" ( "userId" TEXT NOT NULL, "filterOptionFieldId" TEXT NOT NULL, "filterCategoryFieldId" TEXT NOT NULL, "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP ); -- CreateIndex CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "prisma"."Account"("provider", "providerAccountId"); -- CreateIndex CREATE UNIQUE INDEX "Session_sessionToken_key" ON "prisma"."Session"("sessionToken"); -- CreateIndex CREATE UNIQUE INDEX "User_username_key" ON "prisma"."User"("username"); -- CreateIndex CREATE UNIQUE INDEX "User_email_key" ON "prisma"."User"("email"); -- CreateIndex CREATE UNIQUE INDEX "User_calAccountId_key" ON "prisma"."User"("calAccountId"); -- CreateIndex CREATE UNIQUE INDEX "User_calAccessToken_key" ON "prisma"."User"("calAccessToken"); -- CreateIndex CREATE UNIQUE INDEX "User_calRefreshToken_key" ON "prisma"."User"("calRefreshToken"); -- CreateIndex CREATE UNIQUE INDEX "CalAccount_username_key" ON "prisma"."CalAccount"("username"); -- CreateIndex CREATE UNIQUE INDEX "CalAccount_email_key" ON "prisma"."CalAccount"("email"); -- CreateIndex CREATE UNIQUE INDEX "VerificationToken_token_key" ON "prisma"."VerificationToken"("token"); -- CreateIndex CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "prisma"."VerificationToken"("identifier", "token"); -- CreateIndex CREATE UNIQUE INDEX "FilterOption_fieldId_key" ON "prisma"."FilterOption"("fieldId"); -- CreateIndex CREATE INDEX "FilterOption_fieldId_filterCategoryFieldId_idx" ON "prisma"."FilterOption"("fieldId", "filterCategoryFieldId"); -- CreateIndex CREATE UNIQUE INDEX "FilterOption_fieldId_filterCategoryFieldId_key" ON "prisma"."FilterOption"("fieldId", "filterCategoryFieldId"); -- CreateIndex CREATE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCatego_idx" ON "prisma"."FilterOptionsOnUser"("userId", "filterOptionFieldId", "filterCategoryFieldId"); -- CreateIndex CREATE UNIQUE INDEX "FilterOptionsOnUser_userId_filterOptionFieldId_filterCatego_key" ON "prisma"."FilterOptionsOnUser"("userId", "filterOptionFieldId", "filterCategoryFieldId"); -- AddForeignKey ALTER TABLE "prisma"."Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "prisma"."Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "prisma"."User" ADD CONSTRAINT "User_calAccountId_fkey" FOREIGN KEY ("calAccountId") REFERENCES "prisma"."CalAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "prisma"."FilterOptionsOnUser" ADD CONSTRAINT "FilterOptionsOnUser_filterOptionFieldId_filterCategoryFiel_fkey" FOREIGN KEY ("filterOptionFieldId", "filterCategoryFieldId") REFERENCES "prisma"."FilterOption"("fieldId", "filterCategoryFieldId") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "prisma"."FilterOptionsOnUser" ADD CONSTRAINT "FilterOptionsOnUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "prisma"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; ================================================ FILE: with-platform-supabase-tailwind-prisma/prisma/schema.prisma ================================================ generator client { provider = "prisma-client-js" previewFeatures = ["multiSchema"] } datasource db { provider = "postgresql" url = env("POSTGRES_PRISMA_URL") directUrl = env("POSTGRES_URL_NON_POOLING") schemas = ["prisma"] } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? access_token String? expires_at Int? token_type String? scope String? id_token String? session_state String? createdAt DateTime? @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@schema("prisma") } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime createdAt DateTime? @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@schema("prisma") } model User { id String @id @default(cuid()) name String? username String? @unique bio String? email String? @unique emailVerified DateTime? hashedPassword String? image String? calAccountId Int? @unique calAccessToken String? @unique calRefreshToken String? @unique createdAt DateTime? @default(now()) status UserStatus @default(PENDING) accounts Account[] selectedFilterOptions FilterOptionsOnUser[] sessions Session[] calAccount CalAccount? @relation(fields: [calAccountId], references: [id], onDelete: Cascade) @@schema("prisma") } model CalAccount { id Int @id username String? @unique email String @unique timeZone String weekStart String createdDate String timeFormat Int? defaultScheduleId Int? createdAt DateTime? @default(now()) user User? @@schema("prisma") } model VerificationToken { identifier String token String @unique expires DateTime createdAt DateTime? @default(now()) @@unique([identifier, token]) @@schema("prisma") } model FilterOption { fieldId String @id @unique fieldValue String fieldLabel String createdAt DateTime? @default(now()) filterCategoryFieldId String filterCategoryValue String filterCategoryLabel String selectedByUsers FilterOptionsOnUser[] @@unique([fieldId, filterCategoryFieldId]) @@index([fieldId, filterCategoryFieldId]) @@schema("prisma") } model FilterOptionsOnUser { userId String filterOptionFieldId String filterCategoryFieldId String createdAt DateTime? @default(now()) filterOption FilterOption @relation(fields: [filterOptionFieldId, filterCategoryFieldId], references: [fieldId, filterCategoryFieldId]) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([userId, filterOptionFieldId, filterCategoryFieldId]) @@index([userId, filterOptionFieldId, filterCategoryFieldId]) @@schema("prisma") } enum UserStatus { APPROVED PENDING @@schema("prisma") } ================================================ FILE: with-platform-supabase-tailwind-prisma/prisma/seed.ts ================================================ import { filterOptions } from "@/app/_hardcoded"; import { PrismaClient } from "@prisma/client"; const devDb = new PrismaClient(); async function main() { for (const filterOption of filterOptions) { console.log(`attempting to upsert ${filterOption.fieldId}`); await devDb.filterOption.upsert({ where: { fieldId: filterOption.fieldId }, create: filterOption, update: filterOption, }); console.log(`✅ {filterOption.fieldId} upserted`); } } main() .then(async () => { await devDb.$disconnect(); }) .catch(async (e) => { console.error(e); await devDb.$disconnect(); process.exit(1); }); ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/[eventSlug]/page.tsx ================================================ import ExpertBooker from "../_components/expert-booker"; import { cal } from "@/cal/api"; import Image from "next/image"; import { db } from "prisma/client"; export const dynamic = "force-dynamic"; export default async function BookerPage({ params, }: { params: { expertUsername: string; eventSlug: string }; }) { const expert = await db.user.findUnique({ where: { username: params.expertUsername }, select: { id: true, calAccessToken: true, calRefreshToken: true, calAccountId: true, name: true, username: true, calAccount: { select: { id: true, username: true, }, }, }, }); if (!expert?.calAccount?.username) { console.warn("Expert not found", params.expertUsername); return
Expert not found
; } const eventType = await cal({ user: { calAccessToken: expert.calAccessToken, calRefreshToken: expert.calRefreshToken, calAccountId: expert.calAccountId, id: expert.id, }, }).get("/v2/event-types/{username}/{eventSlug}/public", { path: { username: expert.calAccount.username, eventSlug: params.eventSlug, }, query: { isTeamEvent: false, }, }); if (eventType.status === "error") { console.warn( `[BookerPage] Event not found for event slug '${params.eventSlug}'. Check logs above for more info.` ); return
Event not found
; } const descriptionWithoutHtmlTags = eventType.data?.description.replace(/<[^>]*>?/gm, ""); return (
Expert image

{expert.name}: {eventType.data?.title}

{descriptionWithoutHtmlTags}

{Boolean(expert.calAccount) && ( )}
); } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/AboutSection.tsx ================================================ 'use client' import { useState } from 'react' import clsx from 'clsx' import { Info } from 'lucide-react' export function AboutSection(props: React.ComponentPropsWithoutRef<'section'>) { const [isExpanded, setIsExpanded] = useState(false) return (

About

In this show, Eric and Wes dig deep to get to the facts with guests who have been labeled villains by a society quick to judge, without actually getting the full story. Tune in every Thursday to get to the truth with another misunderstood outcast as they share the missing context in their tragic tale.

{!isExpanded && ( )}
) } ================================================ 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 (
{children}
); } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/_components/expert-booker.tsx ================================================ "use client"; import { Booker, useEventTypesPublic } from "@calcom/atoms"; import type { CalAccount, User } from "@prisma/client"; import { Loader } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { toast } from "sonner"; /** * [@calcom] Make sure to wrap your app with our `CalProvider` to enable the use of our hooks. * @link https://cal.com/docs/platform/quick-start#5.3-setup-root-of-your-app */ type BookerProps = Parameters[number]; export const ExpertBooker = ( props: { className?: string; calAccount: Pick; expert: Pick; } & Partial ) => { const { className, calAccount, expert, ...rest } = props; const router = useRouter(); const searchParams = useSearchParams(); const rescheduleUid = searchParams.get("rescheduleUid") ?? undefined; const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypesPublic(calAccount.username ?? ""); if (!calAccount.username) { return
Sorry. We couldn't find this experts' user.
; } if (isLoadingEvents) { return (
); } if (!eventTypes?.length) { return (
Sorry. Unable to load ${expert.name}'s availabilities.
); } return ( { toast.success("Booking successful! "); router.push( `/${expert.username}/booking/${booking.data.uid}${booking.data.fromReschedule ? `?${new URLSearchParams({ fromReschedule: booking.data.fromReschedule }).toString()}` : ""}` ); }} rescheduleUid={rescheduleUid} {...rest} /> ); }; export default ExpertBooker; ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/booking/[bookingUid]/page.tsx ================================================ import { BookingResult } from "@/app/_components/booking-result"; import { Suspense } from "react"; export default function Booking() { return (
); } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/layout.tsx ================================================ import { Logo } from "../_components/universal/logo"; import { SignedIn, SignedOut } from "@/auth"; import { Button } from "@/components/ui/button"; import { LogIn } from "lucide-react"; import Link from "next/link"; import { type ReactNode } from "react"; export default function ExpertLayout({ children }: { children?: ReactNode }) { return (
{/* Tip: Use this for your own navigation */}
{(_user) => ( )}
{children}
); } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/[expertUsername]/page.tsx ================================================ import { cal } from "@/cal/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ArrowRight } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { db } from "prisma/client"; export const dynamic = "force-dynamic"; export default async function ExpertDetails({ params }: { params: { expertUsername: string } }) { const expert = await db.user.findUnique({ where: { username: params.expertUsername }, select: { id: true, calAccessToken: true, calRefreshToken: true, calAccountId: true, name: true, username: true, bio: true, calAccount: { select: { id: true, username: true, }, }, }, }); if (!expert?.calAccount?.username) { console.warn("Expert not found", params.expertUsername); return
Expert not found
; } const eventTypes = await cal({ user: { calAccessToken: expert.calAccessToken, calRefreshToken: expert.calRefreshToken, calAccountId: expert.calAccountId, id: expert.id, }, }).get("/v2/event-types/{username}/public", { path: { username: expert.calAccount.username, }, }); if (eventTypes.status === "error") { console.warn( `[ExpertDetails] Event not found for expert username '${params.expertUsername}'. Check logs above for more info.` ); } return (
Expert image

{expert.name}

About Us

{expert.bio}

{eventTypes.status === "error" ? (
User Events not found
) : ( Book Us Book us for any of the below events. Name Description Duration (min) Availability {eventTypes.data.map((eventType) => (
{eventType.title}
/{eventType.slug}
{eventType.description}
{eventType.length}
))}
Showing{" "} {eventTypes.data.length > 0 ? 1 : 0}- {eventTypes.data.length > 10 ? 10 : eventTypes.data.length} {" "} of {eventTypes.data.length} event types
)}
); } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/_actions.tsx ================================================ "use server"; import { type LoginFormState } from "./login/_components/login"; import { LoginSchema, SignupSchema, auth, signIn, unstable_update, FiltersSchema } from "@/auth"; import { type User } from "@prisma/client"; import { type Prisma } from "@prisma/client"; import { AuthError } from "next-auth"; import { revalidatePath } from "next/cache"; import { isRedirectError } from "next/dist/client/components/redirect"; import { db } from "prisma/client"; import { z } from "zod"; export async function signInWithCredentials(_prevState: LoginFormState, formData: FormData) { try { const credentials = LoginSchema.safeParse({ email: formData.get("email"), password: formData.get("password"), }); if (!credentials.success) { return { inputErrors: credentials.error.flatten().fieldErrors, }; } await signIn("credentials", formData); return { error: null }; } catch (error) { if (isRedirectError(error)) throw error; if (error instanceof AuthError) { switch (error.type) { case "CredentialsSignin": return { error: "Invalid credentials." }; default: console.error("Uncaught error signing in (AuthError): ", error); return { error: "Something went wrong." }; } } console.error("Uncaught error signing in", error); throw error; } } export async function addUserFilters(_prevState: { error?: string | null }, formData: FormData) { try { const sesh = await auth(); if (!sesh?.user?.id) return { error: "User not logged in " }; const filters = FiltersSchema.safeParse({ categories: formData.get("categories"), capabilities: formData.get("capabilities"), frameworks: formData.get("frameworks"), budgets: formData.get("budgets"), languages: formData.get("languages"), regions: formData.get("regions"), }); if (!filters.success) { return { inputErrors: filters.error.flatten().fieldErrors, }; } const selectedFilterOptions = [ { filterOpdtionFieldIds: filters.data.budgets, filterCategoryFieldId: "budgets" }, { filterOpdtionFieldIds: filters.data.capabilities, filterCategoryFieldId: "capabilities" }, { filterOpdtionFieldIds: filters.data.categories, filterCategoryFieldId: "categories" }, { filterOpdtionFieldIds: filters.data.frameworks, filterCategoryFieldId: "frameworks" }, { filterOpdtionFieldIds: filters.data.languages, filterCategoryFieldId: "languages" }, ] .map(({ filterOpdtionFieldIds, filterCategoryFieldId }) => { return filterOpdtionFieldIds.map((fieldId) => { return { filterCategoryFieldId, filterOptionFieldId: fieldId, userId: sesh?.user.id, }; }); }) // to filter out any null values: .filter(Boolean) as Prisma.FilterOptionsOnUserCreateManyInput[][]; const data = selectedFilterOptions.flat(); const createOrUpdateFilterPromises: Array> = []; for (const filter of data) { createOrUpdateFilterPromises.push( db.filterOptionsOnUser.upsert({ where: { userId_filterOptionFieldId_filterCategoryFieldId: { userId: filter.userId, filterOptionFieldId: filter.filterOptionFieldId, filterCategoryFieldId: filter.filterCategoryFieldId, }, }, update: filter, create: filter, }) ); } await Promise.all(createOrUpdateFilterPromises); return { success: true }; } catch (err) { throw err; } } export async function signUpWithCredentials(_prevState: { error?: string | null }, formData: FormData) { try { const credentials = SignupSchema.safeParse({ name: formData.get("name"), username: formData.get("username"), email: formData.get("email"), password: formData.get("password"), }); if (!credentials.success) { return { inputErrors: credentials.error.flatten().fieldErrors, }; } await signIn("credentials", formData); return { error: null }; } catch (error) { if (isRedirectError(error)) throw error; if (error instanceof AuthError) { switch (error.type) { case "CredentialsSignin": return { error: "Invalid credentials." }; default: console.error("Uncaught error signing in (AuthError): ", error); return { error: "Something went wrong." }; } } console.error("Uncaught error signing in", error); throw error; } } export async function expertEdit( _prevState: { error: null | string } | { success: null | string }, formData: FormData ) { console.log("[_actions] Updating expert with form data: ", formData); const sesh = await auth(); if (!sesh?.user.id) { console.log("[_actions] Unauthorized user edit", formData); return { error: "Unauthorized" }; } const formDataWithoutActionFields = Object.fromEntries( Array.from(formData.entries()).filter(([key]) => !key.toLowerCase().startsWith("$action")) ); const userEdit = z .object({ name: z.string().min(1).max(255), }) .or(z.object({ bio: z.string().min(1).max(255) })) .safeParse(formDataWithoutActionFields); if (!userEdit.success) { console.log("[_actions] Inavlid form data", formData); return { error: "Invalid form data" }; } const key = Object.keys(userEdit.data)[0]; if (!key) { console.error("[_actions] Invalid form data", formData); return { error: "Invalid form data" }; } let user: User | null; try { user = await db.user.update({ where: { id: sesh.user.id }, data: { // @ts-expect-error - key as "name" | "bio" didn't work -- not sure why // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [key]: userEdit.data[key], }, }); } catch (error) { console.error("Uncaught error updating expert", error); return { error: "Internal Server Error" }; } revalidatePath("/dashboard/settings/profile"); await unstable_update({ user: { name: user.name } }); // @ts-expect-error - key as "name" | "bio" didn't work -- not sure why return { success: `Successfully updated your ${key} to: '${userEdit.data[key]}'.` }; } ================================================ FILE: with-platform-supabase-tailwind-prisma/src/app/_components/autocomplete.tsx ================================================ "use client"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { Check } from "lucide-react"; import { useRouter } from "next/navigation"; import React, { forwardRef, useState } from "react"; export const defaultSort = { title: "Relevance", slug: null, sortKey: "RELEVANCE", reverse: false, }; export const sorting = [ defaultSort, { title: "Availability", slug: "available-desc", sortKey: "MOST_AVAILABLE", reverse: false, }, // asc ]; export type Option = { value: string; label: string }; export interface AutocompleteSearchProps extends React.ComponentPropsWithoutRef { className?: string; options: Array