├── .eslintrc.json ├── src ├── app │ ├── (auth) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ ├── sign-up │ │ │ └── [[...sign-up]] │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (public) │ │ ├── layout.tsx │ │ └── book │ │ │ └── [clerkUserId] │ │ │ ├── [eventId] │ │ │ ├── loading.tsx │ │ │ ├── success │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── (private) │ │ ├── events │ │ │ ├── new │ │ │ │ └── page.tsx │ │ │ ├── [eventId] │ │ │ │ └── edit │ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── schedule │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── page.tsx │ ├── layout.tsx │ └── globals.css ├── data │ └── constants.ts ├── drizzle │ ├── db.ts │ ├── migrations │ │ ├── meta │ │ │ ├── _journal.json │ │ │ └── 0000_snapshot.json │ │ └── 0000_dazzling_penance.sql │ └── schema.ts ├── lib │ ├── utils.ts │ ├── formatters.ts │ └── getValidTimesFromSchedule.ts ├── schema │ ├── events.ts │ ├── meetings.ts │ └── schedule.ts ├── components │ ├── NavLink.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── switch.tsx │ │ ├── popover.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── calendar.tsx │ │ ├── form.tsx │ │ ├── alert-dialog.tsx │ │ └── select.tsx │ ├── CopyEventButton.tsx │ └── forms │ │ ├── EventForm.tsx │ │ ├── ScheduleForm.tsx │ │ └── MeetingForm.tsx ├── middleware.ts └── server │ ├── actions │ ├── meetings.ts │ ├── schedule.ts │ └── events.ts │ └── googleCalendar.ts ├── postcss.config.mjs ├── next.config.mjs ├── .env.example ├── drizzle.config.ts ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json └── tailwind.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs" 2 | 3 | export default function Page() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs" 2 | 3 | export default function Page() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/data/constants.ts: -------------------------------------------------------------------------------- 1 | export const DAYS_OF_WEEK_IN_ORDER = [ 2 | "monday", 3 | "tuesday", 4 | "wednesday", 5 | "thursday", 6 | "friday", 7 | "saturday", 8 | "sunday", 9 | ] as const 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | staleTimes: { 5 | dynamic: 0, 6 | }, 7 | }, 8 | } 9 | 10 | export default nextConfig 11 | -------------------------------------------------------------------------------- /src/app/(public)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | export default function PublicLayout({ children }: { children: ReactNode }) { 4 | return
{children}
5 | } 6 | -------------------------------------------------------------------------------- /src/drizzle/db.ts: -------------------------------------------------------------------------------- 1 | import { neon } from "@neondatabase/serverless" 2 | import { drizzle } from "drizzle-orm/neon-http" 3 | import * as schema from "./schema" 4 | 5 | const sql = neon(process.env.DATABASE_URL!) 6 | export const db = drizzle(sql, { schema }) 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CLERK_SECRET_KEY= 2 | DATABASE_URL= 3 | GOOGLE_OAUTH_CLIENT_ID= 4 | GOOGLE_OAUTH_CLIENT_SECRET= 5 | GOOGLE_OAUTH_REDIRECT_URL= 6 | 7 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 8 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 9 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up -------------------------------------------------------------------------------- /src/drizzle/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1725382070346, 9 | "tag": "0000_dazzling_penance", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export function timeToInt(time: string) { 9 | return parseFloat(time.replace(":", ".")) 10 | } 11 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit" 2 | 3 | export default defineConfig({ 4 | schema: "./src/drizzle/schema.ts", 5 | out: "./src/drizzle/migrations", 6 | dialect: "postgresql", 7 | strict: true, 8 | verbose: true, 9 | dbCredentials: { 10 | url: process.env.DATABASE_URL as string, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /src/schema/events.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const eventFormSchema = z.object({ 4 | name: z.string().min(1, "Required"), 5 | description: z.string().optional(), 6 | isActive: z.boolean().default(true), 7 | durationInMinutes: z.coerce 8 | .number() 9 | .int() 10 | .positive("Duration must be greater than 0") 11 | .max(60 * 12, `Duration must be less than 12 hours (${60 * 12} minutes)`), 12 | }) 13 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server" 2 | import { redirect } from "next/navigation" 3 | import { ReactNode } from "react" 4 | 5 | export default function AuthLayout({ children }: { children: ReactNode }) { 6 | const { userId } = auth() 7 | if (userId != null) redirect("/") 8 | 9 | return ( 10 |
11 | {children} 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(private)/events/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { EventForm } from "@/components/forms/EventForm" 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 3 | 4 | export default function NewEventPage() { 5 | return ( 6 | 7 | 8 | New Event 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /src/app/(public)/book/[clerkUserId]/[eventId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircle } from "lucide-react" 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
7 | Loading... 8 |
9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/NavLink.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { cn } from "@/lib/utils" 4 | import Link from "next/link" 5 | import { usePathname } from "next/navigation" 6 | import { ComponentProps } from "react" 7 | 8 | export function NavLink({ className, ...props }: ComponentProps) { 9 | const path = usePathname() 10 | const isActive = path === props.href 11 | 12 | return ( 13 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" 2 | 3 | const isPublicRoute = createRouteMatcher([ 4 | "/", 5 | "/sign-in(.*)", 6 | "/sign-up(.*)", 7 | "/book(.*)", 8 | ]) 9 | 10 | export default clerkMiddleware((auth, req) => { 11 | if (!isPublicRoute(req)) { 12 | auth().protect() 13 | } 14 | }) 15 | 16 | export const config = { 17 | matcher: [ 18 | // Skip Next.js internals and all static files, unless found in search params 19 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 20 | // Always run for API routes 21 | "/(api|trpc)(.*)", 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /src/schema/meetings.ts: -------------------------------------------------------------------------------- 1 | import { startOfDay } from "date-fns" 2 | import { z } from "zod" 3 | 4 | const meetingSchemaBase = z.object({ 5 | startTime: z.date().min(new Date()), 6 | guestEmail: z.string().email().min(1, "Required"), 7 | guestName: z.string().min(1, "Required"), 8 | guestNotes: z.string().optional(), 9 | timezone: z.string().min(1, "Required"), 10 | }) 11 | 12 | export const meetingFormSchema = z 13 | .object({ 14 | date: z.date().min(startOfDay(new Date()), "Must be in the future"), 15 | }) 16 | .merge(meetingSchemaBase) 17 | 18 | export const meetingActionSchema = z 19 | .object({ 20 | eventId: z.string().min(1, "Required"), 21 | clerkUserId: z.string().min(1, "Required"), 22 | }) 23 | .merge(meetingSchemaBase) 24 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { SignInButton, SignUpButton, UserButton } from "@clerk/nextjs" 3 | import { auth } from "@clerk/nextjs/server" 4 | import { redirect } from "next/navigation" 5 | 6 | export default function HomePage() { 7 | const { userId } = auth() 8 | if (userId != null) redirect("/events") 9 | 10 | return ( 11 |
12 |

Fancy Home Page

13 |
14 | 17 | 20 | 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |