Merge branch 'feat/user-profile' into feat/user-security
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -3150,6 +3150,7 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
|
||||
"node_modules/iconsax-reactjs": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/iconsax-reactjs/-/iconsax-reactjs-0.0.8.tgz",
|
||||
@@ -3160,6 +3161,7 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
||||
42
public/locales/en/profileSetting.json
Normal file
42
public/locales/en/profileSetting.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"settingForm": {
|
||||
"titlePersonalInfo": "My Personal Information",
|
||||
"descriptionPersonalInfo": "This information is only for your identification and remains with Harmony.",
|
||||
"titlePhoneNumber": "My phone number",
|
||||
"descriptionPhoneNumber": "This information is only for your identification and remains with Harmony.",
|
||||
"titleSocial": "My email and social medias",
|
||||
"descriptionSocial": "This information is only for your identification and remains with Harmony.",
|
||||
"rejectButton": "Cancel",
|
||||
"saveButton": "Save",
|
||||
"editButton": "Edit",
|
||||
"editPhoneNumber": "Change phone number",
|
||||
"addEmailOrSocialButton": "Add email / social",
|
||||
"addEmailButton": "Add email",
|
||||
"name": "Name",
|
||||
"familyName": "Family Name",
|
||||
"country": "Country",
|
||||
"gender": "Gender",
|
||||
"nationalCode": "National code",
|
||||
"man": "Male",
|
||||
"woman": "Female",
|
||||
"genderPlaceholder": "Male",
|
||||
"newPhoneNumber": "New phone number",
|
||||
"verificationCodeButton": "Send verification code",
|
||||
"verificationCode": "Verification code",
|
||||
"checkCode": "Check code",
|
||||
"successButton": "Confirmed",
|
||||
"email": "Email",
|
||||
"apple": "Apple",
|
||||
"google": "Google",
|
||||
"newEmail": "New email",
|
||||
"dialogHeader": "By activating your email, you can use this email to log in the next time you log in.",
|
||||
"or": "Or",
|
||||
"emailError": "Please enter a valid email.",
|
||||
"profilePicture": "User account image",
|
||||
"allowedFormat": "Allowed formats: PNG, JPEG, GIF (maximum 10 MB)",
|
||||
"uploadPicture": "Upload image",
|
||||
"phoneNumberText": "Your new contact number will replace your previous contact number.",
|
||||
"verb": ".",
|
||||
"notDetermined": "Not determined"
|
||||
}
|
||||
}
|
||||
43
public/locales/fa/profileSetting.json
Normal file
43
public/locales/fa/profileSetting.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"settingForm": {
|
||||
"titlePersonalInfo": "اطلاعات شخصی من",
|
||||
"descriptionPersonalInfo": "این اطلاعات شما صرفا برای احراز هویت شما است و نزد هارمونی باقی میماند",
|
||||
"titlePhoneNumber": "شماره تماس من",
|
||||
"descriptionPhoneNumber": "این اطلاعات شما صرفا برای احراز هویت شما است و نزد هارمونی باقی میماند",
|
||||
"titleSocial": "ایمیل و شبکه های اجتماعی من",
|
||||
"descriptionSocial": "این اطلاعات شما صرفاً برای احراز هویت شما است و نزد هارمونی باقی میماند",
|
||||
"rejectButton": "لغو",
|
||||
"saveButton": "ذخیره",
|
||||
"editButton": "ویرایش",
|
||||
"editPhoneNumber": "تغییر شماره تماس",
|
||||
"addEmailOrSocialButton": "افزودن ایمیل / سوشال",
|
||||
"addEmailButton": "افزودن ایمیل",
|
||||
"name": "نام",
|
||||
"familyName": "نام خانوادگی",
|
||||
"country": "کشور",
|
||||
"gender": "جنسیت",
|
||||
"nationalCode": "کد ملی",
|
||||
"man": "مرد",
|
||||
"woman": "زن",
|
||||
"genderPlaceholder": "مرد",
|
||||
"newPhoneNumber": "شماره تماس جدید",
|
||||
"verificationCodeButton": "ارسال کد تایید",
|
||||
"verificationCode": "کد تایید",
|
||||
"checkCode": "بررسی کد",
|
||||
"successButton": "تایید شد",
|
||||
"email": "ایمیل",
|
||||
"apple": "اپل",
|
||||
"google": "گوگل",
|
||||
"newEmail": "ایمیل جدید",
|
||||
"dialogHeader": "با فعالسازی ایمیل میتوانید در دفعات بعدی ورود برای ورود از این ایمیل استفاده کنید",
|
||||
"or": "یا",
|
||||
"emailError": "لطفا یک ایمیل معتبر وارد کنید",
|
||||
"profilePicture": "تصویر حساب کاربری",
|
||||
"allowedFormat": "فرمتهای مجاز: PNG، JPEG، GIF (حداکثر ۱۰ مگابایت)",
|
||||
"uploadPicture": "بارگذاری تصویر",
|
||||
"phoneNumberText": "شماره تماس جدید شما جایگزین شماره تماس قبلی",
|
||||
"verb": "خواهد شد",
|
||||
"notDetermined": "تعیین نشده",
|
||||
"successfulChangePhone": "شماره تماس با موفقیت تغییر کرد"
|
||||
}
|
||||
}
|
||||
10
src/App.tsx
10
src/App.tsx
@@ -1,25 +1,19 @@
|
||||
import { CssBaseline, useColorScheme } from '@mui/material';
|
||||
import './App.css';
|
||||
import { LanguageManager } from './components/LanguageManager';
|
||||
import { Settings } from './features/profile/Settings';
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<LanguageManager />
|
||||
{/* <Setting /> */}
|
||||
{/* <RecentLogins /> */}
|
||||
<Settings />
|
||||
{/* <UserSecurity />
|
||||
<ActiveDevices />
|
||||
<Setting /> */}
|
||||
|
||||
{/* <div style={{ padding: '16px' }}>
|
||||
<Typography variant="h3">{t('helloWorld')}</Typography>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: '10px', mt: 5 }}
|
||||
>
|
||||
<ThemeToggleButton />
|
||||
<Button color="secondary" variant="contained">
|
||||
secondary button
|
||||
</Button>
|
||||
|
||||
39
src/components/CountDownTimer.tsx
Normal file
39
src/components/CountDownTimer.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface CountdownTimerProps {
|
||||
initialSeconds: number;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function CountDownTimer({
|
||||
initialSeconds,
|
||||
onComplete,
|
||||
}: CountdownTimerProps) {
|
||||
const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
|
||||
|
||||
useEffect(() => {
|
||||
setSecondsLeft(initialSeconds);
|
||||
}, [initialSeconds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (secondsLeft <= 0) {
|
||||
onComplete?.();
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(() => {
|
||||
setSecondsLeft((prev) => prev - 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [secondsLeft, onComplete]);
|
||||
|
||||
const toPersianDigits = (str: string) =>
|
||||
str.replace(/\d/g, (d: string) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
|
||||
|
||||
const formatTime = (totalSeconds: number) => {
|
||||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
||||
return toPersianDigits(`${minutes}:${seconds}`);
|
||||
};
|
||||
|
||||
return <span>{formatTime(secondsLeft)}</span>;
|
||||
}
|
||||
40
src/components/Countries.tsx
Normal file
40
src/components/Countries.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// components/Countries.ts
|
||||
|
||||
export interface Country {
|
||||
name: string;
|
||||
fa: string;
|
||||
flag: string;
|
||||
}
|
||||
|
||||
export const countries: Country[] = [
|
||||
{ name: 'Afghanistan', fa: 'افغانستان', flag: 'af' },
|
||||
{ name: 'Albania', fa: 'آلبانی', flag: 'al' },
|
||||
{ name: 'Algeria', fa: 'الجزایر', flag: 'dz' },
|
||||
{ name: 'Argentina', fa: 'آرژانتین', flag: 'ar' },
|
||||
{ name: 'Armenia', fa: 'ارمنستان', flag: 'am' },
|
||||
{ name: 'Australia', fa: 'استرالیا', flag: 'au' },
|
||||
{ name: 'Austria', fa: 'اتریش', flag: 'at' },
|
||||
{ name: 'Bahrain', fa: 'بحرین', flag: 'bh' },
|
||||
{ name: 'Canada', fa: 'کانادا', flag: 'ca' },
|
||||
{ name: 'China', fa: 'چین', flag: 'cn' },
|
||||
{ name: 'France', fa: 'فرانسه', flag: 'fr' },
|
||||
{ name: 'Germany', fa: 'آلمان', flag: 'de' },
|
||||
{ name: 'India', fa: 'هند', flag: 'in' },
|
||||
{ name: 'Iran', fa: 'ایران', flag: 'ir' },
|
||||
{ name: 'Iraq', fa: 'عراق', flag: 'iq' },
|
||||
{ name: 'Italy', fa: 'ایتالیا', flag: 'it' },
|
||||
{ name: 'Japan', fa: 'ژاپن', flag: 'jp' },
|
||||
{ name: 'Netherlands', fa: 'هلند', flag: 'nl' },
|
||||
{ name: 'Pakistan', fa: 'پاکستان', flag: 'pk' },
|
||||
{ name: 'Qatar', fa: 'قطر', flag: 'qa' },
|
||||
{ name: 'Russia', fa: 'روسیه', flag: 'ru' },
|
||||
{ name: 'Saudi Arabia', fa: 'عربستان سعودی', flag: 'sa' },
|
||||
{ name: 'Spain', fa: 'اسپانیا', flag: 'es' },
|
||||
{ name: 'Sweden', fa: 'سوئد', flag: 'se' },
|
||||
{ name: 'Switzerland', fa: 'سوئیس', flag: 'ch' },
|
||||
{ name: 'Turkey', fa: 'ترکیه', flag: 'tr' },
|
||||
{ name: 'United Arab Emirates', fa: 'امارات متحده عربی', flag: 'ae' },
|
||||
{ name: 'United Kingdom', fa: 'بریتانیا', flag: 'gb' },
|
||||
{ name: 'United States', fa: 'ایالات متحده آمریکا', flag: 'us' },
|
||||
{ name: 'Yemen', fa: 'یمن', flag: 'ye' },
|
||||
];
|
||||
46
src/components/CountryFlag.tsx
Normal file
46
src/components/CountryFlag.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export const countryCodeMap: { [key: string]: string } = {
|
||||
ایران: 'ir',
|
||||
قطر: 'qa',
|
||||
آلمان: 'de',
|
||||
آمریکا: 'us',
|
||||
فرانسه: 'fr',
|
||||
ایتالیا: 'it',
|
||||
اسپانیا: 'es',
|
||||
انگلیس: 'gb',
|
||||
کانادا: 'ca',
|
||||
استرالیا: 'au',
|
||||
چین: 'cn',
|
||||
ژاپن: 'jp',
|
||||
هند: 'in',
|
||||
روسیه: 'ru',
|
||||
برزیل: 'br',
|
||||
آرژانتین: 'ar',
|
||||
ترکیه: 'tr',
|
||||
سوئیس: 'ch',
|
||||
سوئد: 'se',
|
||||
نروژ: 'no',
|
||||
عربستان: 'sa',
|
||||
امارات: 'ae',
|
||||
عراق: 'iq',
|
||||
پاکستان: 'pk',
|
||||
};
|
||||
|
||||
export function CountryFlag({ country }: { country: string }) {
|
||||
const countryCode = countryCodeMap[country] || 'un';
|
||||
const flagUrl = `https://flagcdn.com/w40/${countryCode}.png`;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<img
|
||||
src={flagUrl}
|
||||
alt={country}
|
||||
width="24"
|
||||
height="16"
|
||||
style={{ borderRadius: '2px', border: '1px solid #ccc' }}
|
||||
/>
|
||||
<Typography variant="body2">{country}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Alert, Snackbar, type AlertColor } from '@mui/material';
|
||||
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
|
||||
export interface ToastProps extends PropsWithChildren {
|
||||
color: AlertColor | undefined;
|
||||
open: boolean;
|
||||
|
||||
353
src/features/authentication/components/UserCompletionForm.tsx
Normal file
353
src/features/authentication/components/UserCompletionForm.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Box,
|
||||
type SelectChangeEvent,
|
||||
Switch,
|
||||
FormGroup,
|
||||
Button,
|
||||
Typography,
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export function UserCompletionForm() {
|
||||
const [sex, setSex] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showEmail, setShowEmail] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [buttonState, setButtonState] = useState('default'); // default | counting | sent
|
||||
const [countdown, setCountdown] = useState(60);
|
||||
const matchPassword = password === confirmPassword;
|
||||
const hasNumber = /\d/.test(password);
|
||||
const hasMinLength = password.length >= 8;
|
||||
const hasUpperAndLower = /[A-Z]/.test(password) && /[a-z]/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*]/.test(password);
|
||||
const correctEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
|
||||
const handleTogglePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowPassword(e.target.checked);
|
||||
};
|
||||
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowEmail(e.target.checked);
|
||||
};
|
||||
|
||||
const handleChange = (e: SelectChangeEvent) => {
|
||||
setSex(e.target.value);
|
||||
};
|
||||
|
||||
const onClickCodeSent = () => {
|
||||
setCodeSent(true);
|
||||
setButtonState('sent');
|
||||
setTimeout(() => {
|
||||
setButtonState('counting');
|
||||
setCountdown(60);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setInterval>;
|
||||
if (buttonState === 'counting' && countdown > 0) {
|
||||
timer = setInterval(() => {
|
||||
setCountdown((prev) => prev - 1);
|
||||
}, 1000);
|
||||
}
|
||||
if (countdown === 0) {
|
||||
setButtonState('default');
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [buttonState, countdown]);
|
||||
|
||||
const toPersianDigits = (str: string) =>
|
||||
str.replace(/\d/g, (d: string) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
|
||||
|
||||
const getButtonLabel = () => {
|
||||
if (buttonState === 'sent') return 'ارسال شد!';
|
||||
if (buttonState === 'counting') {
|
||||
const minutes = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||
const seconds = String(countdown % 60).padStart(2, '0');
|
||||
const time = `${minutes}:${seconds}`;
|
||||
return toPersianDigits(time);
|
||||
}
|
||||
return 'ارسال کد تایید';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
dir="rtl"
|
||||
style={{
|
||||
backgroundColor: '#F5F5F5',
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '500px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: 2,
|
||||
padding: 5,
|
||||
margin: '40px auto',
|
||||
boxShadow: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexDirection: 'column', mb: 2 }}>
|
||||
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||
تکمیل اطلاعات حساب کاربری
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'gray', fontSize: '14px' }}>
|
||||
اطلاعات کسب و کار خود را وارد کنید
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="نام"
|
||||
placeholder="نام"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="نام خانوادگی"
|
||||
placeholder="نام خانوادگی"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<FormControl sx={{ width: '330px' }}>
|
||||
<InputLabel id="sex-label">جنسیت</InputLabel>
|
||||
<Select
|
||||
labelId="sex-label"
|
||||
id="sex"
|
||||
value={sex}
|
||||
label="جنسیت"
|
||||
onChange={handleChange}
|
||||
sx={{
|
||||
height: '45px',
|
||||
'& .MuiSelect-select': {
|
||||
paddingY: '10px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="female">زن</MenuItem>
|
||||
<MenuItem value="male">مرد</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="کدملی(اختیاری)"
|
||||
placeholder="کدملی(اختیاری)"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<FormGroup>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
|
||||
<Switch
|
||||
checked={showPassword}
|
||||
onChange={handleTogglePassword}
|
||||
sx={{
|
||||
transform: 'scaleX(-1)',
|
||||
'& .MuiSwitch-thumb': {
|
||||
transform: 'scaleX(-1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography> تعیین رمز عبور</Typography>
|
||||
</Box>
|
||||
</FormGroup>
|
||||
{showPassword && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', width: '330px' }}
|
||||
>
|
||||
<TextField
|
||||
label="رمز عبور"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{password && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: hasNumber ? 'green' : 'red' }}
|
||||
>
|
||||
شامل عدد
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: hasMinLength ? 'green' : 'red' }}
|
||||
>
|
||||
حداقل 8 کاراکتر
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: hasUpperAndLower ? 'green' : 'red' }}
|
||||
>
|
||||
شامل یک حرف بزرگ و کوچک
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ color: hasSpecialChar ? 'green' : 'red' }}
|
||||
>
|
||||
شامل علامت(!@#$%^&*)
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{showPassword && (
|
||||
<TextField
|
||||
label="تکرار رمز عبور"
|
||||
variant="outlined"
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? 'مطابقت ندارد'
|
||||
: ' '
|
||||
}
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
|
||||
<Switch
|
||||
checked={showEmail}
|
||||
onChange={handleToggleEmail}
|
||||
sx={{
|
||||
transform: 'scaleX(-1)',
|
||||
'& .MuiSwitch-thumb': {
|
||||
transform: 'scaleX(-1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Typography> اتصال ایمیل خود</Typography>
|
||||
</Box>
|
||||
</FormGroup>
|
||||
{showEmail && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<TextField
|
||||
label="ایمیل"
|
||||
variant="outlined"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{email && (
|
||||
<Typography sx={{ color: correctEmail ? 'green' : 'red' }}>
|
||||
فرم درست ایمیل وارد کنید
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={onClickCodeSent}
|
||||
sx={{ width: '200px' }}
|
||||
disabled={buttonState === 'sent' || buttonState === 'counting'}
|
||||
>
|
||||
{getButtonLabel()}
|
||||
</Button>
|
||||
</Box>
|
||||
{codeSent && (
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="کد تایید"
|
||||
variant="outlined"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
sx={{
|
||||
width: '330px',
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: 45,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{
|
||||
width: '150px',
|
||||
backgroundColor: 'white',
|
||||
border: 0.5,
|
||||
borderColor: '#1976d2',
|
||||
color: '#1976d2',
|
||||
}}
|
||||
>
|
||||
بررسی کد
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Typography variant="body2" sx={{ flex: 1 }}>
|
||||
ادامه فرایند ثبت نام به منزله تایید و قبول{' '}
|
||||
<Link href="" target="_blank" rel="">
|
||||
قوانین و مقررات هارمونی
|
||||
</Link>{' '}
|
||||
می باشد.
|
||||
</Typography>
|
||||
<Button variant="contained" sx={{ width: '250px', height: '40px' }}>
|
||||
تایید و ثبت نام
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
129
src/features/profile/components/PersonalInformation.tsx
Normal file
129
src/features/profile/components/PersonalInformation.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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: 'محمد حسین',
|
||||
lastName: 'برزهگر',
|
||||
country: 'قطر',
|
||||
gender: 'مرد',
|
||||
nationalCode: '',
|
||||
};
|
||||
const [data, setData] = useState(initialData);
|
||||
|
||||
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
|
||||
|
||||
const toggleEdit = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
if (isEditing) {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
gender: gender === 'male' ? 'مرد' : gender === 'female' ? 'زن' : '',
|
||||
}));
|
||||
} else {
|
||||
setGender(
|
||||
data.gender === 'مرد' ? 'male' : data.gender === 'زن' ? 'female' : '',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
title={t('settingForm.titlePersonalInfo')}
|
||||
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.85rem', sm: '1rem' },
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '4px',
|
||||
bgcolor: isEditing ? 'primary.main' : 'background.paper',
|
||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
||||
px: { xs: 2, sm: '22px' },
|
||||
py: { xs: '6px', sm: '8px' },
|
||||
width: { xs: '100%', sm: isEditing ? '85px' : '93px' },
|
||||
fontSize: { xs: '0.9rem', sm: '1rem' },
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settingForm.saveButton')
|
||||
: t('settingForm.editButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
{isEditing && (
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEditing ? (
|
||||
<InfoRowEdit
|
||||
data={data}
|
||||
setData={setData}
|
||||
gender={gender}
|
||||
setGender={setGender}
|
||||
/>
|
||||
) : (
|
||||
<InfoRowDisplay
|
||||
data={data}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
initials={initials}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
286
src/features/profile/components/PhoneNumber.tsx
Normal file
286
src/features/profile/components/PhoneNumber.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import React, { useState, type ChangeEvent } from 'react';
|
||||
import { Box, Typography, Button, TextField, IconButton } from '@mui/material';
|
||||
import { Edit, Refresh, TickCircle } from 'iconsax-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { Toast } from '@/components/Toast';
|
||||
|
||||
export function PhoneNumber() {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
||||
'default',
|
||||
);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
|
||||
const [phones, setPhone] = useState([
|
||||
{ phone: '09123456789', time: '۱ ماه پیش', withCode: '+989123456789' },
|
||||
]);
|
||||
|
||||
const toggleEdit = () => {
|
||||
setIsEditing((prev) => {
|
||||
const enteringEditMode = !prev;
|
||||
if (enteringEditMode) {
|
||||
setPhoneNumber('');
|
||||
setVerificationCode('');
|
||||
setIsVerified(false);
|
||||
setButtonState('default');
|
||||
setShowToast(false);
|
||||
}
|
||||
return enteringEditMode;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhoneNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setPhoneNumber(value);
|
||||
};
|
||||
|
||||
const handleVerificationCodeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setVerificationCode(value);
|
||||
};
|
||||
|
||||
const handleSendCode = () => {
|
||||
if (!phoneNumber) return;
|
||||
setButtonState('counting');
|
||||
setIsVerified(false);
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
setIsVerifying(true);
|
||||
setTimeout(() => {
|
||||
setIsVerifying(false);
|
||||
setIsVerified(true);
|
||||
setShowToast(true);
|
||||
const newPhone = '+98' + phoneNumber.slice(1);
|
||||
setPhone([
|
||||
{ phone: phoneNumber, time: 'چند ثانیه پیش', withCode: newPhone },
|
||||
]);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleVerifyClick = () => {
|
||||
if (!verificationCode || isVerifying) return;
|
||||
handleVerifyCode();
|
||||
setTimeout(() => setShowToast(true), 1600);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
title={t('settingForm.titlePhoneNumber')}
|
||||
subtitle={t('settingForm.descriptionPhoneNumber')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: '43px',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '4px',
|
||||
bgcolor: isEditing ? 'primary.main' : 'background.paper',
|
||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
||||
width: { xs: '100%', sm: isEditing ? '85px' : '161px' },
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settingForm.saveButton')
|
||||
: t('settingForm.editPhoneNumber')}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{isEditing ? (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 }, bgcolor: 'background.paper' }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6">
|
||||
{t('settingForm.editPhoneNumber')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.phoneNumberText')}({phones.map((p) => p.withCode)}
|
||||
){t('settingForm.verb')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="phoneNumber"
|
||||
label={t('settingForm.newPhoneNumber')}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={handlePhoneNumberChange}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder="09123456789"
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setPhoneNumber('');
|
||||
}}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Edit size="24" color="#64B5F6" />
|
||||
</IconButton>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isVerified ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TickCircle
|
||||
size="24"
|
||||
style={{ color: '#43A047' }}
|
||||
variant="Bold"
|
||||
/>
|
||||
<Typography sx={{ color: 'success.main' }}>
|
||||
{t('settingForm.successButton')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleSendCode}
|
||||
disabled={
|
||||
buttonState === 'counting' || phoneNumber.length === 0
|
||||
}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
color: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{buttonState === 'counting' ? (
|
||||
<CountDownTimer
|
||||
initialSeconds={60}
|
||||
onComplete={() => setButtonState('default')}
|
||||
/>
|
||||
) : (
|
||||
t('settingForm.verificationCodeButton')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{buttonState === 'counting' && !isVerified && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="verificationCode"
|
||||
label={t('settingForm.verificationCode')}
|
||||
type="tel"
|
||||
value={verificationCode}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
bgcolor: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{isVerifying ? (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Refresh size="20" color="white" />
|
||||
</Box>
|
||||
) : (
|
||||
t('settingForm.checkCode')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showToast}
|
||||
onClose={() => setShowToast(false)}
|
||||
>
|
||||
{t('settingForm.successfulChangePhone')}
|
||||
</Toast>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 } }}>
|
||||
{phones.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
py: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.primary">
|
||||
{item.phone}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
331
src/features/profile/components/SocialMedia.tsx
Normal file
331
src/features/profile/components/SocialMedia.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
TextField,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
|
||||
import {
|
||||
Google,
|
||||
Apple,
|
||||
Sms,
|
||||
Trash,
|
||||
CloseSquare,
|
||||
Message,
|
||||
ArrowDown3,
|
||||
} from 'iconsax-react';
|
||||
|
||||
export function SocialMedia() {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
const openMenu = Boolean(anchor);
|
||||
const fullScreen = useMediaQuery((theme: Theme) =>
|
||||
theme.breakpoints.down('sm'),
|
||||
);
|
||||
|
||||
const handleOpenDialog = () => setOpenDialog(true);
|
||||
const handleCloseDialog = () => setOpenDialog(false);
|
||||
|
||||
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchor(e.currentTarget);
|
||||
const handleCloseMenu = () => setAnchor(null);
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmailInput(value);
|
||||
setEmailError(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
|
||||
};
|
||||
|
||||
const emailList = [
|
||||
{ email: 'emailtemp@email.com', provider: 'email', time: '1 ماه پیش' },
|
||||
{ email: 'emailtemp@gmail.com', provider: 'google', time: '1 ماه پیش' },
|
||||
{ email: 'emailtemp@icloud.com', provider: 'apple', time: '1 ماه پیش' },
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
title={t('settingForm.titleSocial')}
|
||||
subtitle={t('settingForm.descriptionSocial')}
|
||||
action={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'stretch', sm: 'flex-start' },
|
||||
gap: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClickMenu}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: { sm: 187 },
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '8px',
|
||||
color: 'primary.main',
|
||||
fontSize: '14px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
direction: 'rtl',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.addEmailOrSocialButton')}
|
||||
</Box>
|
||||
<ArrowDown3 size="20" color="#2979FF" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={openMenu}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 240,
|
||||
maxWidth: '90vw',
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
handleOpenDialog();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Message size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.email')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Google size={20} color="#4285F4" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.google')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Apple size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.apple')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box sx={{ width: '100%', borderRadius: '8px', p: { xs: 1, sm: 2 } }}>
|
||||
{emailList.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
py: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{item.provider === 'google' && (
|
||||
<Google size="20" variant="Bold" color="#4285F4" />
|
||||
)}
|
||||
{item.provider === 'apple' && (
|
||||
<Apple size="20" variant="Bold" color="black" />
|
||||
)}
|
||||
{item.provider === 'email' && (
|
||||
<Sms size="20" variant="Bold" color="#1976d2" />
|
||||
)}
|
||||
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="h6" noWrap>
|
||||
{item.email}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton size="small">
|
||||
<Trash size="20" color="gray" variant="Outline" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
fullScreen={fullScreen}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': { m: { xs: 1, sm: 'auto' }, borderRadius: 2 },
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handleCloseDialog} size="small">
|
||||
<CloseSquare size="24" color="#F5F5F5" />
|
||||
</IconButton>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight="bold">
|
||||
{t('settingForm.newEmail')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.dialogHeader')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={handleEmailChange}
|
||||
error={emailError}
|
||||
helperText={emailError ? t('settingForm.emailError') : ''}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '8px' } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ textTransform: 'none', borderRadius: '8px' }}
|
||||
disabled={emailError || emailInput === ''}
|
||||
>
|
||||
{t('settingForm.verificationCodeButton')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: '#ccc' }} />
|
||||
<Typography sx={{ mx: 1, fontSize: '12px', color: 'gray' }}>
|
||||
{t('settingForm.or')}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: '#ccc' }} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: '#1976d2',
|
||||
color: '#1976d2',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Google
|
||||
size="20"
|
||||
color="#4285F4"
|
||||
style={{ marginInlineStart: 8 }}
|
||||
/>
|
||||
{t('settingForm.google')}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: '#1976d2',
|
||||
color: '#1976d2',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Apple
|
||||
size="20"
|
||||
color="black"
|
||||
style={{ marginInlineStart: 8 }}
|
||||
/>
|
||||
{t('settingForm.apple')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
14
src/features/profile/components/UserForm.tsx
Normal file
14
src/features/profile/components/UserForm.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { PersonalInformation } from './PersonalInformation';
|
||||
import { PhoneNumber } from './PhoneNumber';
|
||||
import { SocialMedia } from './SocialMedia';
|
||||
|
||||
export function UserForm() {
|
||||
return (
|
||||
<Box sx={{ width: '100%', overflowX: 'clip' }}>
|
||||
<PersonalInformation />
|
||||
<PhoneNumber />
|
||||
<SocialMedia />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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={{ px: 2, mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
flexWrap: 'wrap',
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
gap: 2,
|
||||
justifyContent: 'space-between',
|
||||
width: '690px',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '337px' }}
|
||||
>
|
||||
<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 sx={{ display: 'flex', flexDirection: 'column', width: '337px' }}>
|
||||
<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,
|
||||
mt: 2,
|
||||
width: '690px',
|
||||
}}
|
||||
>
|
||||
<DisplayField
|
||||
label={t('settingForm.gender')}
|
||||
value={displayValue(data.gender)}
|
||||
/>
|
||||
<DisplayField
|
||||
label={t('settingForm.nationalCode')}
|
||||
value={displayValue(data.nationalCode)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user