Skip to content

Commit 627ee98

Browse files
committed
patient: profile
1 parent 6f5591d commit 627ee98

8 files changed

Lines changed: 396 additions & 294 deletions

File tree

frontend/src/app/(patient)/login/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { useRouter } from 'next/navigation';
77
import { Eye, EyeOff, User } from 'lucide-react';
88
import Link from 'next/link';
99
import Image from 'next/image';
10+
import { promise } from 'zod';
11+
import { resolve } from 'path';
1012

1113
export default function LoginPage() {
1214
const { loginSuccess } = useAuth();
@@ -26,6 +28,7 @@ export default function LoginPage() {
2628
try {
2729
const data = await loginPatient(email, password);
2830
loginSuccess(data);
31+
await new Promise(resolve => setTimeout(resolve,50));
2932
router.push('/dashboard');
3033
} catch {
3134
setError('Invalid email or password');

frontend/src/auth/auth.provider.tsx

Lines changed: 117 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import {
88
useState,
99
ReactNode,
1010
} from 'react';
11+
1112
import { Patient } from './auth.types';
12-
import { refreshPatient } from './auth.service';
13+
import { refreshPatient, logoutPatient } from './auth.service';
1314
import { api } from '../lib/api';
14-
import { logoutPatient } from './auth.service';
15+
16+
import { useQueryClient } from '@tanstack/react-query';
17+
import { getProfile } from '@/src/features/patient/api/getProfile';
1518

1619
type AuthState = {
1720
accessToken: string | null;
@@ -24,14 +27,16 @@ type AuthContextType = {
2427
loginSuccess: (data: {
2528
accessToken: string;
2629
patient: Patient;
27-
2830
}) => void;
2931
logout: () => void;
3032
};
3133

3234
const AuthContext = createContext<AuthContextType | null>(null);
3335

3436
export function AuthProvider({ children }: { children: ReactNode }) {
37+
38+
const queryClient = useQueryClient();
39+
3540
const [auth, setAuth] = useState<AuthState>({
3641
accessToken: null,
3742
patient: null,
@@ -42,12 +47,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
4247
const refreshingRef = useRef<Promise<string> | null>(null);
4348
const interceptorsInitialized = useRef(false);
4449

45-
// Keep token ref in sync
50+
// Keep token ref synced
4651
useEffect(() => {
4752
accessTokenRef.current = auth.accessToken;
4853
}, [auth.accessToken]);
4954

50-
// Setup Axios interceptors once
55+
// Axios interceptors
5156
useEffect(() => {
5257
if (interceptorsInitialized.current) return;
5358
interceptorsInitialized.current = true;
@@ -62,6 +67,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
6267
const resId = api.interceptors.response.use(
6368
res => res,
6469
async error => {
70+
6571
const original = error.config;
6672

6773
if (
@@ -77,123 +83,171 @@ export function AuthProvider({ children }: { children: ReactNode }) {
7783
original._retry = true;
7884

7985
try {
86+
8087
if (!refreshingRef.current) {
81-
refreshingRef.current = refreshPatient().then(res => {
82-
setAuth(prev => ({
83-
...prev,
84-
accessToken: res.accessToken,
85-
}));
86-
refreshingRef.current = null;
87-
return res.accessToken;
88-
});
88+
89+
refreshingRef.current = refreshPatient().then(res => {
90+
91+
api.defaults.headers.common['Authorization'] = `Bearer ${res.accessToken}`;
92+
93+
setAuth(prev => ({
94+
...prev,
95+
accessToken: res.accessToken
96+
}));
97+
98+
refreshingRef.current = null;
99+
100+
return res.accessToken;
101+
});
102+
89103
}
90104

91105
const newToken = await refreshingRef.current;
106+
92107
original.headers.Authorization = `Bearer ${newToken}`;
108+
93109
return api(original);
110+
94111
} catch {
112+
95113
refreshingRef.current = null;
114+
96115
setAuth({
97116
accessToken: null,
98117
patient: null,
99118
isRestoring: false,
100119
});
120+
101121
return Promise.reject(error);
122+
102123
}
124+
103125
}
104126
);
105127

106128
return () => {
107129
api.interceptors.request.eject(reqId);
108130
api.interceptors.response.eject(resId);
109131
};
132+
110133
}, []);
111134

112-
// 🔁 Restore session on page load
113-
// 🔁 Restore session ONLY if no accessToken
114-
// 🔁 Restore session ONCE on mount
115-
useEffect(() => {
116-
let cancelled = false;
117-
118-
(async () => {
119-
// 🔥 IMPORTANT FIX
120-
if (accessTokenRef.current) {
121-
setAuth(prev => ({
122-
...prev,
123-
isRestoring: false,
124-
}));
125-
return;
126-
}
135+
/*
136+
Restore session on page load
137+
*/
127138

128-
try {
129-
const refreshRes = await refreshPatient();
139+
useEffect(() => {
140+
141+
let cancelled = false;
142+
143+
(async () => {
144+
145+
if (accessTokenRef.current) {
146+
setAuth(prev => ({
147+
...prev,
148+
isRestoring: false
149+
}));
150+
return;
151+
}
152+
153+
try {
130154

131-
accessTokenRef.current = refreshRes.accessToken;
155+
const refreshRes = await refreshPatient();
132156

133-
const profileRes = await api.get('/patients/public/auth/me');
157+
accessTokenRef.current = refreshRes.accessToken;
134158

135-
if (cancelled) return;
159+
const profileRes = await api.get('/patients/public/auth/me');
136160

137-
setAuth({
138-
accessToken: refreshRes.accessToken,
139-
patient: profileRes.data,
140-
isRestoring: false,
141-
});
142-
} catch {
143-
if (cancelled) return;
161+
if (cancelled) return;
144162

145-
setAuth({
146-
accessToken: null,
147-
patient: null,
148-
isRestoring: false,
149-
});
150-
}
151-
})();
163+
setAuth({
164+
accessToken: refreshRes.accessToken,
165+
patient: profileRes.data,
166+
isRestoring: false
167+
});
152168

153-
return () => {
154-
cancelled = true;
155-
};
156-
}, []);
169+
} catch {
157170

171+
if (cancelled) return;
158172

173+
setAuth({
174+
accessToken: null,
175+
patient: null,
176+
isRestoring: false
177+
});
178+
179+
}
180+
181+
})();
182+
183+
return () => {
184+
cancelled = true;
185+
};
186+
187+
}, []);
188+
189+
/*
190+
Login success handler
191+
*/
159192

160193
function loginSuccess(data: {
161194
accessToken: string;
162195
patient: Patient;
163196
}) {
164-
accessTokenRef.current = data.accessToken; // 🔥 ADD THIS LINE
197+
198+
accessTokenRef.current = data.accessToken;
199+
200+
// 🔥 Ensure axios always has the token immediately
201+
api.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
165202

166203
setAuth({
167204
accessToken: data.accessToken,
168205
patient: data.patient,
169-
isRestoring: false,
206+
isRestoring: false
170207
});
171-
}
172208

173-
async function logout() {
174-
try {
175-
await logoutPatient();
176-
} catch {}
209+
queryClient.invalidateQueries({
210+
queryKey: ['patient-profile']
211+
});
177212

178-
setAuth({
179-
accessToken: null,
180-
patient: null,
181-
isRestoring: false,
213+
queryClient.prefetchQuery({
214+
queryKey: ['patient-profile'],
215+
queryFn: getProfile
182216
});
183217
}
184218

219+
async function logout() {
220+
221+
try {
222+
await logoutPatient();
223+
} catch {}
224+
225+
setAuth({
226+
accessToken: null,
227+
patient: null,
228+
isRestoring: false
229+
});
230+
231+
queryClient.clear();
232+
233+
}
185234

186235
return (
187236
<AuthContext.Provider value={{ auth, loginSuccess, logout }}>
188237
{children}
189238
</AuthContext.Provider>
190239
);
240+
191241
}
192242

193243
export function useAuth() {
244+
194245
const ctx = useContext(AuthContext);
246+
195247
if (!ctx) {
196248
throw new Error('useAuth must be used inside AuthProvider');
197249
}
250+
198251
return ctx;
199-
}
252+
253+
}
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
'use client';
22

33
import AppointmentCard from './AppointmentCard';
4+
import { CalendarX } from 'lucide-react';
45

56
interface Props {
67
appointments: any[];
78
}
89

910
export default function AppointmentList({ appointments }: Props) {
11+
// Enhanced Empty State
1012
if (!appointments.length) {
11-
return <p>No appointments found.</p>;
13+
return (
14+
<div className="flex flex-col items-center justify-center py-20 px-4 bg-slate-50/50 rounded-[2.5rem] border-2 border-dashed border-slate-200">
15+
<div className="h-16 w-16 bg-white rounded-2xl shadow-sm flex items-center justify-center text-slate-300 mb-4">
16+
<CalendarX size={32} />
17+
</div>
18+
<h3 className="text-lg font-bold text-slate-900">No appointments found</h3>
19+
<p className="text-slate-500 text-sm mt-1 text-center max-w-[250px]">
20+
We couldn't find any scheduled visits in your history.
21+
</p>
22+
</div>
23+
);
1224
}
1325

1426
return (
15-
<div className="space-y-4">
16-
{appointments.map((appt) => (
17-
<AppointmentCard
18-
key={appt.id}
19-
appointment={appt}
20-
/>
21-
))}
27+
<div className="relative">
28+
{/* Optional: Simple timeline line decoration for desktop */}
29+
<div className="hidden lg:block absolute left-[40px] top-0 bottom-0 w-[2px] bg-slate-100 -z-10" />
30+
31+
<div className="space-y-6">
32+
{appointments.map((appt) => (
33+
<AppointmentCard
34+
key={appt.id}
35+
appointment={appt}
36+
/>
37+
))}
38+
</div>
2239
</div>
2340
);
2441
}

0 commit comments

Comments
 (0)