Skip to content

Commit a913257

Browse files
authored
Merge pull request #570 from RUKAYAT-CODER/feat/fe-12-user-profile-page
[FE-12] Build UserProfilePage
2 parents 8890229 + d38b0e9 commit a913257

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"use client";
2+
3+
import { useEffect, useState, useCallback } from "react";
4+
import Link from "next/link";
5+
import { CheckCircle, XCircle, ShieldCheck, ShieldOff } from "lucide-react";
6+
7+
interface UserProfile {
8+
fullName: string;
9+
email: string;
10+
role: string;
11+
isVerified: boolean;
12+
preferredLanguage: string;
13+
twoFactorEnabled: boolean;
14+
createdAt: string;
15+
}
16+
17+
function SkeletonField() {
18+
return (
19+
<div className="animate-pulse space-y-1">
20+
<div className="h-3 w-20 rounded bg-gray-200" />
21+
<div className="h-5 w-40 rounded bg-gray-200" />
22+
</div>
23+
);
24+
}
25+
26+
export default function UserProfilePage() {
27+
const [profile, setProfile] = useState<UserProfile | null>(null);
28+
const [loading, setLoading] = useState(true);
29+
const [resending, setResending] = useState(false);
30+
const [resendMsg, setResendMsg] = useState<string | null>(null);
31+
32+
const fetchProfile = useCallback(async () => {
33+
setLoading(true);
34+
try {
35+
const res = await fetch(
36+
`${process.env.NEXT_PUBLIC_API_URL}/api/module/users/me`,
37+
{
38+
headers: {
39+
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
40+
},
41+
}
42+
);
43+
if (!res.ok) throw new Error();
44+
setProfile(await res.json());
45+
} finally {
46+
setLoading(false);
47+
}
48+
}, []);
49+
50+
useEffect(() => {
51+
fetchProfile();
52+
}, [fetchProfile]);
53+
54+
async function handleResend() {
55+
setResending(true);
56+
setResendMsg(null);
57+
try {
58+
await fetch(
59+
`${process.env.NEXT_PUBLIC_API_URL}/api/auth/resend-verification`,
60+
{
61+
method: "POST",
62+
headers: {
63+
Authorization: `Bearer ${localStorage.getItem("access_token")}`,
64+
},
65+
}
66+
);
67+
setResendMsg("Verification email sent.");
68+
} catch {
69+
setResendMsg("Failed to send. Please try again.");
70+
} finally {
71+
setResending(false);
72+
}
73+
}
74+
75+
return (
76+
<div className="mx-auto max-w-2xl space-y-6 p-6">
77+
<div className="flex items-center justify-between">
78+
<h1 className="text-2xl font-bold text-gray-900">Profile</h1>
79+
{!loading && (
80+
<Link
81+
href="/profile/edit"
82+
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700"
83+
>
84+
Edit Profile
85+
</Link>
86+
)}
87+
</div>
88+
89+
{!loading && profile && !profile.isVerified && (
90+
<div className="flex items-center justify-between rounded-xl border border-yellow-200 bg-yellow-50 px-4 py-3">
91+
<p className="text-sm text-yellow-800">
92+
Your email is not verified. Please check your inbox.
93+
</p>
94+
<button
95+
onClick={handleResend}
96+
disabled={resending}
97+
className="ml-4 text-sm font-semibold text-yellow-800 underline disabled:opacity-60"
98+
>
99+
{resending ? "Sending…" : "Resend"}
100+
</button>
101+
{resendMsg && <span className="ml-2 text-xs text-yellow-700">{resendMsg}</span>}
102+
</div>
103+
)}
104+
105+
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
106+
<dl className="grid grid-cols-1 gap-5 sm:grid-cols-2">
107+
{loading ? (
108+
Array.from({ length: 6 }).map((_, i) => <SkeletonField key={i} />)
109+
) : profile ? (
110+
<>
111+
<div>
112+
<dt className="text-xs font-medium uppercase text-gray-400">Full Name</dt>
113+
<dd className="mt-0.5 text-sm text-gray-900">{profile.fullName}</dd>
114+
</div>
115+
<div>
116+
<dt className="text-xs font-medium uppercase text-gray-400">Email</dt>
117+
<dd className="mt-0.5 flex items-center gap-1.5 text-sm text-gray-900">
118+
{profile.email}
119+
{profile.isVerified ? (
120+
<CheckCircle className="h-4 w-4 text-green-500" aria-label="Verified" />
121+
) : (
122+
<XCircle className="h-4 w-4 text-red-400" aria-label="Unverified" />
123+
)}
124+
</dd>
125+
</div>
126+
<div>
127+
<dt className="text-xs font-medium uppercase text-gray-400">Role</dt>
128+
<dd className="mt-0.5 text-sm text-gray-900">{profile.role}</dd>
129+
</div>
130+
<div>
131+
<dt className="text-xs font-medium uppercase text-gray-400">Language</dt>
132+
<dd className="mt-0.5 text-sm text-gray-900">{profile.preferredLanguage}</dd>
133+
</div>
134+
<div>
135+
<dt className="text-xs font-medium uppercase text-gray-400">Member Since</dt>
136+
<dd className="mt-0.5 text-sm text-gray-900">
137+
{new Date(profile.createdAt).toLocaleDateString()}
138+
</dd>
139+
</div>
140+
<div>
141+
<dt className="text-xs font-medium uppercase text-gray-400">Two-Factor Auth</dt>
142+
<dd className="mt-0.5 flex items-center gap-1.5 text-sm">
143+
{profile.twoFactorEnabled ? (
144+
<>
145+
<ShieldCheck className="h-4 w-4 text-green-500" aria-hidden="true" />
146+
<Link href="/settings/2fa" className="text-green-700 hover:underline">
147+
Enabled
148+
</Link>
149+
</>
150+
) : (
151+
<>
152+
<ShieldOff className="h-4 w-4 text-gray-400" aria-hidden="true" />
153+
<Link href="/settings/2fa" className="text-gray-500 hover:underline">
154+
Disabled
155+
</Link>
156+
</>
157+
)}
158+
</dd>
159+
</div>
160+
</>
161+
) : (
162+
<p className="col-span-2 text-sm text-red-600">Failed to load profile.</p>
163+
)}
164+
</dl>
165+
</div>
166+
</div>
167+
);
168+
}

0 commit comments

Comments
 (0)