fix: code styles
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DisplayFieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function DisplayField({ label, value }: DisplayFieldProps) {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
const displayValue = value?.trim() || t('settingForm.notDetermined');
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{displayValue}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Box, Typography, Avatar } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CountryFlag } from '@/components/CountryFlag';
|
||||
import { DisplayField } from './DisplayField';
|
||||
import { Gender } from '@/features/profile/types';
|
||||
|
||||
interface InfoRowData {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
country: string;
|
||||
gender: Gender | '';
|
||||
nationalCode: string;
|
||||
}
|
||||
|
||||
interface InfoRowDisplayProps {
|
||||
data: InfoRowData;
|
||||
uploadedImageUrl: string | null;
|
||||
initials: string;
|
||||
}
|
||||
|
||||
export function InfoRowDisplay({
|
||||
data,
|
||||
uploadedImageUrl,
|
||||
initials,
|
||||
}: InfoRowDisplayProps) {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
const displayValue = (value: string) =>
|
||||
value?.trim() || t('settingForm.notDetermined');
|
||||
const getGenderLabel = (gender: Gender | '') => {
|
||||
switch (gender) {
|
||||
case Gender.Male:
|
||||
return t('settingForm.man');
|
||||
case Gender.Female:
|
||||
return t('settingForm.woman');
|
||||
default:
|
||||
return t('settingForm.notDetermined');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settingForm.name')} و {t('settingForm.familyName')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar
|
||||
src={uploadedImageUrl || undefined}
|
||||
sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
bgcolor: 'secondary.main',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
{initials}
|
||||
</Avatar>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{`${displayValue(data.firstName)} ${displayValue(data.lastName)}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settingForm.country')}
|
||||
</Typography>
|
||||
<CountryFlag country={data.country} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<DisplayField
|
||||
label={t('settingForm.gender')}
|
||||
value={getGenderLabel(data.gender)}
|
||||
/>
|
||||
<DisplayField
|
||||
label={t('settingForm.nationalCode')}
|
||||
value={displayValue(data.nationalCode)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
FormControl,
|
||||
MenuItem,
|
||||
Select,
|
||||
Autocomplete,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CountryFlag, countryCodeMap } from '@/components/CountryFlag';
|
||||
import { Gender } from '@/features/profile/types';
|
||||
import { type InfoRowData } from '@/features/profile/types';
|
||||
|
||||
interface InfoRowEditProps {
|
||||
data: InfoRowData;
|
||||
setData: React.Dispatch<React.SetStateAction<InfoRowData>>;
|
||||
gender: Gender;
|
||||
setGender: React.Dispatch<React.SetStateAction<Gender>>;
|
||||
}
|
||||
|
||||
export function InfoRowEdit({
|
||||
data,
|
||||
setData,
|
||||
gender,
|
||||
setGender,
|
||||
}: InfoRowEditProps) {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
|
||||
const labels = [
|
||||
{
|
||||
name: 'firstName' as keyof InfoRowData,
|
||||
label: t('settingForm.name'),
|
||||
value: data.firstName,
|
||||
},
|
||||
{
|
||||
name: 'lastName' as keyof InfoRowData,
|
||||
label: t('settingForm.familyName'),
|
||||
value: data.lastName,
|
||||
},
|
||||
{
|
||||
name: 'nationalCode' as keyof InfoRowData,
|
||||
label: t('settingForm.nationalCode'),
|
||||
value: data.nationalCode,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{labels.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) => ({
|
||||
...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 as Gender)}
|
||||
displayEmpty
|
||||
renderValue={(selected) =>
|
||||
selected ? (
|
||||
selected === Gender.Male ? (
|
||||
t('settingForm.man')
|
||||
) : (
|
||||
t('settingForm.woman')
|
||||
)
|
||||
) : (
|
||||
<span>{t('settingForm.genderPlaceholder')}</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItem value={Gender.Male}>{t('settingForm.man')}</MenuItem>
|
||||
<MenuItem value={Gender.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) => ({ ...prev, country: newValue || '' }))
|
||||
}
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props}>
|
||||
<CountryFlag country={option} />
|
||||
</Box>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={t('settingForm.country')} />
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Avatar, Typography, Button, IconButton } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Camera, Trash } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
|
||||
const MAX_FILE_SIZE_MB = 10;
|
||||
|
||||
interface ProfileImageProps {
|
||||
initials: string;
|
||||
uploadedImageUrl: string | null;
|
||||
onImageChange: (file: File) => void;
|
||||
onRemoveImage?: () => void;
|
||||
}
|
||||
|
||||
export function ProfileImage({
|
||||
initials,
|
||||
uploadedImageUrl,
|
||||
onImageChange,
|
||||
onRemoveImage,
|
||||
}: ProfileImageProps) {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setError(null);
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const fileSizeMB = file.size / (1024 * 1024);
|
||||
if (fileSizeMB > MAX_FILE_SIZE_MB) {
|
||||
setError(t('settingForm.fileSizeError', { size: MAX_FILE_SIZE_MB }));
|
||||
return;
|
||||
}
|
||||
|
||||
onImageChange(file);
|
||||
};
|
||||
|
||||
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>
|
||||
{error && (
|
||||
<Typography variant="body2" color="error" mt={1}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box mt={1} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Button
|
||||
variant={
|
||||
uploadedImageUrl && onRemoveImage ? 'outlined' : 'contained'
|
||||
}
|
||||
component="label"
|
||||
size="small"
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
startIcon={
|
||||
<Icon
|
||||
Component={Camera}
|
||||
size="small"
|
||||
color={
|
||||
uploadedImageUrl && onRemoveImage ? 'primary.main' : 'white'
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{uploadedImageUrl && onRemoveImage
|
||||
? t('settingForm.changePicture')
|
||||
: t('settingForm.uploadPicture')}
|
||||
<input
|
||||
hidden
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
type="file"
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
</Button>
|
||||
{uploadedImageUrl && onRemoveImage && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={onRemoveImage}
|
||||
aria-label={t('settingForm.removePicture')}
|
||||
>
|
||||
<Icon Component={Trash} color="error.main" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user