fix: code styles

This commit is contained in:
Koosha Lahouti
2025-08-10 16:21:25 -07:00
parent 57959f39ce
commit 8e6c09225d
28 changed files with 613 additions and 568 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}