feat(userAccount): improve user profile handling with avatar upload and dynamic display name
This commit is contained in:
@@ -1,11 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import CalendarAdD from "@/components/icons/CalendarAdD";
|
import CalendarAdD from "@/components/icons/CalendarAdD";
|
||||||
import CardTickD from "@/components/icons/CardTickD";
|
import CardTickD from "@/components/icons/CardTickD";
|
||||||
import { getParsedUserInfo } from "@/helper";
|
import { getParsedUserInfo } from "@/helper";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
function Head({ user }) {
|
function Head({ user }) {
|
||||||
const userInfo = getParsedUserInfo() || {};
|
// مقدار اولیه از prop سرور تا SSR و کلاینت یکسان باشند (جلوگیری از hydration mismatch)؛
|
||||||
const displayName = userInfo.realName || user?.name || "کاربر";
|
// سپس در کلاینت نام واقعی از کوکی userInfo خوانده میشود.
|
||||||
|
const [displayName, setDisplayName] = useState(user?.name || "کاربر");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userInfo = getParsedUserInfo();
|
||||||
|
const name = userInfo?.realName || user?.name;
|
||||||
|
if (name) setDisplayName(name);
|
||||||
|
}, [user?.name]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden md:flex flex-col lg:flex-row w-full items-stretch justify-center gap-[12px] lg:gap-[24px]">
|
<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,
|
icon,
|
||||||
placeholder,
|
placeholder,
|
||||||
data,
|
data,
|
||||||
|
value,
|
||||||
name,
|
name,
|
||||||
iconGray,
|
iconGray,
|
||||||
error,
|
error,
|
||||||
@@ -16,12 +17,18 @@ function Field({
|
|||||||
props,
|
props,
|
||||||
disable,
|
disable,
|
||||||
}) {
|
}) {
|
||||||
|
// وقتی value پاس داده شود فیلد controlled است (برای مقادیر فقطخواندنی مثل موبایل)؛
|
||||||
|
// در غیر این صورت رفتار قبلی (uncontrolled با defaultValue) حفظ میشود.
|
||||||
|
const controlled =
|
||||||
|
value !== undefined
|
||||||
|
? { value: value ?? "" }
|
||||||
|
: { defaultValue: data };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<TextField
|
<TextField
|
||||||
{...props}
|
{...props}
|
||||||
// value={data}
|
{...controlled}
|
||||||
defaultValue={data}
|
|
||||||
disabled={disable}
|
disabled={disable}
|
||||||
type={type || "text"}
|
type={type || "text"}
|
||||||
placeholder={placeholder || ""}
|
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]">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-[24px] gap-y-[10px]">
|
||||||
<Content title="شماره موبایل" isConfirmed={true}>
|
<Content title="شماره موبایل" isConfirmed={true}>
|
||||||
<Field
|
<Field
|
||||||
type="number"
|
type="text"
|
||||||
disable={true}
|
disable={true}
|
||||||
iconGray={true}
|
iconGray={true}
|
||||||
name="work_phone"
|
name="mobile_number"
|
||||||
isTransparent={true}
|
isTransparent={true}
|
||||||
changeData={changeData}
|
changeData={changeData}
|
||||||
data={parsedUserInfo?.mobile_number ?? parsedUserInfo?.username}
|
value={
|
||||||
|
parsedUserInfo?.mobile_number ??
|
||||||
|
parsedUserInfo?.username ??
|
||||||
|
information?.mobile_number ??
|
||||||
|
""
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
<Content
|
<Content
|
||||||
@@ -43,7 +48,8 @@ function Form({ insurance, errors, changeData, information }) {
|
|||||||
isTransparent={true}
|
isTransparent={true}
|
||||||
changeData={changeData}
|
changeData={changeData}
|
||||||
data={information?.national_code}
|
data={information?.national_code}
|
||||||
disable={information?.prev_data}
|
// کد ملی فقط وقتی از قبل مقدار دارد قفل میشود؛ اگر خالی است قابل افزودن باشد.
|
||||||
|
disable={Boolean(information?.national_code)}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
<Content req={true} error={errors?.name} title="نام" isConfirmed={false}>
|
<Content req={true} error={errors?.name} title="نام" isConfirmed={false}>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import Form from "./form";
|
import Form from "./form";
|
||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
import ButtonSendData from "./ButtonSendData";
|
import ButtonSendData from "./ButtonSendData";
|
||||||
import { changeDateType, getParsedUserInfo, imageUrl } from "@/helper";
|
import { changeDateType, getParsedUserInfo } from "@/helper";
|
||||||
import { disease } from "./form/disease";
|
import { disease } from "./form/disease";
|
||||||
import LoadingComponent from "@/app/component/LoadingComponent";
|
import LoadingComponent from "@/app/component/LoadingComponent";
|
||||||
|
|
||||||
@@ -13,26 +11,6 @@ function Information({ information, setInformation }) {
|
|||||||
const [insurance, setInsurance] = useState();
|
const [insurance, setInsurance] = useState();
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [loadingInfo, setLoadingInfo] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
const parsedUserInfo = getParsedUserInfo();
|
const parsedUserInfo = getParsedUserInfo();
|
||||||
@@ -51,11 +29,9 @@ function Information({ information, setInformation }) {
|
|||||||
setLoadingInfo(false);
|
setLoadingInfo(false);
|
||||||
const profile = res?.data?.data;
|
const profile = res?.data?.data;
|
||||||
|
|
||||||
const hasProfileData = Boolean(
|
// ملاک «پروفایل از قبل وجود دارد» وجود uuid پروفایل است (نه پر بودن فیلدها)؛
|
||||||
profile && (profile.label || profile.family || profile.national_code)
|
// در غیر این صورت برای پروفایلِ خالیِ موجود، POST میزد و 409 میگرفت.
|
||||||
);
|
if (profile?.uuid) {
|
||||||
|
|
||||||
if (hasProfileData) {
|
|
||||||
let changedData = { ...profile };
|
let changedData = { ...profile };
|
||||||
// backend stores the first name in `label`; the form binds to `name`
|
// backend stores the first name in `label`; the form binds to `name`
|
||||||
changedData.name = profile?.label ?? "";
|
changedData.name = profile?.label ?? "";
|
||||||
@@ -75,7 +51,6 @@ function Information({ information, setInformation }) {
|
|||||||
setInformation(changedData);
|
setInformation(changedData);
|
||||||
} else {
|
} else {
|
||||||
setInformation({
|
setInformation({
|
||||||
uuid: profile?.uuid,
|
|
||||||
prev_data: false,
|
prev_data: false,
|
||||||
other: {
|
other: {
|
||||||
disease: disease,
|
disease: disease,
|
||||||
@@ -123,25 +98,6 @@ function Information({ information, setInformation }) {
|
|||||||
return (
|
return (
|
||||||
<LoadingComponent loading={loadingInfo}>
|
<LoadingComponent loading={loadingInfo}>
|
||||||
<div className="opacity-page">
|
<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
|
<Form
|
||||||
errors={errors}
|
errors={errors}
|
||||||
insurance={insurance}
|
insurance={insurance}
|
||||||
|
|||||||
@@ -1,25 +1,84 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { getParsedUserInfo, imageUrl } from "@/helper";
|
import { getParsedUserInfo, imageUrl } from "@/helper";
|
||||||
|
import { request } from "@/services/response";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
function Head() {
|
function Head() {
|
||||||
const [userInfo, setUserInfo] = useState(null);
|
const [userInfo, setUserInfo] = useState(null);
|
||||||
|
const [avatar, setAvatar] = useState("");
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const fileRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<div className="w-full flex flex-row md:flex-col items-center justify-center md:justify-start gap-[12px] md:gap-[4px]">
|
<div className="w-full flex flex-row md:flex-col items-center justify-center md:justify-start gap-[12px] md:gap-[4px]">
|
||||||
<Image
|
<div className="relative">
|
||||||
className="rounded-full border-[3px] border-solid border-[#F8F8FF] shadow-[0px_1px_25.6px_0px_rgba(85,_89,_205,_0.20)]"
|
{avatar ? (
|
||||||
src={imageUrl(userInfo?.picture?.[0]?.url)}
|
<Image
|
||||||
alt="profile"
|
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]"
|
||||||
height={73}
|
src={imageUrl(avatar)}
|
||||||
width={73}
|
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]">
|
<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">
|
<p className="text-[#3B3B3B] text-[14px] font-bold">
|
||||||
{userInfo?.realName}
|
{userInfo?.realName}
|
||||||
|
|||||||
Reference in New Issue
Block a user