chore: change styles and add Api for user profile
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user