From 1f4c828b307210a57e8d5d1679927ed633bf570d Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:13:43 -0500 Subject: [PATCH 1/8] accounts page --- package.json | 2 + src/app/(web)/account/page.jsx | 157 ++++++++++++++++++++++++++++++++ src/app/api/layout.tsx | 16 ++++ src/app/api/patients.js | 9 ++ src/app/api/patients/[email].js | 21 +++++ src/app/api/user.js | 29 ++++++ src/app/api/webhook/route.js | 35 +------ src/components/ui/card.tsx | 76 ++++++++++++++++ src/components/ui/label.tsx | 26 ++++++ src/lib/utils.ts | 48 +++++++++- src/models/User.js | 5 + tsconfig.json | 2 +- 12 files changed, 388 insertions(+), 38 deletions(-) create mode 100644 src/app/(web)/account/page.jsx create mode 100644 src/app/api/layout.tsx create mode 100644 src/app/api/patients.js create mode 100644 src/app/api/patients/[email].js create mode 100644 src/app/api/user.js create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/label.tsx diff --git a/package.json b/package.json index c646905..a7fd19e 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "@clerk/nextjs": "^6.10.2", "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dropdown-menu": "^2.1.5", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-navigation-menu": "^1.2.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.7", "@vercel/analytics": "^1.4.1", "@vercel/speed-insights": "^1.1.0", + "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "hoyahax-2025": "file:", diff --git a/src/app/(web)/account/page.jsx b/src/app/(web)/account/page.jsx new file mode 100644 index 0000000..7a6711f --- /dev/null +++ b/src/app/(web)/account/page.jsx @@ -0,0 +1,157 @@ +"use client"; +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Button } from '../../../components/ui/button'; +import { Input } from '../../../components/ui/input'; +import { Label } from '../../../components/ui/label'; +import { Card, CardHeader, CardContent, CardFooter } from '../../../components/ui/card'; + +const AccountPage = () => { + const [user, setUser] = useState(null); + const [patients, setPatients] = useState([]); + const [selectedPatient, setSelectedPatient] = useState(null); + const [medications, setMedications] = useState([]); + const [diagnoses, setDiagnoses] = useState([]); + + useEffect(() => { + axios.get('/api/user').then(response => { + setUser(response.data); + if (response.data.role === 'caregiver') { + axios.get('/api/patients').then(res => setPatients(res.data)); + } + }); + }, []); + + const handleRoleChange = async () => { + const newRole = user.role === 'patient' ? 'caregiver' : 'patient'; + await axios.put('/api/user', { role: newRole }); + setUser({ ...user, role: newRole }); + if (newRole === 'caregiver') { + axios.get('/api/patients').then(res => setPatients(res.data)); + } else { + setPatients([]); + setSelectedPatient(null); + } + }; + + const handlePatientSelect = (patient) => { + setSelectedPatient(patient); + setMedications(patient.medications); + setDiagnoses(patient.medicalConditions); + }; + + const handleMedicationsChange = (index, field, value) => { + const updatedMedications = [...medications]; + updatedMedications[index][field] = value; + setMedications(updatedMedications); + }; + + const handleDiagnosesChange = (e) => { + setDiagnoses(e.target.value.split(',')); + }; + + const handleSave = async () => { + await axios.put(`/api/patients/${selectedPatient.email}`, { + medications, + medicalConditions: diagnoses, + }); + alert('Patient data updated successfully'); + }; + + if (!user) return
Loading...
; + + return ( +
+ + +

Account Page

+
+ +
+ +

{user.name}

+
+
+ +

{user.email}

+
+
+ +

{user.role}

+
+ + + {user.role === 'caregiver' && ( +
+

Patients

+
    + {patients.map(patient => ( +
  • handlePatientSelect(patient)} + className="cursor-pointer hover:bg-gray-200 p-2" + > + {patient.name} +
  • + ))} +
+ + {selectedPatient && ( + + +

Edit Patient: {selectedPatient.name}

+
+ +
+ + {medications.map((medication, index) => ( +
+ handleMedicationsChange(index, 'name', e.target.value)} + className="mb-2" + /> + handleMedicationsChange(index, 'dosage', e.target.value)} + className="mb-2" + /> + handleMedicationsChange(index, 'frequency', e.target.value)} + className="mb-2" + /> +
+ ))} +
+
+ + +
+
+ + + +
+ )} +
+ )} +
+
+
+ ); +}; + +export default AccountPage; \ No newline at end of file diff --git a/src/app/api/layout.tsx b/src/app/api/layout.tsx new file mode 100644 index 0000000..a14e64f --- /dev/null +++ b/src/app/api/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/src/app/api/patients.js b/src/app/api/patients.js new file mode 100644 index 0000000..d8fd75b --- /dev/null +++ b/src/app/api/patients.js @@ -0,0 +1,9 @@ +import User from '../../models/User'; +import { connectDB } from '../../lib/utils'; + +export default async (req, res) => { + await connectDB(); + + const patients = await User.find({ role: 'patient' }); + res.json(patients); +}; \ No newline at end of file diff --git a/src/app/api/patients/[email].js b/src/app/api/patients/[email].js new file mode 100644 index 0000000..4e97c58 --- /dev/null +++ b/src/app/api/patients/[email].js @@ -0,0 +1,21 @@ +import User from '../../../models/User'; +import connectDB from '../../../lib/utils'; + +export default async (req, res) => { + await connectDB(); + + const { email } = req.query; + const { medications, medicalConditions } = req.body; + + const patient = await User.findOne({ email }); + + if (!patient) { + return res.status(404).json({ message: 'Patient not found' }); + } + + patient.medications = medications; + patient.medicalConditions = medicalConditions; + await patient.save(); + + res.json(patient); +}; \ No newline at end of file diff --git a/src/app/api/user.js b/src/app/api/user.js new file mode 100644 index 0000000..03f6bb1 --- /dev/null +++ b/src/app/api/user.js @@ -0,0 +1,29 @@ +import { getAuth } from '@clerk/nextjs/server'; +import User from '../../models/User'; +import { connectDB } from '../../lib/utils'; + +export default async (req, res) => { + await connectDB(); + const { userId } = getAuth(req); + + if (!userId) { + return res.status(401).json({ message: 'Unauthorized' }); + } + + const user = await User.findOne({ id: userId }); + + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + if (req.method === 'GET') { + res.json(user); + } else if (req.method === 'PUT') { + const { role } = req.body; + user.role = role; + await user.save(); + res.json(user); + } else { + res.status(405).json({ message: 'Method not allowed' }); + } +}; \ No newline at end of file diff --git a/src/app/api/webhook/route.js b/src/app/api/webhook/route.js index 3e00237..f059fad 100644 --- a/src/app/api/webhook/route.js +++ b/src/app/api/webhook/route.js @@ -1,42 +1,10 @@ import { User } from '../../../models/User'; import { NextResponse } from 'next/server'; import { Webhook } from 'svix'; -import mongoose from "mongoose"; +import { connectDB } from '../../../lib/utils'; const CLERK_WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; -const DATABASE_URL = process.env.MONGO_URI; - -let cached = global.mongoose; - -if (!cached) { - cached = global.mongoose = { conn: null, promise: null }; -} -async function connectDB() { - if (cached.conn) { - return cached.conn; - } - - if (!cached.promise) { - const opts = { - bufferCommands: false, - }; - - try { - cached.promise = mongoose.connect(DATABASE_URL, opts).then((mongoose) => { - console.log('MongoDB connected successfully'); - return mongoose; - }); - } catch (error) { - console.error('Error connecting to MongoDB:', error.message); - throw new Error('Error connecting to MongoDB'); - } - } - - cached.conn = await cached.promise; - return cached.conn; -} - export async function POST(req) { console.log('Received request:', req); @@ -106,6 +74,7 @@ export async function POST(req) { } user = new User({ + id, name, email, role: 'patient', diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..710c273 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,46 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; +import mongoose, { Mongoose } from "mongoose"; -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); } + +interface Cached { + conn: Mongoose | null; + promise: Promise | null; +} + +declare global { + var mongoose: Cached; +} + +export async function connectDB(): Promise { + const DATABASE_URL = process.env.MONGO_URI as string; + + let cached: Cached = global.mongoose; + + if (!cached) { + cached = global.mongoose = { conn: null, promise: null }; + } + if (cached.conn) { + return cached.conn; + } + + if (!cached.promise) { + const opts = { + bufferCommands: false, + }; + + cached.promise = mongoose.connect(DATABASE_URL, opts).then((mongoose) => { + console.log('MongoDB connected successfully'); + return mongoose; + }).catch((error) => { + console.error('Error connecting to MongoDB:', error.message); + throw new Error('Error connecting to MongoDB'); + }); + } + + cached.conn = await cached.promise; + return cached.conn; +} \ No newline at end of file diff --git a/src/models/User.js b/src/models/User.js index 5f8a7e9..e5a8cba 100644 --- a/src/models/User.js +++ b/src/models/User.js @@ -2,6 +2,11 @@ import { Schema, model, models } from "mongoose"; const UserSchema = new Schema( { + id: { + type: String, + required: true, + unique: true, + }, name: { type: String, required: true, diff --git a/tsconfig.json b/tsconfig.json index 279409d..b7a29c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/api/connectDB.js"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/api/connectDB.js", "src/lib/utils.js", "src/app/(web)/account/page.jsx"], "exclude": ["node_modules"] } From 7d79f06ca8721908052f34d9b2482f06fc8b2d86 Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:18:58 -0500 Subject: [PATCH 2/8] follow no-var eslint --- src/lib/utils.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 710c273..141b9ac 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -12,17 +12,14 @@ interface Cached { } declare global { - var mongoose: Cached; + var mongoose: Cached | undefined; } +let cached: Cached = global.mongoose || { conn: null, promise: null }; + export async function connectDB(): Promise { const DATABASE_URL = process.env.MONGO_URI as string; - let cached: Cached = global.mongoose; - - if (!cached) { - cached = global.mongoose = { conn: null, promise: null }; - } if (cached.conn) { return cached.conn; } @@ -42,5 +39,6 @@ export async function connectDB(): Promise { } cached.conn = await cached.promise; + global.mongoose = cached; return cached.conn; } \ No newline at end of file From 146f19db554332585b5249a287e88886c7ab8170 Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:22:09 -0500 Subject: [PATCH 3/8] switch to js --- src/lib/{utils.ts => utils.js} | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) rename src/lib/{utils.ts => utils.js} (56%) diff --git a/src/lib/utils.ts b/src/lib/utils.js similarity index 56% rename from src/lib/utils.ts rename to src/lib/utils.js index 141b9ac..5ceeb29 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.js @@ -1,24 +1,15 @@ -import { clsx, type ClassValue } from "clsx"; +import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; -import mongoose, { Mongoose } from "mongoose"; +import mongoose from "mongoose"; -export function cn(...inputs: ClassValue[]): string { +export function cn(...inputs) { return twMerge(clsx(inputs)); } -interface Cached { - conn: Mongoose | null; - promise: Promise | null; -} +const cached = global.mongoose || { conn: null, promise: null }; -declare global { - var mongoose: Cached | undefined; -} - -let cached: Cached = global.mongoose || { conn: null, promise: null }; - -export async function connectDB(): Promise { - const DATABASE_URL = process.env.MONGO_URI as string; +export async function connectDB() { + const DATABASE_URL = process.env.MONGO_URI; if (cached.conn) { return cached.conn; From f5735d85e38c8777afd4d914e3108f49aa7ad185 Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:26:08 -0500 Subject: [PATCH 4/8] fix --- src/app/api/webhook/route.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/webhook/route.js b/src/app/api/webhook/route.js index f059fad..2d15e01 100644 --- a/src/app/api/webhook/route.js +++ b/src/app/api/webhook/route.js @@ -49,7 +49,7 @@ export async function POST(req) { const eventType = evt.type; if (eventType === 'user.created') { - const { first_name, last_name, email_addresses } = evt.data; + const { id, first_name, last_name, email_addresses } = evt.data; const email = email_addresses?.[0]?.email_address || null; if (!email) { From fe228f211f451ef0b1ed42b99eee93956638f8c5 Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:32:26 -0500 Subject: [PATCH 5/8] add userid sending --- src/app/(web)/account/page.jsx | 53 +++++++++++++++++----------------- src/app/api/user.js | 5 ++-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/app/(web)/account/page.jsx b/src/app/(web)/account/page.jsx index 7a6711f..2eff506 100644 --- a/src/app/(web)/account/page.jsx +++ b/src/app/(web)/account/page.jsx @@ -1,31 +1,32 @@ -"use client"; +"use client" import { useState, useEffect } from 'react'; import axios from 'axios'; -import { Button } from '../../../components/ui/button'; -import { Input } from '../../../components/ui/input'; -import { Label } from '../../../components/ui/label'; -import { Card, CardHeader, CardContent, CardFooter } from '../../../components/ui/card'; +import { useUser } from '@clerk/clerk-react'; +import { Button, Input, Label, Card, CardHeader, CardBody, CardFooter } from 'components/ui'; const AccountPage = () => { - const [user, setUser] = useState(null); + const { user } = useUser(); + const [userData, setUserData] = useState(null); const [patients, setPatients] = useState([]); const [selectedPatient, setSelectedPatient] = useState(null); const [medications, setMedications] = useState([]); const [diagnoses, setDiagnoses] = useState([]); useEffect(() => { - axios.get('/api/user').then(response => { - setUser(response.data); - if (response.data.role === 'caregiver') { - axios.get('/api/patients').then(res => setPatients(res.data)); - } - }); - }, []); + if (user) { + axios.get(`/api/user?userId=${user.id}`).then(response => { + setUserData(response.data); + if (response.data.role === 'caregiver') { + axios.get('/api/patients').then(res => setPatients(res.data)); + } + }); + } + }, [user]); const handleRoleChange = async () => { - const newRole = user.role === 'patient' ? 'caregiver' : 'patient'; - await axios.put('/api/user', { role: newRole }); - setUser({ ...user, role: newRole }); + const newRole = userData.role === 'patient' ? 'caregiver' : 'patient'; + await axios.put(`/api/user?userId=${user.id}`, { role: newRole }); + setUserData({ ...userData, role: newRole }); if (newRole === 'caregiver') { axios.get('/api/patients').then(res => setPatients(res.data)); } else { @@ -58,7 +59,7 @@ const AccountPage = () => { alert('Patient data updated successfully'); }; - if (!user) return
Loading...
; + if (!userData) return
Loading...
; return (
@@ -66,24 +67,24 @@ const AccountPage = () => {

Account Page

- +
-

{user.name}

+

{userData.name}

-

{user.email}

+

{userData.email}

-

{user.role}

+

{userData.role}

- {user.role === 'caregiver' && ( + {userData.role === 'caregiver' && (

Patients

    @@ -103,7 +104,7 @@ const AccountPage = () => {

    Edit Patient: {selectedPatient.name}

    - +
    {medications.map((medication, index) => ( @@ -140,7 +141,7 @@ const AccountPage = () => { onChange={handleDiagnosesChange} />
    -
    + @@ -148,7 +149,7 @@ const AccountPage = () => { )}
)} -
+
); diff --git a/src/app/api/user.js b/src/app/api/user.js index 03f6bb1..025c4d0 100644 --- a/src/app/api/user.js +++ b/src/app/api/user.js @@ -1,16 +1,15 @@ -import { getAuth } from '@clerk/nextjs/server'; import User from '../../models/User'; import { connectDB } from '../../lib/utils'; export default async (req, res) => { await connectDB(); - const { userId } = getAuth(req); + const { userId } = req.query; if (!userId) { return res.status(401).json({ message: 'Unauthorized' }); } - const user = await User.findOne({ id: userId }); + const user = await User.findOne({ clerkId: userId }); if (!user) { return res.status(404).json({ message: 'User not found' }); From bd6f4e3cfff13e869160ed15b3eb92bebeb025d0 Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:33:47 -0500 Subject: [PATCH 6/8] fix imports --- src/app/(web)/account/page.jsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/(web)/account/page.jsx b/src/app/(web)/account/page.jsx index 2eff506..c3371be 100644 --- a/src/app/(web)/account/page.jsx +++ b/src/app/(web)/account/page.jsx @@ -2,7 +2,11 @@ import { useState, useEffect } from 'react'; import axios from 'axios'; import { useUser } from '@clerk/clerk-react'; -import { Button, Input, Label, Card, CardHeader, CardBody, CardFooter } from 'components/ui'; +import { Button } from '../../../components/ui/button'; +import { Input } from '../../../components/ui/input'; +import { Label } from '../../../components/ui/label'; +import { Card, CardHeader, CardContent, CardFooter } from '../../../components/ui/card'; + const AccountPage = () => { const { user } = useUser(); @@ -67,7 +71,7 @@ const AccountPage = () => {

Account Page

- +

{userData.name}

@@ -104,7 +108,7 @@ const AccountPage = () => {

Edit Patient: {selectedPatient.name}

- +
{medications.map((medication, index) => ( @@ -141,7 +145,7 @@ const AccountPage = () => { onChange={handleDiagnosesChange} />
-
+ @@ -149,7 +153,7 @@ const AccountPage = () => { )}
)} -
+
); From e794460b94708628efa287a53db7da14f8df03af Mon Sep 17 00:00:00 2001 From: Joseph J Helfenbein Date: Sat, 25 Jan 2025 06:38:32 -0500 Subject: [PATCH 7/8] add clerk provider to layout --- src/app/(web)/layout.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/(web)/layout.tsx b/src/app/(web)/layout.tsx index e69d4da..da9e34c 100644 --- a/src/app/(web)/layout.tsx +++ b/src/app/(web)/layout.tsx @@ -1,5 +1,6 @@ "use client" +import { ClerkProvider } from '@clerk/nextjs'; import { Navbar } from '@/components/navbar'; import { Footer } from '@/components/footer'; import { ThemeProvider } from '@/components/theme-provider'; @@ -14,11 +15,13 @@ export default function RootLayout({ return ( - - -
{children}
-