feat(profile): display dynamic user data from context

The previous profile page was a static placeholder using hardcoded data. This commit refactors the entire component to fetch and display real user information from the `useUser` context, making the page functional.

Key changes include:
- Replaced static `useState` with the `useUser` context hook for data fetching.
- Completely overhauled the UI for a cleaner, more organized, and responsive layout.
- Displays dynamic user details such as full name, email, phone number, and join date.
- Implemented a `Status` component to visually indicate email and phone verification status.
- Added an error fallback for the profile image, showing a default user icon if the image fails to load.
- Removed the previous local state management and image upload logic.
This commit is contained in:
AmirReza Jamali
2025-09-30 17:31:02 +03:30
parent 1ddc1ce0e6
commit 53db465a24
3 changed files with 124 additions and 153 deletions
+104 -127
View File
@@ -1,148 +1,125 @@
"use client"; "use client";
import { CheckIcon, CrossIcon, UserIcon } from "@/assets/icons";
import Status from "@/components/ui/Status";
import { useUser } from "@/contexts";
import { formatPhoneNumber, unixToDate } from "@/utils/shared";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import { CameraIcon } from "./_components/icons";
import { SocialAccounts } from "./_components/social-accounts";
export default function Page() { export default function Page() {
const [data, setData] = useState({ const { user: data } = useUser();
name: "Danish Heilium", const [imageError, setImageError] = useState(false);
profilePhoto: "/images/user/user-03.png",
coverPhoto: "/images/cover/cover-01.png",
});
const handleChange = (e: any) => { const hasProfileImage = data?.profile_image && !imageError;
if (e.target.name === "profilePhoto") {
const file = e.target?.files[0];
setData({
...data,
profilePhoto: file && URL.createObjectURL(file),
});
} else if (e.target.name === "coverPhoto") {
const file = e.target?.files[0];
setData({
...data,
coverPhoto: file && URL.createObjectURL(file),
});
} else {
setData({
...data,
[e.target.name]: e.target.value,
});
}
};
return ( return (
<div className="mx-auto w-full max-w-[970px]"> <div className="mx-auto w-full max-w-4xl rounded-lg bg-white p-6 shadow-default dark:bg-gray-dark">
<div className="overflow-hidden rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="relative h-48 w-full rounded-t-lg bg-gray-200 dark:bg-gray-700">
<div className="relative z-20 h-35 md:h-65"> <Image
<Image src={"/images/cover/cover-01.png"}
src={data?.coverPhoto} alt="Cover"
alt="profile cover" layout="fill"
className="h-full w-full rounded-tl-[10px] rounded-tr-[10px] object-cover object-center" objectFit="cover"
width={970} className="rounded-t-lg"
height={260} />
style={{ <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 transform">
width: "auto", <div className="relative flex h-32 w-32 items-center justify-center overflow-hidden rounded-full border-4 border-white bg-gray-200 dark:border-gray-dark dark:bg-gray-700">
height: "auto", {hasProfileImage ? (
}} <Image
/> onError={() => setImageError(true)}
<div className="absolute bottom-1 right-1 z-10 xsm:bottom-4 xsm:right-4"> alt="Profile"
<label src={data.profile_image!}
htmlFor="cover" layout="fill"
className="flex cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-[15px] py-[5px] text-body-sm font-medium text-white hover:bg-opacity-90" objectFit="cover"
> className="rounded-full"
<input
type="file"
name="coverPhoto"
id="coverPhoto"
className="sr-only"
onChange={handleChange}
accept="image/png, image/jpg, image/jpeg"
/> />
) : (
<CameraIcon /> <UserIcon className="h-16 w-16 text-gray-400 dark:text-gray-500" />
)}
<span>Edit</span>
</label>
</div> </div>
</div> </div>
<div className="px-4 pb-6 text-center lg:pb-8 xl:pb-11.5"> </div>
<div className="relative z-30 mx-auto -mt-22 h-30 w-full max-w-30 rounded-full bg-white/20 p-1 backdrop-blur sm:h-44 sm:max-w-[176px] sm:p-3">
<div className="relative drop-shadow-2">
{data?.profilePhoto && (
<>
<Image
src={data?.profilePhoto}
width={160}
height={160}
className="overflow-hidden rounded-full"
alt="profile"
/>
<label <div className="mt-16 text-center">
htmlFor="profilePhoto" <h1 className="text-2xl font-bold text-dark dark:text-white">
className="absolute bottom-0 right-0 flex size-8.5 cursor-pointer items-center justify-center rounded-full bg-primary text-white hover:bg-opacity-90 sm:bottom-2 sm:right-2" {data?.full_name}
> </h1>
<CameraIcon /> <p className="text-sm text-gray-500 dark:text-gray-400">
@{data?.username}
</p>
</div>
<input <div className="mt-6 border-t border-stroke pt-6 dark:border-stroke-dark">
type="file" <h2 className="mb-4 text-lg font-semibold text-dark dark:text-white">
name="profilePhoto" User Information
id="profilePhoto" </h2>
className="sr-only" <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
onChange={handleChange} <div>
accept="image/png, image/jpg, image/jpeg" <p className="font-medium text-dark dark:text-white">Email</p>
/> <p className="text-gray-600 dark:text-gray-300">{data?.email}</p>
</label> </div>
</> <div>
)} <p className="font-medium text-dark dark:text-white">Phone</p>
<p className="text-gray-600 dark:text-gray-300">
{formatPhoneNumber(data?.phone)}
</p>
</div>
<div>
<p className="font-medium text-dark dark:text-white">Department</p>
<p className="text-gray-600 dark:text-gray-300">
{data?.department}
</p>
</div>
<div>
<p className="font-medium text-dark dark:text-white">Position</p>
<p className="text-gray-600 dark:text-gray-300">{data?.position}</p>
</div>
<div>
<p className="font-medium text-dark dark:text-white">Role</p>
<p className="capitalize text-gray-600 dark:text-gray-300">
{data?.role}
</p>
</div>
<div>
<p className="font-medium text-dark dark:text-white">Status</p>
<div className="w-fit">
<Status status={data?.status}>{data?.status}</Status>
</div> </div>
</div> </div>
<div className="mt-4"> </div>
<h3 className="mb-1 text-heading-6 font-bold text-dark dark:text-white"> </div>
{data?.name}
</h3>
<p className="font-medium">Ui/Ux Designer</p>
<div className="mx-auto mb-5.5 mt-5 grid max-w-[370px] grid-cols-3 rounded-[5px] border border-stroke py-[9px] shadow-1 dark:border-dark-3 dark:bg-dark-2 dark:shadow-card">
<div className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-dark-3 xsm:flex-row">
<span className="font-medium text-dark dark:text-white">
259
</span>
<span className="text-body-sm">Posts</span>
</div>
<div className="flex flex-col items-center justify-center gap-1 border-r border-stroke px-4 dark:border-dark-3 xsm:flex-row">
<span className="font-medium text-dark dark:text-white">
129K
</span>
<span className="text-body-sm">Followers</span>
</div>
<div className="flex flex-col items-center justify-center gap-1 px-4 xsm:flex-row">
<span className="font-medium text-dark dark:text-white">
2K
</span>
<span className="text-body-sm-sm">Following</span>
</div>
</div>
<div className="mx-auto max-w-[720px]"> <div className="mt-6 border-t border-stroke pt-6 dark:border-stroke-dark">
<h4 className="font-medium text-dark dark:text-white"> <h2 className="mb-4 text-lg font-semibold text-dark dark:text-white">
About Me Account Details
</h4> </h2>
<p className="mt-4"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. <div>
Pellentesque posuere fermentum urna, eu condimentum mauris <p className="font-medium text-dark dark:text-white">Verified</p>
tempus ut. Donec fermentum blandit aliquet. Etiam dictum dapibus <div className="w-fit">
ultricies. Sed vel aliquet libero. Nunc a augue fermentum, <Status status={data?.is_verified ? "success" : "warning"}>
pharetra ligula sed, aliquam lacus. {data?.is_verified ? (
</p> <CheckIcon fill="green" width={20} height={20} />
) : (
<CrossIcon fill="red" width={20} />
)}
</Status>
</div> </div>
</div>
<SocialAccounts /> <div>
<p className="font-medium text-dark dark:text-white">Last Login</p>
<p className="text-gray-600 dark:text-gray-300">
{unixToDate({ unix: data?.last_login_at ?? 0, hasTime: true })}
</p>
</div>
<div>
<p className="font-medium text-dark dark:text-white">
Member Since
</p>
<p className="text-gray-600 dark:text-gray-300">
{unixToDate({ unix: data?.created_at ?? 0, hasTime: true })}
</p>
</div> </div>
</div> </div>
</div> </div>
@@ -16,11 +16,6 @@ import { LogOutIcon, SettingsIcon, UserIcon } from "./icons";
export function UserInfo() { export function UserInfo() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { logout, user } = useUser(); const { logout, user } = useUser();
const USER = {
name: "John Smith",
email: "johnson@nextadmin.com",
img: "/images/user/user-03.png",
};
return ( return (
<Dropdown isOpen={isOpen} setIsOpen={setIsOpen}> <Dropdown isOpen={isOpen} setIsOpen={setIsOpen}>
@@ -31,14 +26,7 @@ export function UserInfo() {
<div className="rounded-full bg-gray-3 p-3.5 dark:bg-dark-3"> <div className="rounded-full bg-gray-3 p-3.5 dark:bg-dark-3">
<UserIcon /> <UserIcon />
</div> </div>
{/* <Image
src={USER.img}
className="size-12"
alt={`Avatar of ${USER.name}`}
role="presentation"
width={200}
height={200}
/> */}
<figcaption className="flex items-center gap-1 font-medium text-dark dark:text-dark-6 max-[1024px]:sr-only"> <figcaption className="flex items-center gap-1 font-medium text-dark dark:text-dark-6 max-[1024px]:sr-only">
<span>{user?.full_name}</span> <span>{user?.full_name}</span>
@@ -61,21 +49,27 @@ export function UserInfo() {
<h2 className="sr-only">User information</h2> <h2 className="sr-only">User information</h2>
<figure className="flex items-center gap-2.5 px-5 py-3.5"> <figure className="flex items-center gap-2.5 px-5 py-3.5">
<Image {user?.profile_image ? (
src={USER.img} <Image
className="size-12" src={user.profile_image}
alt={`Avatar for ${USER.name}`} className="size-12"
role="presentation" alt={`Avatar for ${user.full_name}`}
width={200} role="presentation"
height={200} width={200}
/> height={200}
/>
) : (
<div className="rounded-full bg-gray-3 p-4 dark:bg-gray-7">
<UserIcon width={30} height={30} />
</div>
)}
<figcaption className="space-y-1 text-base font-medium"> <figcaption className="space-y-1 text-base font-medium">
<div className="mb-2 leading-none text-dark dark:text-white"> <div className="mb-2 leading-none text-dark dark:text-white">
{USER.name} {user?.full_name}
</div> </div>
<div className="leading-none text-gray-6">{USER.email}</div> <div className="leading-none text-gray-6">{user?.email}</div>
</figcaption> </figcaption>
</figure> </figure>
+3 -3
View File
@@ -2,11 +2,11 @@ import { cn } from "@/lib/utils";
import { ReactNode } from "react"; import { ReactNode } from "react";
interface IProps { interface IProps {
status: string; status?: string;
children: ReactNode; children: ReactNode;
} }
const Status = ({ status, children }: IProps) => { const Status = ({ status = "draft", children }: IProps) => {
const reds = [ const reds = [
"cancelled", "cancelled",
"inactive", "inactive",
@@ -22,7 +22,7 @@ const Status = ({ status, children }: IProps) => {
"success", "success",
"verified", "verified",
"published", "published",
"sent" "sent",
]; ];
const blues = ["awarded", "processing", "in-review", "scheduled"]; const blues = ["awarded", "processing", "in-review", "scheduled"];
const yellows = ["pending", "warning", "on-hold", "delayed"]; const yellows = ["pending", "warning", "on-hold", "delayed"];