fix: styles and add responsiveness

This commit is contained in:
2025-07-26 11:42:37 +03:30
parent 40c0415b63
commit 67a4c4dee4
10 changed files with 361 additions and 267 deletions

View File

@@ -36,6 +36,7 @@
"allowedFormat": "Allowed formats: PNG, JPEG, GIF (maximum 10 MB)",
"uploadPicture": "Upload image",
"phoneNumberText": "Your new contact number will replace your previous contact number.",
"verb": "."
"verb": ".",
"notDetermined": "Not determined"
}
}

View File

@@ -36,6 +36,7 @@
"allowedFormat": "فرمت‌های مجاز: PNG، JPEG، GIF (حداکثر ۱۰ مگابایت)",
"uploadPicture": "بارگذاری تصویر",
"phoneNumberText": "شماره تماس جدید شما جایگزین شماره تماس قبلی",
"verb": "خواهد شد"
"verb": "خواهد شد",
"notDetermined": "تعیین نشده"
}
}

View File

@@ -19,14 +19,14 @@ export function CardContainer({
width: '100%',
maxWidth: {
xs: '100%',
sm: '500pxpx',
md: '786px',
sm: '500px',
md: '818px',
},
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3, md: 4 },
py: 2,
// py: 2,
}}
>
<Box

View File

