├── .eslintrc ├── app ├── favicon.ico ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.jsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.jsx │ └── layout.js ├── (main) │ ├── layout.jsx │ ├── video-call │ │ ├── page.jsx │ │ └── video-call-ui.jsx │ ├── doctors │ │ ├── layout.js │ │ ├── [specialty] │ │ │ ├── [id] │ │ │ │ ├── page.jsx │ │ │ │ ├── layout.js │ │ │ │ └── _components │ │ │ │ │ ├── appointment-form.jsx │ │ │ │ │ ├── slot-picker.jsx │ │ │ │ │ └── doctor-profile.jsx │ │ │ └── page.jsx │ │ ├── page.jsx │ │ └── components │ │ │ └── doctor-card.jsx │ ├── doctor │ │ ├── layout.js │ │ ├── _components │ │ │ ├── appointments-list.jsx │ │ │ └── availability-settings.jsx │ │ ├── page.jsx │ │ └── verification │ │ │ └── page.jsx │ ├── admin │ │ ├── page.jsx │ │ ├── layout.js │ │ └── components │ │ │ ├── verified-doctors.jsx │ │ │ └── pending-doctors.jsx │ ├── onboarding │ │ ├── layout.js │ │ └── page.jsx │ ├── pricing │ │ └── page.jsx │ └── appointments │ │ └── page.jsx ├── layout.js ├── globals.css └── page.js ├── public ├── logo.png ├── banner.png ├── banner2.png └── logo-single.png ├── jsconfig.json ├── postcss.config.mjs ├── lib ├── utils.js ├── prisma.js ├── schema.js ├── checkUser.js ├── private.key ├── specialities.js └── data.js ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20250517054209_credits │ │ └── migration.sql │ ├── 20250517064915_vr │ │ └── migration.sql │ ├── 20250527071527_update_payout │ │ └── migration.sql │ ├── 20250520101140_update_appointments │ │ └── migration.sql │ ├── 20250527063022_payout │ │ └── migration.sql │ └── 20250515081608_create_modal │ │ └── migration.sql └── schema.prisma ├── components ├── theme-provider.jsx ├── ui │ ├── sonner.jsx │ ├── label.jsx │ ├── separator.jsx │ ├── textarea.jsx │ ├── input.jsx │ ├── badge.jsx │ ├── alert.jsx │ ├── tabs.jsx │ ├── button.jsx │ ├── card.jsx │ ├── dialog.jsx │ └── select.jsx ├── pricing.jsx ├── page-header.jsx └── header.jsx ├── README.md ├── next.config.mjs ├── eslint.config.mjs ├── components.json ├── .gitignore ├── actions ├── doctors-listing.js ├── patient.js ├── onboarding.js ├── payout.js ├── credits.js ├── admin.js └── doctor.js ├── hooks └── use-fetch.js ├── middleware.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-vars": ["warn"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/doctors-appointment-platform/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/doctors-appointment-platform/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/doctors-appointment-platform/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/banner2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/doctors-appointment-platform/HEAD/public/banner2.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/logo-single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyush-eon/doctors-appointment-platform/HEAD/public/logo-single.png -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.jsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.jsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import { clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /app/(auth)/layout.js: -------------------------------------------------------------------------------- 1 | const AuthLayout = ({ children }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default AuthLayout; 6 | -------------------------------------------------------------------------------- /app/(main)/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MainLayout = ({ children }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default MainLayout; 8 | -------------------------------------------------------------------------------- /prisma/migrations/20250517054209_credits/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `CreditPackage` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "CreditPackage"; 9 | -------------------------------------------------------------------------------- /app/(main)/video-call/page.jsx: -------------------------------------------------------------------------------- 1 | import VideoCall from "./video-call-ui"; 2 | 3 | export default async function VideoCallPage({ searchParams }) { 4 | const { sessionId, token } = await searchParams; 5 | 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /components/theme-provider.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ children, ...props }) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Doctors Appointment Platform with Next JS, Neon, Tailwind, Vonage, Shadcn UI Tutorial 🔥🔥 2 | ## https://www.youtube.com/watch?v=ID1PRFF1dlw 3 | 4 | Screenshot 2025-05-27 at 1 18 06 PM 5 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverComponentsHmrCache: false, // defaults to true 5 | }, 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: "https", 10 | hostname: "img.clerk.com", 11 | }, 12 | ], 13 | }, 14 | }; 15 | 16 | export default nextConfig; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20250517064915_vr/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `description` on the `CreditTransaction` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "CreditTransaction" DROP COLUMN "description"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "User" ALTER COLUMN "credits" SET DEFAULT 2; 12 | -------------------------------------------------------------------------------- /app/(main)/doctors/layout.js: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Find Doctors - MediMeet", 3 | description: "Browse and book appointments with top healthcare providers", 4 | }; 5 | 6 | export default async function DoctorsLayout({ children }) { 7 | return ( 8 |
9 |
{children}
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [...compat.extends("next/core-web-vitals")]; 13 | 14 | export default eslintConfig; 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": false, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 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 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /lib/prisma.js: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const db = globalThis.prisma || new PrismaClient(); 4 | 5 | if (process.env.NODE_ENV !== "production") { 6 | globalThis.prisma = db; 7 | } 8 | 9 | // globalThis.prisma: This global variable ensures that the Prisma client instance is 10 | // reused across hot reloads during development. Without this, each time your application 11 | // reloads, a new instance of the Prisma client would be created, potentially leading 12 | // to connection issues. 13 | -------------------------------------------------------------------------------- /app/(main)/doctor/layout.js: -------------------------------------------------------------------------------- 1 | import { Stethoscope } from "lucide-react"; 2 | import { PageHeader } from "@/components/page-header"; 3 | 4 | export const metadata = { 5 | title: "Doctor Dashboard - MediMeet", 6 | description: "Manage your appointments and availability", 7 | }; 8 | 9 | export default async function DoctorDashboardLayout({ children }) { 10 | return ( 11 |
12 | } title="Doctor Dashboard" /> 13 | 14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /prisma/migrations/20250527071527_update_payout/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `month` on the `Payout` table. All the data in the column will be lost. 5 | - You are about to drop the column `year` on the `Payout` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- DropIndex 9 | DROP INDEX "Payout_doctorId_month_year_key"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Payout" DROP COLUMN "month", 13 | DROP COLUMN "year"; 14 | 15 | -- CreateIndex 16 | CREATE INDEX "Payout_doctorId_status_idx" ON "Payout"("doctorId", "status"); 17 | -------------------------------------------------------------------------------- /components/ui/sonner.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner"; 5 | 6 | const Toaster = ({ 7 | ...props 8 | }) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | () 23 | ); 24 | } 25 | 26 | export { Toaster } 27 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | 3 | export const doctorFormSchema = z.object({ 4 | specialty: z.string().min(1, "Specialty is required"), 5 | experience: z 6 | .number({ invalid_type_error: "Experience must be a number" }) 7 | .int() 8 | .min(1, "Experience must be at least 1 year") 9 | .max(70, "Experience must be less than 70 years"), 10 | credentialUrl: z 11 | .string() 12 | .url("Please enter a valid URL") 13 | .min(1, "Credential URL is required"), 14 | description: z 15 | .string() 16 | .min(20, "Description must be at least 20 characters") 17 | .max(1000, "Description cannot exceed 1000 characters"), 18 | }); 19 | -------------------------------------------------------------------------------- /components/ui/label.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }) { 12 | return ( 13 | () 20 | ); 21 | } 22 | 23 | export { Label } 24 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | lib/private.key -------------------------------------------------------------------------------- /actions/doctors-listing.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/lib/prisma"; 4 | 5 | /** 6 | * Get doctors by specialty 7 | */ 8 | export async function getDoctorsBySpecialty(specialty) { 9 | try { 10 | const doctors = await db.user.findMany({ 11 | where: { 12 | role: "DOCTOR", 13 | verificationStatus: "VERIFIED", 14 | specialty: specialty.split("%20").join(" "), 15 | }, 16 | orderBy: { 17 | name: "asc", 18 | }, 19 | }); 20 | 21 | return { doctors }; 22 | } catch (error) { 23 | console.error("Failed to fetch doctors by specialty:", error); 24 | return { error: "Failed to fetch doctors" }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /hooks/use-fetch.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { toast } from "sonner"; 3 | 4 | const useFetch = (cb) => { 5 | const [data, setData] = useState(undefined); 6 | const [loading, setLoading] = useState(null); 7 | const [error, setError] = useState(null); 8 | 9 | const fn = async (...args) => { 10 | setLoading(true); 11 | setError(null); 12 | 13 | try { 14 | const response = await cb(...args); 15 | setData(response); 16 | setError(null); 17 | } catch (error) { 18 | setError(error); 19 | toast.error(error.message); 20 | } finally { 21 | setLoading(false); 22 | } 23 | }; 24 | 25 | return { data, loading, error, fn, setData }; 26 | }; 27 | 28 | export default useFetch; 29 | -------------------------------------------------------------------------------- /components/pricing.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Card, CardContent } from "./ui/card"; 5 | import { PricingTable } from "@clerk/nextjs"; 6 | 7 | const Pricing = () => { 8 | return ( 9 | 10 | 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default Pricing; 28 | -------------------------------------------------------------------------------- /components/ui/separator.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }) { 14 | return ( 15 | () 24 | ); 25 | } 26 | 27 | export { Separator } 28 | -------------------------------------------------------------------------------- /components/ui/textarea.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ 6 | className, 7 | ...props 8 | }) { 9 | return ( 10 | (