[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-73817":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":10,"archived":22,"fork":22,"defaultBranch":23,"hasWiki":24,"hasPages":22,"topics":25,"createdAt":10,"pushedAt":10,"updatedAt":30,"readmeContent":31,"aiSummary":32,"trendingCount":16,"starSnapshotCount":16,"syncStatus":33,"lastSyncTime":34,"discoverSource":35},73817,"healthcare","adrianhajdin\u002Fhealthcare","adrianhajdin","Build a healthcare platform that streamlines patient registration, appointment scheduling, and medical records, and learn to implement complex forms and SMS notifications.","https:\u002F\u002Fjsmastery.pro",null,"TypeScript",2687,754,19,101,0,1,6,9,3,63.53,false,"main",true,[26,27,28,29],"appwrite","nextjs","tailwindcss","twilio","2026-06-12 04:01:11","\u003Cdiv align=\"center\">\n  \u003Cbr \u002F>\n    \u003Ca href=\"https:\u002F\u002Fyoutu.be\u002FlEflo_sc82g?feature=shared\" target=\"_blank\">\n      \u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fadrianhajdin\u002Fhealthcare\u002Fassets\u002F151519281\u002Fa7dd73b6-93de-484d-84e0-e7f4e299167b\" alt=\"Project Banner\">\n    \u003C\u002Fa>\n  \u003Cbr \u002F>\n\n  \u003Cdiv>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Next_JS-black?style=for-the-badge&logoColor=white&logo=nextdotjs&color=000000\" alt=\"nextdotjs\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-TypeScript-black?style=for-the-badge&logoColor=white&logo=typescript&color=3178C6\" alt=\"typescript\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Tailwind_CSS-black?style=for-the-badge&logoColor=white&logo=tailwindcss&color=06B6D4\" alt=\"tailwindcss\" \u002F>\n    \u003Cimg src=\"https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F-Appwrite-black?style=for-the-badge&logoColor=white&logo=appwrite&color=FD366E\" alt=\"appwrite\" \u002F>\n  \u003C\u002Fdiv>\n\n  \u003Ch3 align=\"center\">A HealthCare Management System\u003C\u002Fh3>\n\n   \u003Cdiv align=\"center\">\n     Build this project step by step with our detailed tutorial on \u003Ca href=\"https:\u002F\u002Fwww.youtube.com\u002F@javascriptmastery\u002Fvideos\" target=\"_blank\">\u003Cb>JavaScript Mastery\u003C\u002Fb>\u003C\u002Fa> YouTube. Join the JSM family!\n    \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n## 📋 \u003Ca name=\"table\">Table of Contents\u003C\u002Fa>\n\n1. 🤖 [Introduction](#introduction)\n2. ⚙️ [Tech Stack](#tech-stack)\n3. 🔋 [Features](#features)\n4. 🤸 [Quick Start](#quick-start)\n5. 🕸️ [Snippets (Code to Copy)](#snippets)\n6. 🔗 [Assets](#links)\n7. 🚀 [More](#more)\n\n## 🚨 Tutorial\n\nThis repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, \u003Ca href=\"https:\u002F\u002Fwww.youtube.com\u002F@javascriptmastery\u002Fvideos\" target=\"_blank\">\u003Cb>JavaScript Mastery\u003C\u002Fb>\u003C\u002Fa>.\n\nIf you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!\n\n\u003Ca href=\"https:\u002F\u002Fyoutu.be\u002FlEflo_sc82g?feature=shared\" target=\"_blank\">\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002F1736fca5-a031-4854-8c09-bc110e3bc16d\" \u002F>\u003C\u002Fa>\n\n## \u003Ca name=\"introduction\">🤖 Introduction\u003C\u002Fa>\n\nA healthcare patient management application that allows patients to easily register, book, and manage their appointments with doctors, featuring administrative tools for scheduling, confirming, and canceling appointments, along with SMS notifications, all built using Next.js.\n\nIf you're getting started and need assistance or face any bugs, join our active Discord community with over **34k+** members. It's a place where people help each other out.\n\n\u003Ca href=\"https:\u002F\u002Fdiscord.com\u002Finvite\u002Fn6EdbFJ\" target=\"_blank\">\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002F618f4872-1e10-42da-8213-1d69e486d02e\" \u002F>\u003C\u002Fa>\n\n## \u003Ca name=\"tech-stack\">⚙️ Tech Stack\u003C\u002Fa>\n\n- Next.js\n- Appwrite\n- Typescript\n- TailwindCSS\n- ShadCN\n- Twilio\n\n## \u003Ca name=\"features\">🔋 Features\u003C\u002Fa>\n\n👉 **Register as a Patient**: Users can sign up and create a personal profile as a patient.\n\n👉 **Book a New Appointment with Doctor**: Patients can schedule appointments with doctors at their convenience and can book multiple appointments.\n\n👉 **Manage Appointments on Admin Side**: Administrators can efficiently view and handle all scheduled appointments.\n\n👉 **Confirm\u002FSchedule Appointment from Admin Side**: Admins can confirm and set appointment times to ensure they are properly scheduled.\n\n👉 **Cancel Appointment from Admin Side**: Administrators have the ability to cancel any appointment as needed.\n\n👉 **Send SMS on Appointment Confirmation**: Patients receive SMS notifications to confirm their appointment details.\n\n👉 **Complete Responsiveness**: The application works seamlessly on all device types and screen sizes.\n\n👉 **File Upload Using Appwrite Storage**: Users can upload and store files securely within the app using Appwrite storage services.\n\n👉 **Manage and Track Application Performance Using Sentry**: The application uses Sentry to monitor and track its performance and detect any errors.\n\nand many more, including code architecture and reusability\n\n## \u003Ca name=\"quick-start\">🤸 Quick Start\u003C\u002Fa>\n\nFollow these steps to set up the project locally on your machine.\n\n**Prerequisites**\n\nMake sure you have the following installed on your machine:\n\n- [Git](https:\u002F\u002Fgit-scm.com\u002F)\n- [Node.js](https:\u002F\u002Fnodejs.org\u002Fen)\n- [npm](https:\u002F\u002Fwww.npmjs.com\u002F) (Node Package Manager)\n\n**Cloning the Repository**\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fadrianhajdin\u002Fhealthcare.git\ncd healthcare\n```\n\n**Installation**\n\nInstall the project dependencies using npm:\n\n```bash\nnpm install\n```\n\n**Set Up Environment Variables**\n\nCreate a new file named `.env.local` in the root of your project and add the following content:\n\n```env\n#APPWRITE\nNEXT_PUBLIC_ENDPOINT=https:\u002F\u002Fcloud.appwrite.io\u002Fv1\nPROJECT_ID=\nAPI_KEY=\nDATABASE_ID=\nPATIENT_COLLECTION_ID=\nAPPOINTMENT_COLLECTION_ID=\nNEXT_PUBLIC_BUCKET_ID=\n\nNEXT_PUBLIC_ADMIN_PASSKEY=111111\n```\n\nReplace the placeholder values with your actual Appwrite credentials. You can obtain these credentials by signing up on the [Appwrite website](https:\u002F\u002Fappwrite.io\u002F).\n\n**Running the Project**\n\n```bash\nnpm run dev\n```\n\nOpen [http:\u002F\u002Flocalhost:3000](http:\u002F\u002Flocalhost:3000) in your browser to view the project.\n\n## \u003Ca name=\"snippets\">🕸️ Snippets\u003C\u002Fa>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>tailwind.config.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nimport type { Config } from \"tailwindcss\";\n\nconst { fontFamily } = require(\"tailwindcss\u002FdefaultTheme\");\n\nconst config = {\n  darkMode: [\"class\"],\n  content: [\n    \".\u002Fpages\u002F**\u002F*.{ts,tsx}\",\n    \".\u002Fcomponents\u002F**\u002F*.{ts,tsx}\",\n    \".\u002Fapp\u002F**\u002F*.{ts,tsx}\",\n    \".\u002Fsrc\u002F**\u002F*.{ts,tsx}\",\n  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        green: {\n          500: \"#24AE7C\",\n          600: \"#0D2A1F\",\n        },\n        blue: {\n          500: \"#79B5EC\",\n          600: \"#152432\",\n        },\n        red: {\n          500: \"#F37877\",\n          600: \"#3E1716\",\n          700: \"#F24E43\",\n        },\n        light: {\n          200: \"#E8E9E9\",\n        },\n        dark: {\n          200: \"#0D0F10\",\n          300: \"#131619\",\n          400: \"#1A1D21\",\n          500: \"#363A3D\",\n          600: \"#76828D\",\n          700: \"#ABB8C4\",\n        },\n      },\n      fontFamily: {\n        sans: [\"var(--font-sans)\", ...fontFamily.sans],\n      },\n      backgroundImage: {\n        appointments: \"url('\u002Fassets\u002Fimages\u002Fappointments-bg.png')\",\n        pending: \"url('\u002Fassets\u002Fimages\u002Fpending-bg.png')\",\n        cancelled: \"url('\u002Fassets\u002Fimages\u002Fcancelled-bg.png')\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n        \"caret-blink\": {\n          \"0%,70%,100%\": { opacity: \"1\" },\n          \"20%,50%\": { opacity: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n        \"caret-blink\": \"caret-blink 1.25s ease-out infinite\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n} satisfies Config;\n\nexport default config;\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>app\u002Fglobals.css\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n\u002F* ========================================== TAILWIND STYLES *\u002F\n@layer base {\n  \u002F* Remove scrollbar *\u002F\n  .remove-scrollbar::-webkit-scrollbar {\n    width: 0px;\n    height: 0px;\n    border-radius: 0px;\n  }\n\n  .remove-scrollbar::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  .remove-scrollbar::-webkit-scrollbar-thumb {\n    background: transparent;\n    border-radius: 0px;\n  }\n\n  .remove-scrollbar::-webkit-scrollbar-thumb:hover {\n    \u002F* background: #1e2238; *\u002F\n    background: transparent;\n  }\n}\n\n@layer utilities {\n  \u002F* ===== UTILITIES *\u002F\n  .sidebar {\n    @apply remove-scrollbar w-full max-w-72 flex-col overflow-auto bg-black-800 px-7 py-10;\n  }\n\n  .left-sidebar {\n    @apply hidden lg:flex;\n  }\n\n  .right-sidebar {\n    @apply hidden xl:flex;\n  }\n\n  .clip-text {\n    @apply bg-clip-text text-transparent;\n  }\n\n  .bg-image {\n    @apply bg-black-900 bg-light-rays bg-cover bg-no-repeat;\n  }\n\n  .header {\n    @apply text-32-bold md:text-36-bold;\n  }\n\n  .sub-header {\n    @apply text-18-bold md:text-24-bold;\n  }\n\n  .container {\n    @apply relative flex-1 overflow-y-auto px-[5%];\n  }\n\n  .sub-container {\n    @apply mx-auto flex size-full flex-col py-10;\n  }\n\n  .side-img {\n    @apply hidden h-full object-cover md:block;\n  }\n\n  .copyright {\n    @apply text-14-regular justify-items-end text-center text-dark-600 xl:text-left;\n  }\n\n  \u002F* ==== SUCCESS *\u002F\n  .success-img {\n    @apply m-auto flex flex-1 flex-col items-center justify-between gap-10 py-10;\n  }\n\n  .request-details {\n    @apply flex w-full flex-col items-center gap-8 border-y-2 border-dark-400 py-8 md:w-fit md:flex-row;\n  }\n\n  \u002F* ===== ADMIN *\u002F\n  .admin-header {\n    @apply sticky top-3 z-20 mx-3 flex items-center justify-between rounded-2xl bg-dark-200 px-[5%] py-5 shadow-lg xl:px-12;\n  }\n\n  .admin-main {\n    @apply flex flex-col items-center space-y-6 px-[5%] pb-12 xl:space-y-12 xl:px-12;\n  }\n\n  .admin-stat {\n    @apply flex w-full flex-col justify-between gap-5 sm:flex-row xl:gap-10;\n  }\n\n  \u002F* ==== FORM *\u002F\n  .radio-group {\n    @apply flex h-full flex-1 items-center gap-2 rounded-md border border-dashed border-dark-500 bg-dark-400 p-3;\n  }\n\n  .checkbox-label {\n    @apply cursor-pointer text-sm font-medium text-dark-700 peer-disabled:cursor-not-allowed peer-disabled:opacity-70 md:leading-none;\n  }\n\n  \u002F* ==== File Upload *\u002F\n  .file-upload {\n    @apply text-12-regular flex cursor-pointer  flex-col items-center justify-center gap-3 rounded-md border border-dashed border-dark-500 bg-dark-400 p-5;\n  }\n\n  .file-upload_label {\n    @apply flex flex-col justify-center gap-2 text-center text-dark-600;\n  }\n\n  \u002F* ==== Stat Card *\u002F\n  .stat-card {\n    @apply flex flex-1 flex-col gap-6 rounded-2xl bg-cover p-6 shadow-lg;\n  }\n\n  \u002F* ==== Status Badge *\u002F\n  .status-badge {\n    @apply flex w-fit items-center gap-2 rounded-full px-4 py-2;\n  }\n\n  \u002F* Data Table *\u002F\n  .data-table {\n    @apply z-10 w-full overflow-hidden rounded-lg border border-dark-400 shadow-lg;\n  }\n\n  .table-actions {\n    @apply flex w-full items-center justify-between space-x-2 p-4;\n  }\n\n  \u002F* ===== ALIGNMENTS *\u002F\n  .flex-center {\n    @apply flex items-center justify-center;\n  }\n\n  .flex-between {\n    @apply flex items-center justify-between;\n  }\n\n  \u002F* ===== TYPOGRAPHY *\u002F\n  .text-36-bold {\n    @apply text-[36px] leading-[40px] font-bold;\n  }\n\n  .text-24-bold {\n    @apply text-[24px] leading-[28px] font-bold;\n  }\n\n  .text-32-bold {\n    @apply text-[32px] leading-[36px] font-bold;\n  }\n\n  .text-18-bold {\n    @apply text-[18px] leading-[24px] font-bold;\n  }\n\n  .text-16-semibold {\n    @apply text-[16px] leading-[20px] font-semibold;\n  }\n\n  .text-16-regular {\n    @apply text-[16px] leading-[20px] font-normal;\n  }\n\n  .text-14-medium {\n    @apply text-[14px] leading-[18px] font-medium;\n  }\n\n  .text-14-regular {\n    @apply text-[14px] leading-[18px] font-normal;\n  }\n\n  .text-12-regular {\n    @apply text-[12px] leading-[16px] font-normal;\n  }\n\n  .text-12-semibold {\n    @apply text-[12px] leading-[16px] font-semibold;\n  }\n\n  \u002F* =====  SHADCN OVERRIDES *\u002F\n  .shad-primary-btn {\n    @apply bg-green-500 text-white !important;\n  }\n\n  .shad-danger-btn {\n    @apply bg-red-700 text-white !important;\n  }\n\n  .shad-gray-btn {\n    @apply border border-dark-500 cursor-pointer bg-dark-400 text-white !important;\n  }\n\n  .shad-input-label {\n    @apply text-14-medium text-dark-700 !important;\n  }\n\n  .shad-input {\n    @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 h-11 focus-visible:ring-0 focus-visible:ring-offset-0 !important;\n  }\n\n  .shad-input-icon {\n    @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 h-11 focus-visible:ring-0 focus-visible:ring-offset-0 !important;\n  }\n\n  .shad-textArea {\n    @apply bg-dark-400 placeholder:text-dark-600 border-dark-500 focus-visible:ring-0 focus-visible:ring-offset-0 !important;\n  }\n\n  .shad-combobox-item {\n    @apply data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 !important;\n  }\n\n  .shad-combobox-trigger {\n    @apply h-11 !important;\n  }\n\n  .shad-select-trigger {\n    @apply bg-dark-400  placeholder:text-dark-600 border-dark-500 h-11 focus:ring-0 focus:ring-offset-0 !important;\n  }\n\n  .shad-select-content {\n    @apply bg-dark-400 border-dark-500 !important;\n  }\n\n  .shad-dialog {\n    @apply bg-dark-400 border-dark-500 !important;\n  }\n\n  .shad-dialog button {\n    @apply focus:ring-0 focus:ring-offset-0 focus-visible:border-none focus-visible:outline-none focus-visible:ring-transparent focus-visible:ring-offset-0 !important;\n  }\n\n  .shad-error {\n    @apply text-red-400 !important;\n  }\n\n  .shad-table {\n    @apply rounded-lg overflow-hidden !important;\n  }\n\n  .shad-table-row-header {\n    @apply border-b border-dark-400 text-light-200 hover:bg-transparent !important;\n  }\n\n  .shad-table-row {\n    @apply border-b border-dark-400 text-light-200 !important;\n  }\n\n  .shad-otp {\n    @apply w-full flex justify-between !important;\n  }\n\n  .shad-otp-slot {\n    @apply text-36-bold justify-center flex border border-dark-500 rounded-lg size-16 gap-4 !important;\n  }\n\n  .shad-alert-dialog {\n    @apply space-y-5 bg-dark-400 border-dark-500 outline-none !important;\n  }\n\n  .shad-sheet-content button {\n    @apply top-2 focus:ring-0 focus:ring-offset-0 focus-visible:border-none focus-visible:outline-none focus-visible:ring-transparent focus-visible:ring-offset-0 !important;\n  }\n\n  \u002F* =====  REACT PHONE NUMBER INPUT OVERRIDES *\u002F\n  .input-phone {\n    @apply mt-2 h-11 rounded-md px-3 text-sm border bg-dark-400 placeholder:text-dark-600 border-dark-500 !important;\n  }\n\n  \u002F* =====  REACT DATE PICKER OVERRIDES *\u002F\n  .date-picker {\n    @apply overflow-hidden border-transparent w-full placeholder:text-dark-600  h-11 text-14-medium rounded-md px-3 outline-none !important;\n  }\n}\n\n\u002F* =====  REACT-DATEPICKER OVERRIDES *\u002F\n.react-datepicker-wrapper.date-picker {\n  display: flex;\n  align-items: center;\n}\n\n.react-datepicker,\n.react-datepicker__time,\n.react-datepicker__header,\n.react-datepicker__current-month,\n.react-datepicker__day-name,\n.react-datepicker__day,\n.react-datepicker-time__header {\n  background-color: #1a1d21 !important;\n  border-color: #363a3d !important;\n  color: #abb8c4 !important;\n}\n\n.react-datepicker__current-month,\n.react-datepicker__day-name,\n.react-datepicker-time__header {\n  color: #ffffff !important;\n}\n\n.react-datepicker__triangle {\n  fill: #1a1d21 !important;\n  color: #1a1d21 !important;\n  stroke: #1a1d21 !important;\n}\n\n.react-datepicker__time-list-item:hover {\n  background-color: #363a3d !important;\n}\n\n.react-datepicker__input-container input {\n  background-color: #1a1d21 !important;\n  width: 100%;\n  outline: none;\n}\n\n.react-datepicker__day--selected {\n  background-color: #24ae7c !important;\n  color: #ffffff !important;\n  border-radius: 4px;\n}\n\n.react-datepicker__time-list-item--selected {\n  background-color: #24ae7c !important;\n}\n\n.react-datepicker__time-container {\n  border-left: 1px solid #363a3d !important;\n}\n\n.react-datepicker__time-list-item {\n  display: flex !important;\n  align-items: center !important;\n}\n\n\u002F* =====  REACT PHONE NUMBER INPUT OVERRIDES *\u002F\n.PhoneInputInput {\n  outline: none;\n  margin-left: 4px;\n  background: #1a1d21;\n  font-size: 14px;\n  font-weight: 500;\n}\n\n.PhoneInputInput::placeholder {\n  color: #1a1d21;\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>types\u002Findex.d.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\n\u002F* eslint-disable no-unused-vars *\u002F\n\ndeclare type SearchParamProps = {\n  params: { [key: string]: string };\n  searchParams: { [key: string]: string | string[] | undefined };\n};\n\ndeclare type Gender = \"Male\" | \"Female\" | \"Other\";\ndeclare type Status = \"pending\" | \"scheduled\" | \"cancelled\";\n\ndeclare interface CreateUserParams {\n  name: string;\n  email: string;\n  phone: string;\n}\ndeclare interface User extends CreateUserParams {\n  $id: string;\n}\n\ndeclare interface RegisterUserParams extends CreateUserParams {\n  userId: string;\n  birthDate: Date;\n  gender: Gender;\n  address: string;\n  occupation: string;\n  emergencyContactName: string;\n  emergencyContactNumber: string;\n  primaryPhysician: string;\n  insuranceProvider: string;\n  insurancePolicyNumber: string;\n  allergies: string | undefined;\n  currentMedication: string | undefined;\n  familyMedicalHistory: string | undefined;\n  pastMedicalHistory: string | undefined;\n  identificationType: string | undefined;\n  identificationNumber: string | undefined;\n  identificationDocument: FormData | undefined;\n  privacyConsent: boolean;\n}\n\ndeclare type CreateAppointmentParams = {\n  userId: string;\n  patient: string;\n  primaryPhysician: string;\n  reason: string;\n  schedule: Date;\n  status: Status;\n  note: string | undefined;\n};\n\ndeclare type UpdateAppointmentParams = {\n  appointmentId: string;\n  userId: string;\n  appointment: Appointment;\n  type: string;\n};\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>types\u002Fappwrite.types.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nimport { Models } from \"node-appwrite\";\n\nexport interface Patient extends Models.Document {\n  userId: string;\n  name: string;\n  email: string;\n  phone: string;\n  birthDate: Date;\n  gender: Gender;\n  address: string;\n  occupation: string;\n  emergencyContactName: string;\n  emergencyContactNumber: string;\n  primaryPhysician: string;\n  insuranceProvider: string;\n  insurancePolicyNumber: string;\n  allergies: string | undefined;\n  currentMedication: string | undefined;\n  familyMedicalHistory: string | undefined;\n  pastMedicalHistory: string | undefined;\n  identificationType: string | undefined;\n  identificationNumber: string | undefined;\n  identificationDocument: FormData | undefined;\n  privacyConsent: boolean;\n}\n\nexport interface Appointment extends Models.Document {\n  patient: Patient;\n  schedule: Date;\n  status: Status;\n  primaryPhysician: string;\n  reason: string;\n  note: string;\n  userId: string;\n  cancellationReason: string | null;\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>lib\u002Futils.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));\n\nexport const convertFileToUrl = (file: File) => URL.createObjectURL(file);\n\n\u002F\u002F FORMAT DATE TIME\nexport const formatDateTime = (dateString: Date | string) => {\n  const dateTimeOptions: Intl.DateTimeFormatOptions = {\n    \u002F\u002F weekday: \"short\", \u002F\u002F abbreviated weekday name (e.g., 'Mon')\n    month: \"short\", \u002F\u002F abbreviated month name (e.g., 'Oct')\n    day: \"numeric\", \u002F\u002F numeric day of the month (e.g., '25')\n    year: \"numeric\", \u002F\u002F numeric year (e.g., '2023')\n    hour: \"numeric\", \u002F\u002F numeric hour (e.g., '8')\n    minute: \"numeric\", \u002F\u002F numeric minute (e.g., '30')\n    hour12: true, \u002F\u002F use 12-hour clock (true) or 24-hour clock (false)\n  };\n\n  const dateDayOptions: Intl.DateTimeFormatOptions = {\n    weekday: \"short\", \u002F\u002F abbreviated weekday name (e.g., 'Mon')\n    year: \"numeric\", \u002F\u002F numeric year (e.g., '2023')\n    month: \"2-digit\", \u002F\u002F abbreviated month name (e.g., 'Oct')\n    day: \"2-digit\", \u002F\u002F numeric day of the month (e.g., '25')\n  };\n\n  const dateOptions: Intl.DateTimeFormatOptions = {\n    month: \"short\", \u002F\u002F abbreviated month name (e.g., 'Oct')\n    year: \"numeric\", \u002F\u002F numeric year (e.g., '2023')\n    day: \"numeric\", \u002F\u002F numeric day of the month (e.g., '25')\n  };\n\n  const timeOptions: Intl.DateTimeFormatOptions = {\n    hour: \"numeric\", \u002F\u002F numeric hour (e.g., '8')\n    minute: \"numeric\", \u002F\u002F numeric minute (e.g., '30')\n    hour12: true, \u002F\u002F use 12-hour clock (true) or 24-hour clock (false)\n  };\n\n  const formattedDateTime: string = new Date(dateString).toLocaleString(\n    \"en-US\",\n    dateTimeOptions\n  );\n\n  const formattedDateDay: string = new Date(dateString).toLocaleString(\n    \"en-US\",\n    dateDayOptions\n  );\n\n  const formattedDate: string = new Date(dateString).toLocaleString(\n    \"en-US\",\n    dateOptions\n  );\n\n  const formattedTime: string = new Date(dateString).toLocaleString(\n    \"en-US\",\n    timeOptions\n  );\n\n  return {\n    dateTime: formattedDateTime,\n    dateDay: formattedDateDay,\n    dateOnly: formattedDate,\n    timeOnly: formattedTime,\n  };\n};\n\nexport function encryptKey(passkey: string) {\n  return btoa(passkey);\n}\n\nexport function decryptKey(passkey: string) {\n  return atob(passkey);\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>lib\u002Fvalidation.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nimport { z } from \"zod\";\n\nexport const UserFormValidation = z.object({\n  name: z\n    .string()\n    .min(2, \"Name must be at least 2 characters\")\n    .max(50, \"Name must be at most 50 characters\"),\n  email: z.string().email(\"Invalid email address\"),\n  phone: z\n    .string()\n    .refine((phone) => \u002F^\\+\\d{10,15}$\u002F.test(phone), \"Invalid phone number\"),\n});\n\nexport const PatientFormValidation = z.object({\n  name: z\n    .string()\n    .min(2, \"Name must be at least 2 characters\")\n    .max(50, \"Name must be at most 50 characters\"),\n  email: z.string().email(\"Invalid email address\"),\n  phone: z\n    .string()\n    .refine((phone) => \u002F^\\+\\d{10,15}$\u002F.test(phone), \"Invalid phone number\"),\n  birthDate: z.coerce.date(),\n  gender: z.enum([\"Male\", \"Female\", \"Other\"]),\n  address: z\n    .string()\n    .min(5, \"Address must be at least 5 characters\")\n    .max(500, \"Address must be at most 500 characters\"),\n  occupation: z\n    .string()\n    .min(2, \"Occupation must be at least 2 characters\")\n    .max(500, \"Occupation must be at most 500 characters\"),\n  emergencyContactName: z\n    .string()\n    .min(2, \"Contact name must be at least 2 characters\")\n    .max(50, \"Contact name must be at most 50 characters\"),\n  emergencyContactNumber: z\n    .string()\n    .refine(\n      (emergencyContactNumber) => \u002F^\\+\\d{10,15}$\u002F.test(emergencyContactNumber),\n      \"Invalid phone number\"\n    ),\n  primaryPhysician: z.string().min(2, \"Select at least one doctor\"),\n  insuranceProvider: z\n    .string()\n    .min(2, \"Insurance name must be at least 2 characters\")\n    .max(50, \"Insurance name must be at most 50 characters\"),\n  insurancePolicyNumber: z\n    .string()\n    .min(2, \"Policy number must be at least 2 characters\")\n    .max(50, \"Policy number must be at most 50 characters\"),\n  allergies: z.string().optional(),\n  currentMedication: z.string().optional(),\n  familyMedicalHistory: z.string().optional(),\n  pastMedicalHistory: z.string().optional(),\n  identificationType: z.string().optional(),\n  identificationNumber: z.string().optional(),\n  identificationDocument: z.custom\u003CFile[]>().optional(),\n  treatmentConsent: z\n    .boolean()\n    .default(false)\n    .refine((value) => value === true, {\n      message: \"You must consent to treatment in order to proceed\",\n    }),\n  disclosureConsent: z\n    .boolean()\n    .default(false)\n    .refine((value) => value === true, {\n      message: \"You must consent to disclosure in order to proceed\",\n    }),\n  privacyConsent: z\n    .boolean()\n    .default(false)\n    .refine((value) => value === true, {\n      message: \"You must consent to privacy in order to proceed\",\n    }),\n});\n\nexport const CreateAppointmentSchema = z.object({\n  primaryPhysician: z.string().min(2, \"Select at least one doctor\"),\n  schedule: z.coerce.date(),\n  reason: z\n    .string()\n    .min(2, \"Reason must be at least 2 characters\")\n    .max(500, \"Reason must be at most 500 characters\"),\n  note: z.string().optional(),\n  cancellationReason: z.string().optional(),\n});\n\nexport const ScheduleAppointmentSchema = z.object({\n  primaryPhysician: z.string().min(2, \"Select at least one doctor\"),\n  schedule: z.coerce.date(),\n  reason: z.string().optional(),\n  note: z.string().optional(),\n  cancellationReason: z.string().optional(),\n});\n\nexport const CancelAppointmentSchema = z.object({\n  primaryPhysician: z.string().min(2, \"Select at least one doctor\"),\n  schedule: z.coerce.date(),\n  reason: z.string().optional(),\n  note: z.string().optional(),\n  cancellationReason: z\n    .string()\n    .min(2, \"Reason must be at least 2 characters\")\n    .max(500, \"Reason must be at most 500 characters\"),\n});\n\nexport function getAppointmentSchema(type: string) {\n  switch (type) {\n    case \"create\":\n      return CreateAppointmentSchema;\n    case \"cancel\":\n      return CancelAppointmentSchema;\n    default:\n      return ScheduleAppointmentSchema;\n  }\n}\n```\n\n\u003C\u002Fdetails>\n\n\u003Cdetails>\n\u003Csummary>\u003Ccode>constants\u002Findex.ts\u003C\u002Fcode>\u003C\u002Fsummary>\n\n```typescript\nexport const GenderOptions = [\"Male\", \"Female\", \"Other\"];\n\nexport const PatientFormDefaultValues = {\n  firstName: \"\",\n  lastName: \"\",\n  email: \"\",\n  phone: \"\",\n  birthDate: new Date(Date.now()),\n  gender: \"Male\" as Gender,\n  address: \"\",\n  occupation: \"\",\n  emergencyContactName: \"\",\n  emergencyContactNumber: \"\",\n  primaryPhysician: \"\",\n  insuranceProvider: \"\",\n  insurancePolicyNumber: \"\",\n  allergies: \"\",\n  currentMedication: \"\",\n  familyMedicalHistory: \"\",\n  pastMedicalHistory: \"\",\n  identificationType: \"Birth Certificate\",\n  identificationNumber: \"\",\n  identificationDocument: [],\n  treatmentConsent: false,\n  disclosureConsent: false,\n  privacyConsent: false,\n};\n\nexport const IdentificationTypes = [\n  \"Birth Certificate\",\n  \"Driver's License\",\n  \"Medical Insurance Card\u002FPolicy\",\n  \"Military ID Card\",\n  \"National Identity Card\",\n  \"Passport\",\n  \"Resident Alien Card (Green Card)\",\n  \"Social Security Card\",\n  \"State ID Card\",\n  \"Student ID Card\",\n  \"Voter ID Card\",\n];\n\nexport const Doctors = [\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-green.png\",\n    name: \"John Green\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-cameron.png\",\n    name: \"Leila Cameron\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-livingston.png\",\n    name: \"David Livingston\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-peter.png\",\n    name: \"Evan Peter\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-powell.png\",\n    name: \"Jane Powell\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-remirez.png\",\n    name: \"Alex Ramirez\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-lee.png\",\n    name: \"Jasmine Lee\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-cruz.png\",\n    name: \"Alyana Cruz\",\n  },\n  {\n    image: \"\u002Fassets\u002Fimages\u002Fdr-sharma.png\",\n    name: \"Hardik Sharma\",\n  },\n];\n\nexport const StatusIcon = {\n  scheduled: \"\u002Fassets\u002Ficons\u002Fcheck.svg\",\n  pending: \"\u002Fassets\u002Ficons\u002Fpending.svg\",\n  cancelled: \"\u002Fassets\u002Ficons\u002Fcancelled.svg\",\n};\n```\n\n\u003C\u002Fdetails>\n\n## \u003Ca name=\"links\">🔗 Assets\u003C\u002Fa>\n\nPublic assets used in the project can be found [here](https:\u002F\u002Fdrive.google.com\u002Ffile\u002Fd\u002F1yGvWFeSaH1_-aiQ1gejT23lqz5979RKB\u002Fview?usp=sharing)\n\n## \u003Ca name=\"more\">🚀 More\u003C\u002Fa>\n\n**Advance your skills with Next.js 14 Pro Course**\n\nEnjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!\n\n\u003Ca href=\"https:\u002F\u002Fjsmastery.pro\u002Fnext14\" target=\"_blank\">\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002F557837ce-f612-4530-ab24-189e75133c71\" alt=\"Project Banner\">\n\u003C\u002Fa>\n\n\u003Cbr \u002F>\n\u003Cbr \u002F>\n\n**Accelerate your professional journey with the Expert Training program**\n\nAnd if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together!\n\n\u003Ca href=\"https:\u002F\u002Fwww.jsmastery.pro\u002Fmasterclass\" target=\"_blank\">\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fsujatagunale\u002FEasyRead\u002Fassets\u002F151519281\u002Ffed352ad-f27b-400d-9b8f-c7fe628acb84\" alt=\"Project Banner\">\n\u003C\u002Fa>\n\n#\n","该项目是一个医疗健康管理平台，旨在简化患者注册、预约安排和病历管理。其核心功能包括复杂的表单处理与SMS通知实现，支持用户注册、预约医生及管理个人预约等功能。技术上，项目基于TypeScript开发，使用了Next.js框架确保高效渲染，TailwindCSS提供美观且响应式的界面设计，Appwrite作为后端服务支撑数据存储与认证，Twilio用于发送短信提醒。适用于需要数字化管理和优化医疗服务流程的医疗机构或相关企业，尤其是那些希望提高患者体验和服务效率的场景。",2,"2026-06-11 03:47:31","high_star"]