feat: add using enter instead pushing the button

This commit is contained in:
Koosha Lahouti
2025-09-14 00:02:08 +03:30
parent 9ec1415f70
commit 19bbf55904
17 changed files with 675 additions and 559 deletions

View File

@@ -46,6 +46,12 @@ export function PasswordDialog({
const [showConfirm, setShowConfirm] = useState(false);
const navigate = useNavigate();
const onFormSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
e.stopPropagation();
if (!loading) handleSubmit();
};
return (
<Dialog
open={open}
@@ -69,120 +75,132 @@ export function PasswordDialog({
</Box>
</DialogTitle>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
>
{changePassword && (
<>
<TextField
label={t('securityForm.currentPassword')}
type={showCurrent ? 'text' : 'password'}
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowCurrent(!showCurrent)}>
<Icon Component={showCurrent ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
<Link
sx={{ fontSize: '15px', cursor: 'pointer' }}
onClick={() => navigate('/forget-password')}
>
{t('securityForm.forgetPassword')}
</Link>
</>
)}
<TextField
label={t('securityForm.newPassword')}
type={showNew ? 'text' : 'password'}
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowNew(!showNew)}>
<Icon Component={showNew ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
<Box component="form" onSubmit={onFormSubmit}>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
/>
{showValidation && (
<Box sx={{ mt: 1 }}>
<PasswordValidationItem
isValid={hasNumber}
label={t('securityForm.hasNumber')}
/>
<PasswordValidationItem
isValid={hasMinLength}
label={t('securityForm.hasMinLength')}
/>
<PasswordValidationItem
isValid={hasUpperAndLower}
label={t('securityForm.hasUpperAndLower')}
/>
<PasswordValidationItem
isValid={hasSpecialChar}
label={t('securityForm.hasSpecialChar')}
/>
</Box>
)}
<TextField
label={t('securityForm.confirmPassword')}
type={showConfirm ? 'text' : 'password'}
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('securityForm.notCompatibility')
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowConfirm(!showConfirm)}>
<Icon Component={showConfirm ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
<Button
fullWidth
sx={{ height: 48, textTransform: 'none' }}
variant="contained"
onClick={handleSubmit}
>
{loading ? (
<CircularProgress size={24} color="inherit" />
) : (
t('securityForm.confirm')
{changePassword && (
<>
<TextField
label={t('securityForm.currentPassword')}
type={showCurrent ? 'text' : 'password'}
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowCurrent(!showCurrent)}
type="button"
>
<Icon Component={showCurrent ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
<Link
sx={{ fontSize: '15px', cursor: 'pointer' }}
onClick={() => navigate('/forget-password')}
>
{t('securityForm.forgetPassword')}
</Link>
</>
)}
</Button>
</DialogActions>
<TextField
label={t('securityForm.newPassword')}
type={showNew ? 'text' : 'password'}
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowNew(!showNew)}
type="button"
>
<Icon Component={showNew ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
{showValidation && (
<Box sx={{ mt: 1 }}>
<PasswordValidationItem
isValid={hasNumber}
label={t('securityForm.hasNumber')}
/>
<PasswordValidationItem
isValid={hasMinLength}
label={t('securityForm.hasMinLength')}
/>
<PasswordValidationItem
isValid={hasUpperAndLower}
label={t('securityForm.hasUpperAndLower')}
/>
<PasswordValidationItem
isValid={hasSpecialChar}
label={t('securityForm.hasSpecialChar')}
/>
</Box>
)}
<TextField
label={t('securityForm.confirmPassword')}
type={showConfirm ? 'text' : 'password'}
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('securityForm.notCompatibility')
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowConfirm(!showConfirm)}
type="button"
>
<Icon Component={showConfirm ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
<Button
fullWidth
sx={{ height: 48, textTransform: 'none' }}
variant="contained"
type="submit"
disabled={loading}
>
{loading ? (
<CircularProgress size={24} color="inherit" />
) : (
t('securityForm.confirm')
)}
</Button>
</DialogActions>
</Box>
</Dialog>
);
}

View File

@@ -14,7 +14,6 @@ import { useProfile } from '../../hooks/useProfile';
export function PersonalInformation() {
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
const { t } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false);
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
@@ -29,18 +28,14 @@ export function PersonalInformation() {
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
const showToast = useToast();
const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null);
const { isLoadingProfile, refetchProfile } = useProfile();
const { loading: isSavingProfile, execute: executeSaveProfile } =
useApi(saveProfile);
useEffect(() => {
const loadProfile = async () => {
const profileData = await refetchProfile();
if (!profileData) return;
if (profileData.success) {
const fetchedData = {
firstName: profileData.firstName ?? '',
@@ -66,7 +61,6 @@ export function PersonalInformation() {
});
}
};
loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -85,14 +79,11 @@ export function PersonalInformation() {
if (!data) return;
const isValid = infoRowEditRef.current?.validateFields?.();
if (!isValid) return;
const saveData = await executeSaveProfile({
data,
imageUrl: uploadedImageFile,
});
if (!saveData) return;
if (saveData?.success) {
showToast({
message: t('settingForm.successSaveProfile'),
@@ -101,7 +92,6 @@ export function PersonalInformation() {
setIsEditing(false);
setOriginalData(data);
setUploadedImageFile(null);
// Force a refetch to update the cache with the new data
refetchProfile({ force: true });
} else {
showToast({
@@ -122,6 +112,7 @@ export function PersonalInformation() {
{isEditing ? (
<>
<Button
type="button"
variant="text"
onClick={handleCancelClick}
size="large"
@@ -134,9 +125,11 @@ export function PersonalInformation() {
{t('settingForm.rejectButton')}
</Button>
<Button
onClick={handleSaveClick}
type="submit"
form="profileForm"
size="large"
variant="contained"
disabled={isSavingProfile}
sx={{
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
@@ -174,6 +167,42 @@ export function PersonalInformation() {
>
<CircularProgress />
</Box>
) : isEditing ? (
<Box
component="form"
id="profileForm"
noValidate
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
if (!isSavingProfile) void handleSaveClick();
}}
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
setUploadedImageFile(file);
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}}
onRemoveImage={() => {
setUploadedImageFile(null);
setUploadedImageUrl(null);
}}
/>
<InfoRowEdit ref={infoRowEditRef} data={data} setData={setData} />
</Box>
) : (
<Box
sx={{
@@ -185,37 +214,11 @@ export function PersonalInformation() {
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
setUploadedImageFile(file);
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}}
onRemoveImage={() => {
setUploadedImageFile(null);
setUploadedImageUrl(null);
}}
/>
)}
{data &&
(isEditing ? (
<InfoRowEdit
ref={infoRowEditRef}
data={data}
setData={setData}
/>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
))}
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
</Box>
)}
</CardContainer>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper';
@@ -40,20 +40,21 @@ export function PhoneNumber() {
const { loading: isSendingCode, execute: executeSendCode } =
useApi(sendVerificationCode);
const { loading: isVerifying, execute: executeConfirmCode } = useApi(
confirmPhoneNumberCode,
);
const { loading: isChangingPhone, execute: executeChangePhone } =
useApi(changePhoneNumber);
const isBusy = useMemo(
() => isVerifying || isSendingCode || isChangingPhone,
[isVerifying, isSendingCode, isChangingPhone],
);
useEffect(() => {
const loadProfile = async () => {
const profileData = await refetchProfile();
if (!profileData) return;
if (profileData?.success) {
setPhones([
{
@@ -69,36 +70,32 @@ export function PhoneNumber() {
});
}
};
loadProfile();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
void loadProfile();
}, [refetchProfile, toast]);
useEffect(() => {
if (!phoneNumberTouched) return;
if (!phoneNumber) {
setPhoneNumberError(t('settingForm.thisFieldIsRequired'));
return;
}
if (!isPhoneValid(countryCode, phoneNumber)) {
const parsed = parsePhoneNumberFromString(countryCode + phoneNumber);
const valid = Boolean(parsed?.isValid());
if (!valid) {
setPhoneNumberError(t('settingForm.phoneNumberIsInvalid'));
} else {
setPhoneNumberError(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [countryCode, phoneNumber, phoneNumberTouched]);
}, [countryCode, phoneNumber, phoneNumberTouched, t]);
useEffect(() => {
if (!verificationCodeTouched) return;
if (!verificationCode) {
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
return;
}
setVerificationCodeError(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [verificationCode, verificationCodeTouched]);
const isPhoneValid = (code: string, phone: string) => {
const phoneNum = parsePhoneNumberFromString(code + phone);
return phoneNum?.isValid();
};
}, [verificationCode, verificationCodeTouched, t]);
const handleBlur = (key: string) => {
if (key === 'phoneNumber') {
@@ -126,14 +123,11 @@ export function PhoneNumber() {
const handleSendCode = async () => {
setPhoneNumberTouched(true);
if (phoneNumberError) return;
if (!phoneNumber || phoneNumberError) return;
const result = await executeSendCode({
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
});
if (!result) return;
if (result?.success) {
setButtonState('counting');
setIsVerified(false);
@@ -151,18 +145,13 @@ export function PhoneNumber() {
const handleVerifyCode = async () => {
setVerificationCodeTouched(true);
if (verificationCodeError) return;
if (!verificationCode || verificationCodeError) return;
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
const confirmResult = await executeConfirmCode({
phoneNumber: fullPhoneNumber,
verifyCode: verificationCode,
});
if (!confirmResult) return;
if (!confirmResult.success || !confirmResult.confirm) {
toast({
message: confirmResult?.message || t('message.serverError'),
@@ -170,19 +159,15 @@ export function PhoneNumber() {
});
return;
}
setIsVerified(true);
toast({
message: t('settingForm.phoneVerified'),
severity: 'success',
});
const changeResult = await executeChangePhone({
phoneNumber: fullPhoneNumber,
});
if (!changeResult) return;
if (changeResult?.success) {
setPhones([
{
@@ -212,6 +197,8 @@ export function PhoneNumber() {
isEditing={isEditing}
toggleEdit={toggleEdit}
t={t}
formId="phoneForm"
isSubmitting={isBusy}
/>
}
>
@@ -228,27 +215,40 @@ export function PhoneNumber() {
<CircularProgress />
</Box>
) : isEditing ? (
<PhoneEditForm
phoneNumber={phoneNumber}
setPhoneNumber={setPhoneNumber}
countryCode={countryCode}
setCountryCode={setCountryCode}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
isVerified={isVerified}
isVerifying={isVerifying || isSendingCode || isChangingPhone}
isSendingCode={isSendingCode}
buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode}
phoneNumberError={phoneNumberTouched ? phoneNumberError : undefined}
verificationCodeError={
verificationCodeTouched ? verificationCodeError : undefined
}
handleBlur={handleBlur}
phones={phones}
/>
<Box
component="form"
id="phoneForm"
noValidate
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
if (!isBusy) void handleVerifyCode();
}}
>
<PhoneEditForm
phoneNumber={phoneNumber}
setPhoneNumber={setPhoneNumber}
countryCode={countryCode}
setCountryCode={setCountryCode}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
isVerified={isVerified}
isVerifying={isBusy}
isSendingCode={isSendingCode}
buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyCode}
phoneNumberError={
phoneNumberTouched ? phoneNumberError : undefined
}
verificationCodeError={
verificationCodeTouched ? verificationCodeError : undefined
}
handleBlur={handleBlur}
phones={phones}
/>
</Box>
) : (
<PhoneDisplay
phones={phones.map((p) => {

View File

@@ -8,36 +8,51 @@ export default function PhoneActionButtons({
}: PhoneActionButtonsProps) {
return (
<Box sx={{ display: 'flex', gap: 1 }}>
{isEditing && (
{isEditing ? (
<>
<Button
type="button"
variant="text"
onClick={toggleEdit}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.rejectButton')}
</Button>
<Button
type="submit"
form="phoneForm"
size="large"
variant="outlined"
sx={{
borderRadius: 1,
bgcolor: 'primary.main',
color: 'primary.contrastText',
whiteSpace: 'nowrap',
textTransform: 'none',
}}
>
{t('settingForm.saveButton')}
</Button>
</>
) : (
<Button
variant="text"
onClick={toggleEdit}
size="large"
variant="outlined"
sx={{
color: 'primary.main',
borderRadius: 1,
whiteSpace: 'nowrap',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.rejectButton')}
{t('settingForm.editPhoneNumber')}
</Button>
)}
<Button
onClick={toggleEdit}
size="large"
variant="outlined"
sx={{
borderRadius: 1,
bgcolor: isEditing ? 'primary.main' : 'background.default',
color: isEditing ? 'primary.contrastText' : 'primary.main',
whiteSpace: 'nowrap',
textTransform: 'none',
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editPhoneNumber')}
</Button>
</Box>
);
}

View File

@@ -54,6 +54,21 @@ export default function SocialMediaDialog({
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}
@@ -77,76 +92,80 @@ export default function SocialMediaDialog({
</Box>
</DialogTitle>
<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);
<Box component="form" onSubmit={handleSubmit}>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
onBlur={() => {
setTouched(true);
validateEmail(emailInput);
}}
label={t('settingForm.email')}
placeholder="abc@email.com"
autoComplete="email"
inputMode="email"
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
autoFocus
disabled={isLoading || dialogStep === 'enterCode'}
error={touched && !!emailError}
helperText={touched && emailError ? emailError : ' '}
/>
>
<Box>
<Typography fontWeight="bold">
{t('settingForm.newEmail')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.dialogHeader')}
</Typography>
</Box>
{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: 2 } }}
autoFocus
disabled={isLoading}
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: 2 }, mt: 2 }}
autoFocus={dialogStep === 'enterEmail'}
disabled={isLoading || dialogStep === 'enterCode'}
error={touched && !!emailError}
helperText={touched && emailError ? emailError : ' '}
/>
)}
</DialogContent>
<Box sx={{ px: 3, pb: 2 }}>
<Button
fullWidth
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
variant="contained"
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
onClick={dialogStep === 'enterEmail' ? onSendCode : onConfirmEmail}
>
{isLoading ? (
<CircularProgress size={24} color="inherit" />
) : dialogStep === 'enterEmail' ? (
t('settingForm.verificationCodeButton')
) : (
t('settingForm.confirmAndSave')
{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: 2 } }}
autoFocus
disabled={isLoading}
/>
)}
</Button>
</DialogContent>
<Box sx={{ px: 3, pb: 2 }}>
<Button
fullWidth
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
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>
);