@@ -1,25 +1,16 @@
import {
Box,
Typography,
Button,
TextField,
FormControl,
Select,
MenuItem,
Avatar,
Autocomplete,
type SelectChangeEvent,
} from '@mui/material';
import { useState, type ChangeEvent } from 'react';
import { CardContainer } from '@/components/CardContainer';
import { CountryFlag, countryCodeMap } from '@/components/CountryFlag';
import { Box, Button } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Camera } from 'iconsax-react';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personlInformation/ProfileImage';
import { InfoRowDisplay } from './personlInformation/InfoRowDisplay';
import { InfoRowEdit } from './personlInformation/InfoRowEdit';
export function PersonalInformation() {
const { t } = useTranslation('profileSetting');
const [isEditing, setIsEditing] = useState(false);
const [gender, setGender] = useState('');
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const initialData = {
firstName: 'محمد حسین',
@@ -29,15 +20,8 @@ export function PersonalInformation() {
nationalCode: '',
};
const [data, setData] = useState(initialData);
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setData((prev) => ({
...prev,
[name]: value,
}));
};
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
const toggleEdit = () => {
setIsEditing((prev) => !prev);
@@ -53,30 +37,13 @@ export function PersonalInformation() {
}
};
const handleChangeGender = (e: SelectChangeEvent) => {
setGender(e.target.value);
};
const displayValue = (value: string | null | undefined) => {
return value && value.trim() !== '' ? value : 'تعیین نشده';
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setUploadedImageUrl(reader.result as string);
};
reader.readAsDataURL(file);
}
};
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
return (
<Box
sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
sx={{
display: 'flex',
justifyContent: 'center',
backgroundColor: 'background.paper',
}}
>
<CardContainer
title={t('settingForm.titlePersonalInfo')}
@@ -130,199 +97,39 @@ export function PersonalInformation() {
display: 'flex',
flexDirection: 'column',
gap: 2,
backgroundColor: 'background.paper',
// width: '754px',
}}
>
{isEditing && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
flexWrap: 'wrap',
}}
>
<Avatar
sx={{
bgcolor: 'secondary.main',
width: 88,
height: 88,
fontSize: '20px',
}}
src={uploadedImageUrl || undefined}
>
{initials}
</Avatar>
<Box>
<Typography variant="body1">
{t('settingForm.profilePicture')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.allowedFormat')}
</Typography>
<Box mt={1}>
<Button
variant="contained"
component="label"
sx={{
borderRadius: 2,
textTransform: 'none',
height: '30px',
fontSize: '13px',
}}
startIcon={<Camera size={18} color="white" />}
>
{t('settingForm.uploadPicture')}
<input
hidden
accept="image/png, image/jpeg, image/gif"
type="file"
onChange={handleImageChange}
/>
</Button>
</Box>
</Box>
</Box>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
{[
{
name: 'firstName',
label: t('settingForm.name'),
value: data.firstName,
},
{
name: 'lastName',
label: t('settingForm.familyName'),
value: data.lastName,
},
{
name: 'nationalCode',
label: t('settingForm.nationalCode'),
value: data.nationalCode,
},
].map((field, index) => (
<Box
key={index}
sx={{
width: {
xs: '100%',
sm: '48%',
md: 'calc(50% - 8px)',
},
}}
>
{isEditing ? (
<TextField
fullWidth
name={field.name}
value={field.value}
onChange={handleChange}
label={field.label}
/>
) : (
<Box sx={{ px: 2 }}>
<Typography variant="caption" color="text.secondary">
{field.label}
</Typography>
<Typography variant="body1" color="text.primary">
{displayValue(field.value)}
</Typography>
</Box>
)}
</Box>
))}
<Box
sx={{
width: {
xs: '100%',
sm: '48%',
md: 'calc(50% - 8px)',
},
}}
>
{isEditing ? (
<FormControl fullWidth>
<Select
value={gender}
onChange={handleChangeGender}
displayEmpty
renderValue={(selected) => {
if (!selected) {
return (
<span style={{ color: '#aaa' }}>
{t('settingForm.genderPlaceholder')}
</span>
);
}
return selected === 'male'
? t('settingForm.man')
: t('settingForm.woman');
}}
>
<MenuItem value="male">{t('settingForm.man')}</MenuItem>
<MenuItem value="female">{t('settingForm.woman')}</MenuItem>
</Select>
</FormControl>
) : (
<Box sx={{ px: 2 }}>
<Typography variant="caption" color="text.secondary">
{t('settingForm.gender')}
</Typography>
<Typography variant="body1" color="text.primary">
{displayValue(data.gender)}
</Typography>
</Box>
)}
</Box>
{isEditing && (
<Autocomplete
sx={{
width: {
xs: '100%',
sm: '48%',
md: 'calc(50% - 8px)',
},
}}
options={Object.keys(countryCodeMap)}
value={data.country}
onChange={(_, newValue) =>
setData((prev) => ({
...prev,
country: newValue || '',
}))
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}
renderOption={(props, option) => (
<Box component="li" {...props}>
<CountryFlag country={option} />
</Box>
)}
renderInput={(params) => (
<TextField {...params} label={t('settingForm.country')} />
)}
/>
)}
{!isEditing && (
<Box
sx={{
px: 2,
width: {
xs: '100%',
sm: '48%',
md: 'calc(50% - 8px)',
},
}}
>
<Typography variant="caption" color="text.secondary">
{t('settingForm.country')}
</Typography>
<CountryFlag country={data.country} />
</Box>
)}
</Box>
}}
/>
)}
{isEditing ? (
<InfoRowEdit
data={data}
setData={setData}
gender={gender}
setGender={setGender}
/>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
)}
</Box>
</CardContainer>
</Box>

View File

