chore: change styles and add Api for user profile

This commit is contained in:
Koosha Lahouti
2025-08-12 20:20:28 +03:30
parent ed57858c2e
commit 782ef2a2de
35 changed files with 2785 additions and 1843 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Box, Button } from '@mui/material';
import { Box, Button, Typography, CircularProgress } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personalInformation/ProfileImage';
@@ -7,43 +7,193 @@ import { InfoRowDisplay } from './personalInformation/InfoRowDisplay';
import { InfoRowEdit } from './personalInformation/InfoRowEdit';
import { PageWrapper } from '../PageWrapper';
import { Gender, type InfoRowData } from '../../types';
import axios, { isAxiosError } from 'axios';
import apiClient from '@/lib/apiClient';
interface GetProfileApiResponse {
success: boolean;
message?: string;
firstName?: string;
lastName?: string;
nationalCode?: string;
gender?: Gender;
countryCode?: string;
profileImageUrl?: string;
}
interface SaveProfileApiResponse {
success: boolean;
message?: string;
}
interface TokenApiResponse {
access_token: string;
}
export function PersonalInformation() {
const { t } = useTranslation('profileSetting');
const { t } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false);
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const initialData: InfoRowData = {
firstName: 'محمد حسین',
lastName: 'برزه‌گر',
country: 'قطر',
const [data, setData] = useState<InfoRowData>({
firstName: '',
lastName: '',
nationalCode: '',
gender: Gender.None,
};
country: '',
});
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
// const [token, setToken] = useState<string | null>(null);
const [tokenError, setTokenError] = useState<string | null>(null);
const [saveError, setSaveError] = useState<string | null>(null);
const storedToken = localStorage.getItem('authToken');
const [data, setData] = useState<InfoRowData>(initialData);
const [gender, setGender] = useState<Gender>(Gender.None);
const [isLoading, setIsLoading] = useState(true);
const [fetchError, setFetchError] = useState<string | null>(null);
useEffect(() => {
if (Object.values(Gender).includes(data.gender)) {
setGender(data.gender);
}
}, [data.gender]);
const fetchProfile = async () => {
setIsLoading(true);
setFetchError(null);
try {
const res = await apiClient.post<GetProfileApiResponse>(
'/Profile/GetProfile',
{},
{
headers: {
Authorization: `Bearer ${storedToken}`,
},
},
);
if (res.data?.success) {
const profile = res.data;
const fetchedData = {
firstName: profile.firstName ?? '',
lastName: profile.lastName ?? '',
nationalCode: profile.nationalCode ?? '',
gender: Object.values(Gender).includes(profile.gender as Gender)
? (profile.gender as Gender)
: Gender.None,
country: profile.countryCode ?? '',
};
setData(fetchedData);
setOriginalData(fetchedData);
setUploadedImageUrl(profile.profileImageUrl || null);
} else {
throw new Error(res.data.message || t('settingForm.failRetrieve'));
}
} catch (error: unknown) {
let message = t('settingForm.errorFetch');
if (error instanceof Error) {
message = error.message;
}
setFetchError(message);
} finally {
setIsLoading(false);
}
};
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
const toggleEdit = () => {
if (isEditing) {
setData((prev) => ({
...prev,
gender: gender,
}));
if (storedToken) {
fetchProfile();
} else {
setGender(
Object.values(Gender).includes(data.gender) ? data.gender : Gender.None,
);
setIsLoading(false);
setFetchError(t('settingForm.notLoggedIn'));
}
}, [storedToken, t]);
const initials = `${data?.firstName?.trim()[0] || ''}${
data?.lastName?.trim()[0] || ''
}`;
const handleEditClick = () => {
setIsEditing(true);
setSaveError(null);
setOriginalData(data);
};
const handleCancelClick = () => {
setIsEditing(false);
if (originalData) {
setData(originalData);
}
setSaveError(null);
};
const handleSaveClick = async () => {
if (!data) return;
setSaveError(null);
try {
const formData = new FormData();
formData.append('FirstName', data.firstName || '');
formData.append('LastName', data.lastName || '');
formData.append('NationalCode', data.nationalCode || '');
formData.append('Gender', String(data.gender ?? Gender.None));
formData.append('CountryCode', data.country || '');
if (uploadedImageUrl && uploadedImageUrl.startsWith('data:')) {
const response = await fetch(uploadedImageUrl);
const blob = await response.blob();
formData.append('Image', blob, 'profile.jpg');
}
const res = await apiClient.post<SaveProfileApiResponse>(
'Profile/SaveProfilePersonalInforamtion',
formData,
{
headers: {
Authorization: `Bearer ${storedToken}`,
},
},
);
if (res.data.success) {
setIsEditing(false);
setOriginalData(data);
} else {
throw new Error(res.data.message || t('settingForm.unknownError'));
}
} catch (error: unknown) {
let message = t('settingForm.checkConnection');
if (error instanceof Error) {
message = error.message;
}
setSaveError(message);
}
};
const apiUrl = 'https://accounts.business-harmony.com';
const tokenEndpoint = `${apiUrl}/connect/token`;
const getToken = async () => {
setTokenError(null);
try {
const body = new URLSearchParams();
body.set('grant_type', 'password');
body.set('username', 'zareian.1381@gmail.com');
body.set('password', '123@Qweasd');
body.set('client_id', 'harmony_identity');
body.set('scope', 'openid harmony_identity profile offline_access');
const response = await axios.post<TokenApiResponse>(
tokenEndpoint,
body.toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
},
);
if (response.data?.access_token) {
localStorage.setItem('authToken', response.data.access_token);
} else {
throw new Error('No access token in response');
}
} catch (error: unknown) {
let message = 'Failed to get token';
if (isAxiosError(error) && error.response) {
message = `Request failed with status ${error.response.status}`;
} else if (error instanceof Error) {
message = error.message;
}
setTokenError(message);
}
setIsEditing(!isEditing);
};
return (
@@ -53,78 +203,140 @@ export function PersonalInformation() {
subtitle={t('settingForm.descriptionPersonalInfo')}
highlighted={isEditing}
action={
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{isEditing && (
<Button
variant="text"
onClick={() => setIsEditing(false)}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
// fontSize: { xs: '0.8 5rem', sm: '1rem' },
}}
>
{t('settingForm.rejectButton')}
</Button>
)}
<Button
onClick={toggleEdit}
size="large"
variant="outlined"
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
width: '100%',
}}
>
<Box
sx={{
borderRadius: 1,
bgcolor: isEditing ? 'primary.main' : 'background.default',
color: isEditing ? 'primary.contrastText' : 'primary.main',
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: 'flex-end',
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editButton')}
</Button>
<Button
variant="contained"
color="secondary"
onClick={getToken}
size="large"
sx={{ textTransform: 'none' }}
disabled={isLoading}
>
Get Token
</Button>
{isEditing ? (
<>
<Button
variant="text"
onClick={handleCancelClick}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.rejectButton')}
</Button>
<Button
onClick={handleSaveClick}
size="large"
variant="contained"
sx={{
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.saveButton')}
</Button>
</>
) : (
<Button
onClick={handleEditClick}
size="large"
variant="outlined"
sx={{
borderRadius: 1,
}}
disabled={isLoading}
>
{t('settingForm.editButton')}
</Button>
)}
</Box>
{saveError && (
<Typography
color="error"
sx={{ mt: 1, textAlign: 'right', width: '100%' }}
>
{saveError}
</Typography>
)}
</Box>
}
>
<Box
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
{isLoading ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
p: 4,
minHeight: '200px',
}}
>
<CircularProgress />
</Box>
) : fetchError ? (
<Box sx={{ textAlign: 'center', p: 4 }}>
<Typography color="error">{fetchError}</Typography>
</Box>
) : (
<>
{tokenError && (
<Box sx={{ mt: 2, color: 'red' }}>Error: {tokenError}</Box>
)}
<Box
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
onRemoveImage={() => setUploadedImageUrl(null)}
/>
)}
{isEditing ? (
<InfoRowEdit
data={data}
setData={setData}
gender={gender}
setGender={setGender}
/>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
)}
</Box>
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}}
onRemoveImage={() => setUploadedImageUrl(null)}
/>
)}
{data &&
(isEditing ? (
<InfoRowEdit data={data} setData={setData} />
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
))}
</Box>
</>
)}
</CardContainer>
</PageWrapper>
);