Merge pull request #41 from rkheftan/fix/email-section
Fix/email section
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@mui/x-data-grid-premium": "^8.10.0",
|
||||
"@mui/x-date-pickers": "^8.10.0",
|
||||
"@rkheftan/harmony-ui": "^0.2.89",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"axios": "^1.11.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-jalali": "^4.0.0-0",
|
||||
@@ -1962,9 +1962,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
@@ -2228,7 +2226,9 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@mui/x-data-grid-premium": "^8.10.0",
|
||||
"@mui/x-date-pickers": "^8.10.0",
|
||||
"@rkheftan/harmony-ui": "^0.2.89",
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.46.3",
|
||||
"@rollup/rollup-darwin-arm64": "^4.46.3",
|
||||
"axios": "^1.11.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-jalali": "^4.0.0-0",
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
"errorConfirmCode": "Failed to confirm code",
|
||||
"errorChangePhone": "Failed to change phone number",
|
||||
"verificationCodeSent": "Verification code sent",
|
||||
"onlyOneAccountAllowed": "You can only link one email account."
|
||||
"onlyOneAccountAllowed": "You can only link one email account.",
|
||||
"editEmailOrGoogle": "Change Email / Google",
|
||||
"emailText": "Your new email will replace your previous email (<1>{{email}}</1>)"
|
||||
},
|
||||
"active": {
|
||||
"activeDevices": "Active devices",
|
||||
@@ -147,4 +149,4 @@
|
||||
"description": "Encourage repeat purchases by sending discount codes and points to your customers."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
"errorConfirmCode": "تایید کد با خطا مواجه شد",
|
||||
"errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد",
|
||||
"verificationCodeSent": "کد تایید ارسال شد",
|
||||
"onlyOneAccountAllowed": "شما فقط میتوانید یک حساب ایمیل را متصل کنید"
|
||||
"onlyOneAccountAllowed": "شما فقط میتوانید یک حساب ایمیل را متصل کنید",
|
||||
"editEmailOrGoogle": "تغییر ایمیل / گوگل",
|
||||
"emailText": "ایمیل جدید شما جایگزین ایمل قبلی ({{email}}) خواهد شد"
|
||||
},
|
||||
"active": {
|
||||
"activeDevices": "نشست های فعال",
|
||||
@@ -147,4 +149,4 @@
|
||||
"description": "با ارسال کد تخفیف و امتیاز به مشتریان کسب و کارتان آنها را به خرید مجدد ترغیب کنید"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
|
||||
@@ -14,27 +14,34 @@ export function CountDownTimer({
|
||||
const { i18n } = useTranslation();
|
||||
const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
|
||||
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
useEffect(() => {
|
||||
onCompleteRef.current = onComplete;
|
||||
}, [onComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
setSecondsLeft(initialSeconds);
|
||||
}, [initialSeconds]);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
useEffect(() => {
|
||||
if (secondsLeft <= 0) return;
|
||||
const id = setInterval(() => {
|
||||
setSecondsLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer);
|
||||
onComplete?.();
|
||||
clearInterval(id);
|
||||
onCompleteRef.current?.();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [initialSeconds, onComplete]);
|
||||
return () => clearInterval(id);
|
||||
}, [secondsLeft]);
|
||||
|
||||
const formatTime = (totalSeconds: number) => {
|
||||
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${minutes}:${seconds}`, i18n.language);
|
||||
const m = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||
const s = String(totalSeconds % 60).padStart(2, '0');
|
||||
return toLocaleDigits(`${m}:${s}`, i18n.language);
|
||||
};
|
||||
|
||||
return <span>{formatTime(secondsLeft)}</span>;
|
||||
|
||||
@@ -92,7 +92,7 @@ export function PasswordDialog({
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -121,7 +121,7 @@ export function PasswordDialog({
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
@@ -169,7 +169,7 @@ export function PasswordDialog({
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 } }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { Box, CircularProgress, Button } from '@mui/material';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import SocialMediaList from './socialMedia/SocialMediaList';
|
||||
import SocialMediaMenu from './socialMedia/SocialMediaMenu';
|
||||
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
import SocialEditForm from './socialMedia/SocialEditForm';
|
||||
import SocialMediaMenu, {
|
||||
type SocialChoice,
|
||||
} from './socialMedia/SocialMediaMenu';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import {
|
||||
sendEmailCode,
|
||||
@@ -20,51 +20,46 @@ import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function SocialMedia() {
|
||||
const { t } = useTranslation('setting');
|
||||
const toast = useToast();
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [dialogStep, setDialogStep] = useState<'enterEmail' | 'enterCode'>(
|
||||
'enterEmail',
|
||||
const [email, setEmail] = useState<EmailAccount[]>([]);
|
||||
const [emailError, setEmailError] = useState<string | undefined>();
|
||||
const [verificationCodeError, setVerificationCodeError] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [isCodeSent, setIsCodeSent] = useState(false);
|
||||
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
||||
'default',
|
||||
);
|
||||
const [emailList, setEmailList] = useState<EmailAccount[]>([]);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const toast = useToast();
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const { loading: isSendingCode, execute: executeSendCode } =
|
||||
useApi(sendEmailCode);
|
||||
|
||||
const { loading: isConfirming, execute: executeConfirmCode } =
|
||||
useApi(confirmEmailCode);
|
||||
|
||||
const { loading: isChangingEmail, execute: executeChangeEmail } =
|
||||
useApi(changeEmail);
|
||||
|
||||
const fullScreen = useMediaQuery((theme: Theme) =>
|
||||
theme?.breakpoints.down('sm'),
|
||||
const isBusy = useMemo(
|
||||
() => isSendingCode || isConfirming || isChangingEmail,
|
||||
[isSendingCode, isConfirming, isChangingEmail],
|
||||
);
|
||||
const downMd = useMediaQuery((theme: Theme) => theme?.breakpoints.down('md'));
|
||||
const computedMaxWidth = (fullScreen ? 'xs' : downMd ? 'sm' : 'md') as
|
||||
| 'xs'
|
||||
| 'sm'
|
||||
| 'md'
|
||||
| 'lg'
|
||||
| 'xl';
|
||||
|
||||
const isSubmitting = isConfirming || isChangingEmail;
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData?.success) {
|
||||
const { email } = profileData;
|
||||
|
||||
if (!email) return;
|
||||
|
||||
setEmailList([
|
||||
setEmail([
|
||||
{
|
||||
email,
|
||||
provider: email.includes('@gmail.') ? 'google' : 'email',
|
||||
@@ -72,33 +67,48 @@ export function SocialMedia() {
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
toast({
|
||||
message: profileData.message,
|
||||
severity: 'error',
|
||||
});
|
||||
toast({ message: profileData.message, severity: 'error' });
|
||||
}
|
||||
};
|
||||
void loadProfile();
|
||||
}, [refetchProfile, toast]);
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const resetDialog = () => {
|
||||
setOpenDialog(false);
|
||||
const resetState = () => {
|
||||
setEmailInput('');
|
||||
setVerificationCode('');
|
||||
setFormError(null);
|
||||
setDialogStep('enterEmail');
|
||||
setEmailError(undefined);
|
||||
setVerificationCodeError(undefined);
|
||||
setIsCodeSent(false);
|
||||
setButtonState('default');
|
||||
setIsVerified(false);
|
||||
};
|
||||
|
||||
const startEmailEdit = () => {
|
||||
resetState();
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const buttonLabel = email.length
|
||||
? t('settingForm.editEmailOrGoogle')
|
||||
: t('settingForm.addEmailOrSocialButton');
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
if (!value) {
|
||||
setEmailError(t('settingForm.thisFieldIsRequired'));
|
||||
return false;
|
||||
}
|
||||
const ok = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
|
||||
if (!ok) {
|
||||
setEmailError(t('settingForm.emailIsInvalid'));
|
||||
return false;
|
||||
}
|
||||
setEmailError(undefined);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSendCode = async () => {
|
||||
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(emailInput)) {
|
||||
setFormError(t('settingForm.emailIsInvalid'));
|
||||
return;
|
||||
}
|
||||
setFormError(null);
|
||||
if (!validateEmail(emailInput)) return;
|
||||
const sendCodeData = await executeSendCode({ email: emailInput });
|
||||
|
||||
if (!sendCodeData) return;
|
||||
|
||||
if (sendCodeData.success) {
|
||||
@@ -106,28 +116,27 @@ export function SocialMedia() {
|
||||
message: t('settingForm.verificationCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setDialogStep('enterCode');
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
resetDialog();
|
||||
},
|
||||
3 * 60 * 1000,
|
||||
);
|
||||
setIsCodeSent(true);
|
||||
setButtonState('counting');
|
||||
} else {
|
||||
toast({
|
||||
message: sendCodeData.message || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmAndChangeEmail = async () => {
|
||||
if (verificationCode.length < 4) {
|
||||
setFormError(t('settingForm.verificationCodeRequired'));
|
||||
const handleVerifyClick = async () => {
|
||||
if (!verificationCode || verificationCode.length < 4) {
|
||||
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
|
||||
return;
|
||||
}
|
||||
setFormError(null);
|
||||
setVerificationCodeError(undefined);
|
||||
|
||||
const confirmData = await executeConfirmCode({
|
||||
email: emailInput,
|
||||
verifyCode: verificationCode,
|
||||
});
|
||||
|
||||
if (!confirmData) return;
|
||||
|
||||
if (!confirmData.success || !confirmData.confirm) {
|
||||
@@ -138,43 +147,111 @@ export function SocialMedia() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailList.length >= 1) {
|
||||
toast({
|
||||
message: t('settingForm.onlyOneAccountAllowed'),
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const changeEmailData = await executeChangeEmail({ email: emailInput });
|
||||
if (!changeEmailData) return;
|
||||
|
||||
if (changeEmailData.success) {
|
||||
setEmailList([
|
||||
setEmail([
|
||||
{
|
||||
email: emailInput,
|
||||
provider: emailInput.includes('@gmail.') ? 'google' : 'email',
|
||||
time: t('settingForm.justNow'),
|
||||
},
|
||||
]);
|
||||
|
||||
setIsVerified(true);
|
||||
toast({
|
||||
message: t('settingForm.emailChangedSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
resetDialog();
|
||||
setIsEditing(false);
|
||||
refetchProfile({ force: true });
|
||||
} else {
|
||||
toast({
|
||||
message: changeEmailData.message || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (field: 'email' | 'verificationCode') => {
|
||||
if (field === 'email') validateEmail(emailInput);
|
||||
if (field === 'verificationCode') {
|
||||
if (!verificationCode || verificationCode.length < 4) {
|
||||
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
|
||||
} else {
|
||||
setVerificationCodeError(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMenuSelect = (choice: SocialChoice) => {
|
||||
if (choice === 'email') {
|
||||
startEmailEdit();
|
||||
return;
|
||||
}
|
||||
if (choice === 'google') {
|
||||
toast({
|
||||
message: t('settingForm.googleLinkComingSoon'),
|
||||
severity: 'info',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rejectButton = () => {
|
||||
setIsEditing(false);
|
||||
setIsCodeSent(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<CardContainer
|
||||
title={t('settingForm.titleSocial')}
|
||||
subtitle={t('settingForm.descriptionSocial')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<SocialMediaMenu t={t} onOpenDialog={() => setOpenDialog(true)} />
|
||||
isEditing ? (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text"
|
||||
onClick={rejectButton}
|
||||
size="large"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="emailForm"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
bgcolor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : (
|
||||
t('settingForm.saveButton')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<SocialMediaMenu
|
||||
t={t}
|
||||
onSelect={onMenuSelect}
|
||||
buttonLabel={buttonLabel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{isLoadingProfile ? (
|
||||
@@ -184,30 +261,45 @@ export function SocialMedia() {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 4,
|
||||
minHeight: '100px',
|
||||
minHeight: '150px',
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : isEditing ? (
|
||||
<Box
|
||||
component="form"
|
||||
id="emailForm"
|
||||
noValidate
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isBusy) void handleVerifyClick();
|
||||
}}
|
||||
>
|
||||
<SocialEditForm
|
||||
email={emailInput}
|
||||
setEmail={setEmailInput}
|
||||
emails={email.map((e) => ({ address: e.email }))}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
isVerified={isVerified}
|
||||
isVerifying={isBusy}
|
||||
isSendingCode={isSendingCode}
|
||||
isCodeSent={isCodeSent}
|
||||
setIsCodeSent={setIsCodeSent}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyClick={handleVerifyClick}
|
||||
handleBlur={handleBlur}
|
||||
emailError={emailError}
|
||||
verificationCodeError={verificationCodeError}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<SocialMediaList t={t} emailList={emailList} />
|
||||
<SocialMediaList emailList={email} />
|
||||
)}
|
||||
<SocialMediaDialog
|
||||
open={openDialog}
|
||||
onClose={resetDialog}
|
||||
t={t}
|
||||
emailInput={emailInput}
|
||||
setEmailInput={setEmailInput}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
apiError={formError}
|
||||
isLoading={isSendingCode || isConfirming || isChangingEmail}
|
||||
dialogStep={dialogStep}
|
||||
onSendCode={handleSendCode}
|
||||
onConfirmEmail={handleConfirmAndChangeEmail}
|
||||
fullScreen={fullScreen}
|
||||
computedMaxWidth={computedMaxWidth}
|
||||
/>
|
||||
</CardContainer>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import { Edit2, TickCircle } from 'iconsax-react';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type SocialEditFormProps = {
|
||||
email: string;
|
||||
setEmail: (v: string) => void;
|
||||
emails: { address: string }[];
|
||||
verificationCode: string;
|
||||
setVerificationCode: (v: string) => void;
|
||||
isVerified: boolean;
|
||||
isVerifying: boolean;
|
||||
isSendingCode: boolean;
|
||||
isCodeSent: boolean;
|
||||
setIsCodeSent: (v: boolean) => void;
|
||||
buttonState: 'default' | 'counting';
|
||||
setButtonState: (v: 'default' | 'counting') => void;
|
||||
handleSendCode: () => void;
|
||||
handleVerifyClick: () => void;
|
||||
handleBlur: (field: 'email' | 'verificationCode') => void;
|
||||
emailError?: string;
|
||||
verificationCodeError?: string;
|
||||
};
|
||||
|
||||
export default function SocialEditForm({
|
||||
email,
|
||||
setEmail,
|
||||
emails,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
isVerified,
|
||||
isVerifying,
|
||||
isSendingCode,
|
||||
isCodeSent,
|
||||
setIsCodeSent,
|
||||
buttonState,
|
||||
setButtonState,
|
||||
handleSendCode,
|
||||
handleVerifyClick,
|
||||
handleBlur,
|
||||
emailError,
|
||||
verificationCodeError,
|
||||
}: SocialEditFormProps) {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6">
|
||||
{t('settingForm.editEmailOrGoogle')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.emailText', {
|
||||
email: emails.map((e) => e.address).join(', '),
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
pb: 2,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="email"
|
||||
label={t('settingForm.newEmail')}
|
||||
type="email"
|
||||
value={email}
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
onBlur={() => handleBlur('email')}
|
||||
error={!!emailError}
|
||||
helperText={emailError}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
disabled={isCodeSent}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: isCodeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setVerificationCode('');
|
||||
setIsCodeSent(false);
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : undefined,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{isVerified ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
color: 'success.main',
|
||||
}}
|
||||
>
|
||||
<Icon Component={TickCircle} />
|
||||
<Typography>{t('settingForm.successButton')}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
loading={isSendingCode}
|
||||
onClick={handleSendCode}
|
||||
sx={{ color: 'primary.main', width: { xs: '100%', sm: 208 } }}
|
||||
disabled={buttonState === 'counting'}
|
||||
>
|
||||
{buttonState === 'counting' ? (
|
||||
<CountDownTimer
|
||||
initialSeconds={60}
|
||||
onComplete={() => setButtonState('default')}
|
||||
/>
|
||||
) : (
|
||||
t('settingForm.verificationCodeButton')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isCodeSent && !isSendingCode && !isVerified && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
mb: 2,
|
||||
pt: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="verificationCode"
|
||||
label={t('settingForm.verificationCode')}
|
||||
type="tel"
|
||||
value={verificationCode}
|
||||
error={!!verificationCodeError}
|
||||
helperText={verificationCodeError}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
onChange={(e) =>
|
||||
setVerificationCode(e.target.value.replace(/\D/g, ''))
|
||||
}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{ bgcolor: 'primary.main', width: { xs: '100%', sm: 200 } }}
|
||||
>
|
||||
{isVerifying ? (
|
||||
<CircularProgress size={20} />
|
||||
) : (
|
||||
t('settingForm.checkCode')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import React, { type ReactElement, type ElementType, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import type { TransitionProps } from '@mui/material/transitions';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type SocialMediaDialogProps } from '@/features/profile/types/settingsType';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
|
||||
const MobileSlide = React.forwardRef(function MobileSlide(
|
||||
props: TransitionProps & { children: ReactElement<unknown, ElementType> },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export default function SocialMediaDialog({
|
||||
open,
|
||||
onClose,
|
||||
t,
|
||||
emailInput,
|
||||
setEmailInput,
|
||||
fullScreen,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
isLoading,
|
||||
dialogStep,
|
||||
onSendCode,
|
||||
onConfirmEmail,
|
||||
}: SocialMediaDialogProps) {
|
||||
const [emailError, setEmailError] = useState<string | undefined>();
|
||||
const [touched, setTouched] = useState(false);
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
if (!value) {
|
||||
setEmailError(t('settingForm.thisFieldIsRequired'));
|
||||
return false;
|
||||
}
|
||||
if (!isEmail(value)) {
|
||||
setEmailError(t('settingForm.emailIsInvalid'));
|
||||
return false;
|
||||
}
|
||||
setEmailError(undefined);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isLoading) return;
|
||||
|
||||
if (dialogStep === 'enterEmail') {
|
||||
setTouched(true);
|
||||
if (!validateEmail(emailInput)) return;
|
||||
onSendCode();
|
||||
} else {
|
||||
onConfirmEmail();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
maxWidth="xs"
|
||||
scroll="body"
|
||||
keepMounted
|
||||
TransitionComponent={fullScreen ? MobileSlide : undefined}
|
||||
PaperProps={{ sx: { mx: 1 } }}
|
||||
>
|
||||
<DialogTitle sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton onClick={onClose} disabled={isLoading}>
|
||||
<Icon Component={CloseCircle} size="large" color="primary.main" />
|
||||
</IconButton>
|
||||
<Box component="span" fontWeight="bold" fontSize={18}>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<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={(e) => {
|
||||
setEmailInput(e.target.value);
|
||||
if (touched) validateEmail(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTouched(true);
|
||||
validateEmail(emailInput);
|
||||
}}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 }, mt: 1 }}
|
||||
autoFocus={dialogStep === 'enterEmail'}
|
||||
disabled={isLoading || dialogStep === 'enterCode'}
|
||||
error={touched && !!emailError}
|
||||
helperText={touched && emailError ? emailError : ' '}
|
||||
/>
|
||||
|
||||
{dialogStep === 'enterCode' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 1 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 1 }}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : dialogStep === 'enterEmail' ? (
|
||||
t('settingForm.verificationCodeButton')
|
||||
) : (
|
||||
t('settingForm.confirmAndSave')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -19,12 +19,7 @@ export default function SocialMediaList({ emailList }: SocialMediaListProps) {
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -37,15 +32,14 @@ export default function SocialMediaList({ emailList }: SocialMediaListProps) {
|
||||
borderRadius: 0.5,
|
||||
}}
|
||||
>
|
||||
{item.provider === 'google' && (
|
||||
{item.provider === 'google' ? (
|
||||
<Icon
|
||||
Component={Google}
|
||||
size="medium"
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
)}
|
||||
{item.provider === 'email' && (
|
||||
) : (
|
||||
<Icon
|
||||
Component={Sms}
|
||||
size="medium"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -7,84 +7,62 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Message, Google, ArrowDown3 } from 'iconsax-react';
|
||||
import { Message, Google, ArrowDown2 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type SocialMediaMenuProps } from '@/features/profile/types/settingsType';
|
||||
|
||||
export default function SocialMediaMenu({
|
||||
t,
|
||||
onOpenDialog,
|
||||
}: SocialMediaMenuProps) {
|
||||
export type SocialChoice = 'email' | 'google';
|
||||
|
||||
type Props = {
|
||||
t: (key: string) => string;
|
||||
onSelect: (choice: SocialChoice) => void;
|
||||
buttonLabel: string;
|
||||
};
|
||||
|
||||
export default function SocialMediaMenu({ t, onSelect, buttonLabel }: Props) {
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
const openMenu = Boolean(anchor);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setOpen(true);
|
||||
setAnchor(e.currentTarget);
|
||||
};
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(false);
|
||||
setAnchor(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-start', gap: 1 }}>
|
||||
<Button
|
||||
onClick={(e) => setAnchor(e.currentTarget)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'stretch', sm: 'flex-start' },
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
gap: 1,
|
||||
}}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={openMenu ? 'true' : undefined}
|
||||
aria-controls={openMenu ? 'social-menu' : undefined}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClickMenu}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
border: '1px solid',
|
||||
borderRadius: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
backgroundColor: open ? 'primary.light' : 'primary.default',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
<Box component="span">{t('settingForm.addEmailOrSocialButton')}</Box>
|
||||
<Icon
|
||||
Component={ArrowDown3}
|
||||
size="small"
|
||||
color="primary.main"
|
||||
variant="Outline"
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
<Box component="span">{buttonLabel}</Box>
|
||||
<Icon
|
||||
Component={ArrowDown2}
|
||||
size="small"
|
||||
color="primary.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
id="social-menu"
|
||||
anchorEl={anchor}
|
||||
open={openMenu}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
width: anchor ? anchor.offsetWidth : 'auto',
|
||||
// maxWidth: '90vw',
|
||||
},
|
||||
}}
|
||||
onClose={() => setAnchor(null)}
|
||||
PaperProps={{ sx: { width: anchor ? anchor.offsetWidth : 'auto' } }}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
onOpenDialog();
|
||||
setAnchor(null);
|
||||
onSelect('email');
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
@@ -92,7 +70,13 @@ export default function SocialMediaMenu({
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.email')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setAnchor(null);
|
||||
onSelect('google');
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Icon Component={Google} size="medium" color="primary.main" />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -69,7 +69,7 @@ export interface SocialMediaDialogProps {
|
||||
}
|
||||
|
||||
export interface SocialMediaListProps {
|
||||
t: (key: string) => string;
|
||||
// t: (key: string) => string;
|
||||
emailList: readonly {
|
||||
email: string;
|
||||
provider: 'email' | 'google' | 'apple';
|
||||
|
||||
Reference in New Issue
Block a user