@@ -57,6 +57,7 @@ export function PhoneNumber() {
justifyContent: 'center',
alignItems: 'center',
px: 2,
backgroundColor: 'background.paper',
}}
>
<CardContainer
@@ -104,7 +105,9 @@ export function PhoneNumber() {
}
>
{isEditing ? (
<Box sx={{ px: { xs: 2, sm: 4 } }}>
<Box
sx={{ px: { xs: 2, sm: 4 }, backgroundColor: 'background.paper' }}
>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">
{t('settingForm.editPhoneNumber')}
@@ -120,6 +123,7 @@ export function PhoneNumber() {
display: 'flex',
flexWrap: 'wrap',
gap: 2,
height: '88px',
}}
>
<TextField
@@ -128,7 +132,7 @@ export function PhoneNumber() {
type="tel"
value={phoneNumber}
onChange={handlePhoneNumberChange}
sx={{ flex: 1, minWidth: '250px' }}
sx={{ flex: 1, minWidth: '250px', mb: 2 }}
placeholder="09123456789"
inputProps={{
dir: 'rtl',
@@ -169,25 +173,28 @@ export function PhoneNumber() {
</Typography>
</Box>
) : (
<Button
variant="text"
onClick={handleSendCode}
disabled={buttonState === 'counting'}
sx={{
textTransform: 'none',
minWidth: '170px',
color: 'primary.main',
}}
>
{buttonState === 'counting' ? (
<CountDownTimer
initialSeconds={60}
onComplete={() => setButtonState('default')}
/>
) : (
t('settingForm.verificationCodeButton')
)}
</Button>
<Box sx={{}}>
<Button
variant="text"
onClick={handleSendCode}
disabled={buttonState === 'counting'}
sx={{
textTransform: 'none',
minWidth: '170px',
color: 'primary.main',
height: '56px',
}}
>
{buttonState === 'counting' ? (
<CountDownTimer
initialSeconds={60}
onComplete={() => setButtonState('default')}
/>
) : (
t('settingForm.verificationCodeButton')
)}
</Button>
</Box>
)}
</Box>
@@ -198,6 +205,7 @@ export function PhoneNumber() {
flexWrap: 'wrap',
gap: 2,
mt: 2,
height: '88px',
}}
>
<TextField
@@ -221,6 +229,7 @@ export function PhoneNumber() {
textTransform: 'none',
minWidth: '170px',
backgroundColor: 'primary.main',
height: '56px',
}}
>
{isVerifying ? (
@@ -229,6 +238,10 @@ export function PhoneNumber() {
sx={{
display: 'flex',
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
>
<Refresh size="20" color="white" />
@@ -249,7 +262,7 @@ export function PhoneNumber() {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
py: 1,
height: '148px',
width: '100%',
}}
>

View File

@@ -58,6 +58,7 @@ export function SocialMedia() {
justifyContent: 'center',
alignItems: 'center',
px: 2,
backgroundColor: 'background.paper',
}}
>
<CardContainer
@@ -155,7 +156,6 @@ export function SocialMedia() {
<Box
sx={{
width: '100%',
backgroundColor: 'background.paper',
borderRadius: '8px',
p: { xs: 1, sm: 2 },
}}
@@ -170,8 +170,6 @@ export function SocialMedia() {
flexWrap: 'wrap',
py: 1,
gap: 1,
borderBottom:
index !== emailList.length - 1 ? '1px solid #eee' : 'none',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
@@ -209,7 +207,6 @@ export function SocialMedia() {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
// backgroundColor: 'background.paper',
fontWeight: 'bold',
fontSize: '16px',
gap: 1,
@@ -258,7 +255,7 @@ export function SocialMedia() {
sx={{
textTransform: 'none',
borderRadius: '8px',
backgroundColor: '#1976d2',
// backgroundColor: '#1976d2',
}}
disabled={emailError || emailInput === ''}
>

View File

@@ -0,0 +1,24 @@
import { Box, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
export function DisplayField({
label,
value,
}: {
label: string;
value: string;
}) {
const { t } = useTranslation('profileSetting');
const displayValue = value?.trim() || t('settingForm.notDetermined');
return (
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
<Typography variant="caption" color="text.secondary">
{label}
</Typography>
<Typography variant="body1" color="text.primary">
{displayValue}
</Typography>
</Box>
);
}

View File

@@ -0,0 +1,92 @@
import { Box, Typography, Avatar } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CountryFlag } from '@/components/CountryFlag';
import { DisplayField } from './DisplayField';
export function InfoRowDisplay({
data,
uploadedImageUrl,
initials,
}: {
data: any;
uploadedImageUrl: string | null;
initials: string;
}) {
const { t } = useTranslation('profileSetting');
const displayValue = (value: string) =>
value?.trim() || t('settingForm.notDetermined');
return (
<>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
flexWrap: 'wrap',
alignItems: { xs: 'flex-start', sm: 'center' },
gap: { xs: 2, sm: 1 },
px: 2,
mb: 2,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: '337px',
// maxWidth: '337px',
flexWrap: 'wrap',
gap: 2,
}}
>
<Avatar
src={uploadedImageUrl || undefined}
sx={{
width: 32,
height: 32,
bgcolor: 'secondary.main',
fontSize: '16px',
}}
>
{initials}
</Avatar>
<Box>
<Typography variant="caption" color="text.secondary">
{t('settingForm.name')} و {t('settingForm.familyName')}
</Typography>
<Typography variant="body1" color="text.primary">
{`${displayValue(data.firstName)} ${displayValue(data.lastName)}`}
</Typography>
</Box>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
{t('settingForm.country')}
</Typography>
<CountryFlag country={data.country} />
</Box>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
flexWrap: 'wrap',
gap: 2,
px: 6,
}}
>
<DisplayField
label={t('settingForm.gender')}
value={displayValue(data.gender)}
/>
<DisplayField
label={t('settingForm.nationalCode')}
value={displayValue(data.nationalCode)}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,97 @@
import {
Box,
TextField,
FormControl,
MenuItem,
Select,
Autocomplete,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CountryFlag, countryCodeMap } from '@/components/CountryFlag';
export function InfoRowEdit({ data, setData, gender, setGender }: any) {
const { t } = useTranslation('profileSetting');
return (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
{[
{
name: 'firstName',
label: t('settingForm.name'),
value: data.firstName,
},
{
name: 'lastName',
label: t('settingForm.familyName'),
value: data.lastName,
},
{
name: 'nationalCode',
label: t('settingForm.nationalCode'),
value: data.nationalCode,
},
].map((field, idx) => (
<Box
key={idx}
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
>
<TextField
fullWidth
name={field.name}
value={field.value}
onChange={(e) =>
setData((prev: any) => ({
...prev,
[field.name]: e.target.value,
}))
}
label={field.label}
/>
</Box>
))}
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
<FormControl fullWidth>
<Select
value={gender}
onChange={(e) => setGender(e.target.value)}
displayEmpty
renderValue={(selected) =>
selected ? (
selected === 'male' ? (
t('settingForm.man')
) : (
t('settingForm.woman')
)
) : (
<span style={{ color: '#aaa' }}>
{t('settingForm.genderPlaceholder')}
</span>
)
}
>
<MenuItem value="male">{t('settingForm.man')}</MenuItem>
<MenuItem value="female">{t('settingForm.woman')}</MenuItem>
</Select>
</FormControl>
</Box>
<Autocomplete
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
options={Object.keys(countryCodeMap)}
value={data.country}
onChange={(_, newValue) =>
setData((prev: any) => ({ ...prev, country: newValue || '' }))
}
renderOption={(props, option) => (
<Box component="li" {...props}>
<CountryFlag country={option} />
</Box>
)}
renderInput={(params) => (
<TextField {...params} label={t('settingForm.country')} />
)}
/>
</Box>
);
}

View File

@@ -0,0 +1,62 @@
import { Box, Avatar, Typography, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { Camera } from 'iconsax-react';
export function ProfileImage({
initials,
uploadedImageUrl,
onImageChange,
}: {
initials: string;
uploadedImageUrl: string | null;
onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
const { t } = useTranslation('profileSetting');
return (
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}
>
<Avatar
sx={{
bgcolor: 'secondary.main',
width: 88,
height: 88,
fontSize: '20px',
}}
src={uploadedImageUrl || undefined}
>
{initials}
</Avatar>
<Box>
<Typography variant="body1">
{t('settingForm.profilePicture')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.allowedFormat')}
</Typography>
<Box mt={1}>
<Button
variant="contained"
component="label"
sx={{
borderRadius: 2,
textTransform: 'none',
height: '30px',
fontSize: '13px',
}}
startIcon={<Camera size={18} color="white" />}
>
{t('settingForm.uploadPicture')}
<input
hidden
accept="image/png, image/jpeg, image/gif"
type="file"
onChange={onImageChange}
/>
</Button>
</Box>
</Box>
</Box>
);
}