feat(userAccount): improve user profile handling with avatar upload and dynamic display name

This commit is contained in:
hamed
2026-06-19 11:05:43 +03:30
parent ea2c0c29ec
commit da05a4e13c
5 changed files with 103 additions and 65 deletions
+12 -2
View File
@@ -1,11 +1,21 @@
"use client";
import { useEffect, useState } from "react";
import CalendarAdD from "@/components/icons/CalendarAdD";
import CardTickD from "@/components/icons/CardTickD";
import { getParsedUserInfo } from "@/helper";
import Image from "next/image";
function Head({ user }) {
const userInfo = getParsedUserInfo() || {};
const displayName = userInfo.realName || user?.name || "کاربر";
// مقدار اولیه از prop سرور تا SSR و کلاینت یکسان باشند (جلوگیری از hydration mismatch)؛
// سپس در کلاینت نام واقعی از کوکی userInfo خوانده می‌شود.
const [displayName, setDisplayName] = useState(user?.name || "کاربر");
useEffect(() => {
const userInfo = getParsedUserInfo();
const name = userInfo?.realName || user?.name;
if (name) setDisplayName(name);
}, [user?.name]);
return (
<div className="hidden md:flex flex-col lg:flex-row w-full items-stretch justify-center gap-[12px] lg:gap-[24px]">
@@ -8,6 +8,7 @@ function Field({
icon,
placeholder,
data,
value,
name,
iconGray,
error,
@@ -16,12 +17,18 @@ function Field({
props,
disable,
}) {
// وقتی value پاس داده شود فیلد controlled است (برای مقادیر فقط‌خواندنی مثل موبایل)؛
// در غیر این صورت رفتار قبلی (uncontrolled با defaultValue) حفظ می‌شود.
const controlled =
value !== undefined
? { value: value ?? "" }
: { defaultValue: data };
return (
<div className="relative w-full">
<TextField
{...props}
// value={data}
defaultValue={data}
{...controlled}
disabled={disable}
type={type || "text"}
placeholder={placeholder || ""}
@@ -19,13 +19,18 @@ function Form({ insurance, errors, changeData, information }) {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-[24px] gap-y-[10px]">
<Content title="شماره موبایل" isConfirmed={true}>
<Field
type="number"
type="text"
disable={true}
iconGray={true}
name="work_phone"
name="mobile_number"
isTransparent={true}
changeData={changeData}
data={parsedUserInfo?.mobile_number ?? parsedUserInfo?.username}
value={
parsedUserInfo?.mobile_number ??
parsedUserInfo?.username ??
information?.mobile_number ??
""
}
/>
</Content>
<Content
@@ -43,7 +48,8 @@ function Form({ insurance, errors, changeData, information }) {
isTransparent={true}
changeData={changeData}
data={information?.national_code}
disable={information?.prev_data}
// کد ملی فقط وقتی از قبل مقدار دارد قفل می‌شود؛ اگر خالی است قابل افزودن باشد.
disable={Boolean(information?.national_code)}
/>
</Content>
<Content req={true} error={errors?.name} title="نام" isConfirmed={false}>
@@ -1,10 +1,8 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { toast } from "react-toastify";
import Form from "./form";
import { request } from "@/services/response";
import ButtonSendData from "./ButtonSendData";
import { changeDateType, getParsedUserInfo, imageUrl } from "@/helper";
import { changeDateType, getParsedUserInfo } from "@/helper";
import { disease } from "./form/disease";
import LoadingComponent from "@/app/component/LoadingComponent";
@@ -13,26 +11,6 @@ function Information({ information, setInformation }) {
const [insurance, setInsurance] = useState();
const [errors, setErrors] = useState({});
const [loadingInfo, setLoadingInfo] = useState(true);
const [avatarUploading, setAvatarUploading] = useState(false);
const handleAvatarChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setAvatarUploading(true);
try {
const res = await request.uploadUserAvatar(file);
const url = res?.data?.avatar || res?.data?.url;
if (url) {
setInformation((prev) => ({ ...(prev || {}), avatar: url }));
toast.success("عکس پروفایل به‌روزرسانی شد");
}
} catch {
toast.error("خطا در آپلود عکس");
} finally {
setAvatarUploading(false);
e.target.value = "";
}
};
useEffect(() => {
const parsedUserInfo = getParsedUserInfo();
@@ -51,11 +29,9 @@ function Information({ information, setInformation }) {
setLoadingInfo(false);
const profile = res?.data?.data;
const hasProfileData = Boolean(
profile && (profile.label || profile.family || profile.national_code)
);
if (hasProfileData) {
// ملاک «پروفایل از قبل وجود دارد» وجود uuid پروفایل است (نه پر بودن فیلدها)؛
// در غیر این صورت برای پروفایلِ خالیِ موجود، POST می‌زد و 409 می‌گرفت.
if (profile?.uuid) {
let changedData = { ...profile };
// backend stores the first name in `label`; the form binds to `name`
changedData.name = profile?.label ?? "";
@@ -75,7 +51,6 @@ function Information({ information, setInformation }) {
setInformation(changedData);
} else {
setInformation({
uuid: profile?.uuid,
prev_data: false,
other: {
disease: disease,
@@ -123,25 +98,6 @@ function Information({ information, setInformation }) {
return (
<LoadingComponent loading={loadingInfo}>
<div className="opacity-page">
<div className="flex items-center gap-[16px] mb-[24px]">
<Image
src={imageUrl(information?.avatar, "/assets/images/profile-user.png")}
alt="avatar"
width={72}
height={72}
className="w-[72px] h-[72px] rounded-full object-cover border border-[#EFEFEF]"
/>
<label className="cursor-pointer text-[#5559CE] text-[14px] font-medium">
{avatarUploading ? "در حال آپلود..." : "تغییر عکس"}
<input
type="file"
accept="image/*"
hidden
disabled={avatarUploading}
onChange={handleAvatarChange}
/>
</label>
</div>
<Form
errors={errors}
insurance={insurance}
+68 -9
View File
@@ -1,25 +1,84 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { getParsedUserInfo, imageUrl } from "@/helper";
import { request } from "@/services/response";
import { toast } from "react-toastify";
import Image from "next/image";
function Head() {
const [userInfo, setUserInfo] = useState(null);
const [avatar, setAvatar] = useState("");
const [uploading, setUploading] = useState(false);
const fileRef = useRef(null);
useEffect(() => {
setUserInfo(getParsedUserInfo());
const parsed = getParsedUserInfo();
setUserInfo(parsed);
if (parsed?.uuid) {
request
.getUserProfile(parsed.uuid)
.then((res) => {
const profile = res?.data?.data ?? res?.data;
if (profile?.avatar) setAvatar(profile.avatar);
})
.catch(() => {});
}
}, []);
const handleAvatarChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const res = await request.uploadUserAvatar(file);
const url = res?.data?.avatar || res?.data?.url;
if (url) {
setAvatar(url);
toast.success("عکس پروفایل به‌روزرسانی شد");
}
} catch {
toast.error("خطا در آپلود عکس");
} finally {
setUploading(false);
if (fileRef.current) fileRef.current.value = "";
}
};
return (
<div className="w-full flex flex-row md:flex-col items-center justify-center md:justify-start gap-[12px] md:gap-[4px]">
<Image
className="rounded-full border-[3px] border-solid border-[#F8F8FF] shadow-[0px_1px_25.6px_0px_rgba(85,_89,_205,_0.20)]"
src={imageUrl(userInfo?.picture?.[0]?.url)}
alt="profile"
height={73}
width={73}
/>
<div className="relative">
{avatar ? (
<Image
className="rounded-full border-[3px] border-solid border-[#F8F8FF] shadow-[0px_1px_25.6px_0px_rgba(85,_89,_205,_0.20)] object-cover w-[73px] h-[73px]"
src={imageUrl(avatar)}
alt="profile"
height={73}
width={73}
/>
) : (
<div className="w-[73px] h-[73px] rounded-full border-[3px] border-solid border-[#F8F8FF] shadow-[0px_1px_25.6px_0px_rgba(85,_89,_205,_0.20)] bg-[#5559CE] text-[#FFF] flex items-center justify-center text-[28px] font-bold select-none">
{userInfo?.realName?.trim()?.[0] ?? "؟"}
</div>
)}
<button
type="button"
onClick={() => fileRef.current?.click()}
disabled={uploading}
title="تغییر عکس"
className="absolute bottom-0 left-0 bg-[#5559CE] text-white rounded-full w-[24px] h-[24px] flex items-center justify-center text-[12px] shadow-md disabled:opacity-60"
>
{uploading ? "…" : "✎"}
</button>
<input
ref={fileRef}
type="file"
accept="image/*"
hidden
disabled={uploading}
onChange={handleAvatarChange}
/>
</div>
<div className="flex flex-col items-start md:items-center justify-center gap-[8px] md:gap-[4px]">
<p className="text-[#3B3B3B] text-[14px] font-bold">
{userInfo?.realName}