Skip to content

Commit 6f5591d

Browse files
committed
patient profile
1 parent 457b92b commit 6f5591d

13 files changed

Lines changed: 568 additions & 5 deletions

File tree

frontend/src/app/(patient)/dashboard/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ export default function PatientDashboardLayout({
5252
label="Book Appointment"
5353
active={pathname === '/dashboard/book'}
5454
/>
55+
<SidebarLink
56+
href="/dashboard/profile"
57+
label="Patient Profile"
58+
active={pathname === '/dashboard/profile'}
59+
/>
60+
5561

5662
</nav>
5763
</div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import ProfileForm from '@/src/features/patient/components/ProfileForm';
4+
5+
export default function ProfilePage() {
6+
7+
return (
8+
9+
<div className="p-6">
10+
11+
<h1 className="text-2xl font-bold mb-6">
12+
13+
My Profile
14+
15+
</h1>
16+
17+
<ProfileForm />
18+
19+
</div>
20+
21+
);
22+
23+
}

frontend/src/auth/auth.provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type AuthContextType = {
2424
loginSuccess: (data: {
2525
accessToken: string;
2626
patient: Patient;
27+
2728
}) => void;
2829
logout: () => void;
2930
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { api } from '@/src/lib/api';
2+
3+
export async function getProfile() {
4+
const res = await api.get('/patients/me');
5+
return res.data;
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { api } from '@/src/lib/api';
2+
3+
export async function updateProfile(data: any) {
4+
const res = await api.patch('/patients/me', data);
5+
return res.data;
6+
}
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { usePatientProfile } from '../hooks/usePatientProfile';
5+
import { useUpdateProfile } from '../hooks/useUpdateProfile';
6+
import {
7+
User,
8+
Activity,
9+
MapPin,
10+
PhoneCall,
11+
Pencil,
12+
Save,
13+
X,
14+
Droplets,
15+
Ruler,
16+
Weight,
17+
Calendar,
18+
ShieldAlert,
19+
Mail,
20+
Phone,
21+
Fingerprint
22+
} from 'lucide-react';
23+
24+
export default function ProfileForm() {
25+
const { data, isLoading } = usePatientProfile();
26+
const updateProfile = useUpdateProfile();
27+
const [isEditing, setIsEditing] = useState(false);
28+
29+
const [form, setForm] = useState({
30+
name: '',
31+
email: '',
32+
phone: '',
33+
dob: '',
34+
blood_group: '',
35+
height_cm: '',
36+
weight_kg: '',
37+
allergies: '',
38+
chronic_conditions: '',
39+
address_line1: '',
40+
city: '',
41+
state: '',
42+
country: '',
43+
pincode: '',
44+
emergency_contact_name: '',
45+
emergency_contact_phone: '',
46+
emergency_contact_relation: ''
47+
});
48+
49+
useEffect(() => {
50+
if (!data) return;
51+
setForm({
52+
name: data.name || '',
53+
email: data.email || '',
54+
phone: data.phone || '',
55+
dob: data.dob || '',
56+
blood_group: data.blood_group || '',
57+
height_cm: data.height_cm || '',
58+
weight_kg: data.weight_kg || '',
59+
allergies: data.allergies || '',
60+
chronic_conditions: data.chronic_conditions || '',
61+
address_line1: data.address_line1 || '',
62+
city: data.city || '',
63+
state: data.state || '',
64+
country: data.country || '',
65+
pincode: data.pincode || '',
66+
emergency_contact_name: data.emergency_contact_name || '',
67+
emergency_contact_phone: data.emergency_contact_phone || '',
68+
emergency_contact_relation: data.emergency_contact_relation || ''
69+
});
70+
}, [data]);
71+
72+
if (isLoading) {
73+
return (
74+
<div className="flex items-center justify-center min-h-100">
75+
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600"></div>
76+
</div>
77+
);
78+
}
79+
80+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
81+
setForm({ ...form, [e.target.name]: e.target.value });
82+
};
83+
84+
const handleSubmit = (e: React.FormEvent) => {
85+
e.preventDefault();
86+
updateProfile.mutate(form, {
87+
onSuccess: () => setIsEditing(false)
88+
});
89+
};
90+
91+
const age = data?.dob ? new Date().getFullYear() - new Date(data.dob).getFullYear() : '—';
92+
93+
return (
94+
<div className="max-w-5xl mx-auto space-y-6 pb-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
95+
96+
{/* HEADER SECTION */}
97+
<div className="relative overflow-hidden bg-white border border-slate-200 rounded-3xl p-8 shadow-sm">
98+
<div className="absolute top-0 right-0 w-48 h-48 bg-blue-50/50 rounded-full -mr-20 -mt-20 blur-3xl" />
99+
100+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
101+
<div className="flex items-center gap-6">
102+
<div className="h-24 w-24 rounded-2xl bg-linear-to-br from-blue-600 to-indigo-700 flex items-center justify-center text-white text-3xl font-bold shadow-xl shadow-blue-100">
103+
{form.name?.charAt(0)}
104+
</div>
105+
<div>
106+
<h1 className="text-3xl font-extrabold text-slate-900 tracking-tight">{form.name}</h1>
107+
<div className="flex flex-wrap gap-3 mt-2">
108+
<span className="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-bold bg-blue-50 text-blue-700 border border-blue-100 uppercase tracking-widest">
109+
Patient ID: {data.id?.slice(-6).toUpperCase() || 'H-OS-092'}
110+
</span>
111+
<span className="text-slate-400 text-sm font-medium flex items-center gap-1">
112+
<Activity size={14} className="text-emerald-500" /> Account Active
113+
</span>
114+
</div>
115+
</div>
116+
</div>
117+
118+
<div className="flex items-center gap-3">
119+
{!isEditing ? (
120+
<button
121+
onClick={() => setIsEditing(true)}
122+
className="flex items-center gap-2 px-6 py-3 bg-slate-900 text-white rounded-2xl font-bold hover:bg-slate-800 transition-all active:scale-95 shadow-lg shadow-slate-200"
123+
>
124+
<Pencil size={18} /> Edit Profile
125+
</button>
126+
) : (
127+
<button
128+
onClick={() => setIsEditing(false)}
129+
className="flex items-center gap-2 px-6 py-3 bg-white border border-slate-200 text-slate-600 rounded-2xl font-bold hover:bg-slate-50 transition-all"
130+
>
131+
<X size={18} /> Cancel
132+
</button>
133+
)}
134+
</div>
135+
</div>
136+
137+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-10 pt-8 border-t border-slate-100">
138+
<StatBox label="Blood Type" value={form.blood_group || 'Unknown'} icon={<Droplets className="text-red-500" size={16} />} />
139+
<StatBox label="Height" value={form.height_cm ? `${form.height_cm} cm` : '—'} icon={<Ruler className="text-blue-500" size={16} />} />
140+
<StatBox label="Weight" value={form.weight_kg ? `${form.weight_kg} kg` : '—'} icon={<Weight className="text-emerald-500" size={16} />} />
141+
<StatBox label="Age" value={`${age} Years`} icon={<User className="text-indigo-500" size={16} />} />
142+
</div>
143+
</div>
144+
145+
<form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-3 gap-6">
146+
147+
{/* LEFT COLUMN */}
148+
<div className="lg:col-span-2 space-y-6">
149+
150+
{/* PERSONAL IDENTITY SECTION */}
151+
<section className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm">
152+
<header className="flex items-center gap-3 mb-8">
153+
<div className="p-2 bg-blue-50 rounded-xl text-blue-600"><Fingerprint size={20}/></div>
154+
<h2 className="text-xl font-bold text-slate-900 tracking-tight">Personal Identity</h2>
155+
</header>
156+
157+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
158+
<div className="md:col-span-2">
159+
<FloatingInput label="Full Name" name="name" value={form.name} onChange={handleChange} disabled={!isEditing} icon={<User size={16}/>} />
160+
</div>
161+
<FloatingInput label="Email Address" name="email" value={form.email} onChange={handleChange} disabled={!isEditing} icon={<Mail size={16}/>} />
162+
<FloatingInput label="Phone Number" name="phone" value={form.phone} onChange={handleChange} disabled={!isEditing} icon={<Phone size={16}/>} />
163+
<div className="md:col-span-2">
164+
<FloatingInput label="Date of Birth" name="dob" type="date" value={form.dob} onChange={handleChange} disabled={!isEditing} icon={<Calendar size={16}/>} />
165+
</div>
166+
</div>
167+
</section>
168+
169+
{/* CLINICAL BACKGROUND */}
170+
<section className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm">
171+
<header className="flex items-center gap-3 mb-8">
172+
<div className="p-2 bg-rose-50 rounded-xl text-rose-600"><ShieldAlert size={20}/></div>
173+
<h2 className="text-xl font-bold text-slate-900 tracking-tight">Clinical Background</h2>
174+
</header>
175+
176+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
177+
<div className="space-y-4">
178+
<label className="text-xs font-bold text-slate-400 uppercase tracking-widest ml-1">Physical Metrics</label>
179+
<div className="grid grid-cols-2 gap-4">
180+
<FloatingInput label="Blood Type" name="blood_group" value={form.blood_group} onChange={handleChange} disabled={!isEditing} />
181+
<FloatingInput label="Height (cm)" name="height_cm" value={form.height_cm} onChange={handleChange} disabled={!isEditing} />
182+
<FloatingInput label="Weight (kg)" name="weight_kg" value={form.weight_kg} onChange={handleChange} disabled={!isEditing} />
183+
</div>
184+
</div>
185+
186+
<div className="space-y-4">
187+
<label className="text-xs font-bold text-slate-400 uppercase tracking-widest ml-1">Medical Notes</label>
188+
<textarea
189+
name="allergies"
190+
value={form.allergies}
191+
onChange={handleChange}
192+
disabled={!isEditing}
193+
placeholder="Record allergies or sensitivities..."
194+
className="w-full bg-slate-50 border-slate-200 rounded-2xl p-4 text-sm font-medium focus:ring-2 focus:ring-blue-500 transition-all min-h-27.5 disabled:bg-slate-50/50 resize-none"
195+
/>
196+
</div>
197+
</div>
198+
</section>
199+
</div>
200+
201+
{/* RIGHT COLUMN */}
202+
<div className="space-y-6">
203+
204+
{/* EMERGENCY SECTION */}
205+
<section className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm">
206+
<header className="flex items-center gap-3 mb-8">
207+
<div className="p-2 bg-amber-50 rounded-xl text-amber-600"><PhoneCall size={20}/></div>
208+
<h2 className="text-xl font-bold text-slate-900 tracking-tight">Emergency Contact</h2>
209+
</header>
210+
211+
<div className="space-y-5">
212+
<FloatingInput label="Contact Name" name="emergency_contact_name" value={form.emergency_contact_name} onChange={handleChange} disabled={!isEditing} />
213+
<FloatingInput label="Emergency Phone" name="emergency_contact_phone" value={form.emergency_contact_phone} onChange={handleChange} disabled={!isEditing} />
214+
<FloatingInput label="Relationship" name="emergency_contact_relation" value={form.emergency_contact_relation} onChange={handleChange} disabled={!isEditing} />
215+
</div>
216+
</section>
217+
218+
{/* ADDRESS SECTION */}
219+
<section className="bg-white border border-slate-200 rounded-3xl p-6 shadow-sm">
220+
<header className="flex items-center gap-3 mb-8">
221+
<div className="p-2 bg-emerald-50 rounded-xl text-emerald-600"><MapPin size={20}/></div>
222+
<h2 className="text-xl font-bold text-slate-900 tracking-tight">Residency</h2>
223+
</header>
224+
225+
<div className="space-y-5">
226+
<FloatingInput label="Street Address" name="address_line1" value={form.address_line1} onChange={handleChange} disabled={!isEditing} />
227+
<div className="grid grid-cols-2 gap-4">
228+
<FloatingInput label="City" name="city" value={form.city} onChange={handleChange} disabled={!isEditing} />
229+
<FloatingInput label="Pincode" name="pincode" value={form.pincode} onChange={handleChange} disabled={!isEditing} />
230+
</div>
231+
</div>
232+
</section>
233+
234+
{isEditing && (
235+
<div className="sticky bottom-6 bg-blue-600 rounded-3xl p-6 text-white shadow-2xl shadow-blue-300 animate-in zoom-in-95 duration-300">
236+
<h3 className="font-bold flex items-center gap-2 mb-2"><Activity size={18}/> Review Updates</h3>
237+
<p className="text-xs text-blue-100 mb-6 font-medium">Please verify the contact and medical data accuracy before saving to the central database.</p>
238+
<button
239+
type="submit"
240+
className="w-full flex items-center justify-center gap-2 bg-white text-blue-600 py-4 rounded-2xl font-black hover:shadow-lg transition-all active:scale-95"
241+
>
242+
<Save size={20} /> SYNC PROFILE
243+
</button>
244+
</div>
245+
)}
246+
</div>
247+
</form>
248+
</div>
249+
);
250+
}
251+
252+
/* HELPER COMPONENTS */
253+
254+
function StatBox({ label, value, icon }: { label: string, value: string, icon: React.ReactNode }) {
255+
return (
256+
<div className="flex items-center gap-4 p-4 rounded-2xl bg-slate-50 border border-slate-100/50">
257+
<div className="p-2.5 bg-white rounded-xl shadow-sm border border-slate-100">{icon}</div>
258+
<div>
259+
<p className="text-[10px] uppercase font-black text-slate-400 tracking-tighter leading-none mb-1">{label}</p>
260+
<p className="text-sm font-bold text-slate-800">{value}</p>
261+
</div>
262+
</div>
263+
);
264+
}
265+
266+
function FloatingInput({ label, name, value, onChange, disabled, type = "text", icon }: any) {
267+
return (
268+
<div className="group relative flex flex-col gap-1.5 w-full">
269+
<div className="flex items-center justify-between px-1">
270+
<label className="text-[10px] font-black text-slate-400 group-focus-within:text-blue-600 uppercase tracking-widest transition-colors">
271+
{label}
272+
</label>
273+
{icon && <span className="text-slate-300 group-focus-within:text-blue-400 transition-colors">{icon}</span>}
274+
</div>
275+
<input
276+
type={type}
277+
name={name}
278+
value={value}
279+
onChange={onChange}
280+
disabled={disabled}
281+
className={`
282+
w-full px-4 py-3.5 bg-slate-50 border border-slate-200 rounded-2xl text-sm font-bold tracking-tight transition-all
283+
focus:ring-4 focus:ring-blue-100 focus:border-blue-500 focus:bg-white outline-none
284+
disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-slate-50/20 disabled:border-slate-100 disabled:font-medium
285+
`}
286+
/>
287+
</div>
288+
);
289+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { getProfile } from '../api/getProfile';
3+
4+
export function usePatientProfile() {
5+
return useQuery({
6+
queryKey: ['patient-profile'],
7+
queryFn: getProfile,
8+
});
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
import { updateProfile } from '../api/updateProfile';
3+
4+
export function useUpdateProfile() {
5+
const queryClient = useQueryClient();
6+
7+
return useMutation({
8+
mutationFn: updateProfile,
9+
10+
onSuccess: () => {
11+
queryClient.invalidateQueries({
12+
queryKey: ['patient-profile'],
13+
});
14+
},
15+
});
16+
}

0 commit comments

Comments
 (0)