From ea94df5af3d97ff2c70904524d58677f1ffcadcc Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Fri, 18 Jul 2025 02:54:03 +0330 Subject: [PATCH 01/32] feat: login form and page with country code part and animation --- public/locales/fa/authentication.json | 9 +++ src/App.tsx | 30 +------- src/assets/logo.svg | 30 ++++++++ src/components/Logo.tsx | 7 ++ .../components/common/Container.tsx | 8 +++ src/components/components/common/FlexBox.tsx | 21 ++++++ src/components/components/common/Stack.tsx | 19 +++++ .../components/CountryCodeAdornment.tsx | 35 +++++++++ .../authentication/components/LoginForm.tsx | 72 +++++++++++++++++++ src/features/authentication/index.ts | 0 .../authentication/routes/LoginPage.tsx | 30 ++++++++ src/providers/CustomThemeProvider.tsx | 15 ++++ 12 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 public/locales/fa/authentication.json create mode 100644 src/assets/logo.svg create mode 100644 src/components/Logo.tsx create mode 100644 src/components/components/common/Container.tsx create mode 100644 src/components/components/common/FlexBox.tsx create mode 100644 src/components/components/common/Stack.tsx create mode 100644 src/features/authentication/components/CountryCodeAdornment.tsx create mode 100644 src/features/authentication/components/LoginForm.tsx create mode 100644 src/features/authentication/index.ts create mode 100644 src/features/authentication/routes/LoginPage.tsx diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json new file mode 100644 index 0000000..d38deda --- /dev/null +++ b/public/locales/fa/authentication.json @@ -0,0 +1,9 @@ +{ + "loginForm": { + "title": "ورود/ثبت‌نام", + "description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.", + "emailOrPhoneLabel": "شماره موبایل/ایمیل", + "submitButton": "ورود/ثبت‌نام", + "loginWithGoogle": "ورود با گوگل" + } +} diff --git a/src/App.tsx b/src/App.tsx index 0659ade..1fcbd6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,40 +1,16 @@ -import { Box, CssBaseline, TextField, useColorScheme } from '@mui/material'; +import { CssBaseline } from '@mui/material'; import './App.css'; -import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; +import { LoginPage } from './features/authentication/routes/LoginPage'; function App() { - const { t } = useTranslation(); - return ( <> -
-

{t('helloWorld')}

-

The main content and router will go here.

- - - - -
+ ); } export default App; - -import { Button } from '@mui/material'; - -export const ThemeToggleButton = () => { - const { mode, setMode } = useColorScheme(); - - return ( - - ); -}; diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6f53ef6 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..dbba376 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,7 @@ +import LogoSvg from '@/assets/logo.svg'; + +function Logo() { + return ; +} + +export default Logo; diff --git a/src/components/components/common/Container.tsx b/src/components/components/common/Container.tsx new file mode 100644 index 0000000..c9efc78 --- /dev/null +++ b/src/components/components/common/Container.tsx @@ -0,0 +1,8 @@ +import { Box, styled } from '@mui/material'; + +export const Container = styled(Box)(() => ({ + width: '100%', + maxWidth: '100vw', + height: '100vh', + margin: '0 auto', +})); diff --git a/src/components/components/common/FlexBox.tsx b/src/components/components/common/FlexBox.tsx new file mode 100644 index 0000000..e15bb8c --- /dev/null +++ b/src/components/components/common/FlexBox.tsx @@ -0,0 +1,21 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +// Define the props our component will accept +interface FlexBoxProps extends BoxProps { + direction?: 'row' | 'column'; + justify?: string; + align?: string; +} + +export const FlexBox = styled(Box, { + // Do not forward these custom props to the DOM element + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'justify' && prop !== 'align', +})( + ({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + justifyContent: justify, + alignItems: align, + }), +); diff --git a/src/components/components/common/Stack.tsx b/src/components/components/common/Stack.tsx new file mode 100644 index 0000000..0e00bfd --- /dev/null +++ b/src/components/components/common/Stack.tsx @@ -0,0 +1,19 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +interface StackProps extends BoxProps { + direction?: 'row' | 'column'; + spacing?: number; // Spacing factor (multiplied by theme.spacing) + align?: string; +} + +export const Stack = styled(Box, { + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'spacing' && prop !== 'align', +})( + ({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + alignItems: align, + gap: theme.spacing(spacing), + }), +); diff --git a/src/features/authentication/components/CountryCodeAdornment.tsx b/src/features/authentication/components/CountryCodeAdornment.tsx new file mode 100644 index 0000000..a7593d1 --- /dev/null +++ b/src/features/authentication/components/CountryCodeAdornment.tsx @@ -0,0 +1,35 @@ +import { Box, Typography } from '@mui/material'; + +interface CountryCodeAdornmentProps { + show: boolean; +} + +/** + * An animated country code adornment that fades and slides into view. + * Its visibility is controlled by the `show` prop. + */ +export function CountryCodeAdornment({ show }: CountryCodeAdornmentProps) { + return ( + + theme.transitions.create(['width', 'opacity'], { + duration: theme.transitions.duration.short, + }), + // Prevent content from wrapping or spilling out during animation + overflow: 'hidden', + whiteSpace: 'nowrap', + }} + > + {/* This inner Box prevents the content from being squeezed during the transition */} + + + +41 + + + + ); +} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx new file mode 100644 index 0000000..dedb027 --- /dev/null +++ b/src/features/authentication/components/LoginForm.tsx @@ -0,0 +1,72 @@ +import { + Box, + Button, + InputAdornment, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CountryCodeAdornment } from './CountryCodeAdornment'; + +const isNumeric = (value: string) => /^\d*$/.test(value); + +export function LoginForm() { + const { t } = useTranslation('authentication'); + const [value, setValue] = useState(''); + const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + + // If the new value contains only digits (or is empty), it's a phone number + if (isNumeric(newValue)) { + setInputType('phone'); + } else { + setInputType('email'); + } + }; + + const showAdornment = inputType === 'phone' && value.length > 0; + + return ( + + + {t('loginForm.title')} + + {t('loginForm.description')} + + + + theme.transitions.create('margin'), + }} + > + + + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + + ); +} diff --git a/src/features/authentication/index.ts b/src/features/authentication/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/features/authentication/routes/LoginPage.tsx b/src/features/authentication/routes/LoginPage.tsx new file mode 100644 index 0000000..9177456 --- /dev/null +++ b/src/features/authentication/routes/LoginPage.tsx @@ -0,0 +1,30 @@ +import { FlexBox } from '@/components/components/common/FlexBox'; +import Logo from '@/components/Logo'; +import { Paper } from '@mui/material'; +import { LoginForm } from '../components/LoginForm'; + +export function LoginPage() { + return ( + + + + + + + ); +} diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx index 29df78d..ee1201e 100644 --- a/src/providers/CustomThemeProvider.tsx +++ b/src/providers/CustomThemeProvider.tsx @@ -25,6 +25,21 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ cssVariables: { colorSchemeSelector: 'class', }, + components: { + MuiTextField: { + defaultProps: { + variant: 'outlined', + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + size: 'large', + fullWidth: true, + variant: 'contained', + }, + }, + }, spacing: 8, typography: typography, }); From 381e274851ef2fdd9bfba9256836dcccef2c4397 Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Fri, 18 Jul 2025 02:54:03 +0330 Subject: [PATCH 02/32] feat: login form and page with country code part and animation --- public/locales/fa/authentication.json | 9 +++ src/App.tsx | 30 +------- src/assets/logo.svg | 30 ++++++++ src/components/Logo.tsx | 7 ++ .../components/common/Container.tsx | 8 +++ src/components/components/common/FlexBox.tsx | 21 ++++++ src/components/components/common/Stack.tsx | 19 +++++ .../components/CountryCodeAdornment.tsx | 35 +++++++++ .../authentication/components/LoginForm.tsx | 72 +++++++++++++++++++ src/features/authentication/index.ts | 0 .../authentication/routes/LoginPage.tsx | 30 ++++++++ src/providers/CustomThemeProvider.tsx | 15 ++++ 12 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 public/locales/fa/authentication.json create mode 100644 src/assets/logo.svg create mode 100644 src/components/Logo.tsx create mode 100644 src/components/components/common/Container.tsx create mode 100644 src/components/components/common/FlexBox.tsx create mode 100644 src/components/components/common/Stack.tsx create mode 100644 src/features/authentication/components/CountryCodeAdornment.tsx create mode 100644 src/features/authentication/components/LoginForm.tsx create mode 100644 src/features/authentication/index.ts create mode 100644 src/features/authentication/routes/LoginPage.tsx diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json new file mode 100644 index 0000000..d38deda --- /dev/null +++ b/public/locales/fa/authentication.json @@ -0,0 +1,9 @@ +{ + "loginForm": { + "title": "ورود/ثبت‌نام", + "description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.", + "emailOrPhoneLabel": "شماره موبایل/ایمیل", + "submitButton": "ورود/ثبت‌نام", + "loginWithGoogle": "ورود با گوگل" + } +} diff --git a/src/App.tsx b/src/App.tsx index 0659ade..1fcbd6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,40 +1,16 @@ -import { Box, CssBaseline, TextField, useColorScheme } from '@mui/material'; +import { CssBaseline } from '@mui/material'; import './App.css'; -import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; +import { LoginPage } from './features/authentication/routes/LoginPage'; function App() { - const { t } = useTranslation(); - return ( <> -
-

{t('helloWorld')}

-

The main content and router will go here.

- - - - -
+ ); } export default App; - -import { Button } from '@mui/material'; - -export const ThemeToggleButton = () => { - const { mode, setMode } = useColorScheme(); - - return ( - - ); -}; diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6f53ef6 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..dbba376 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,7 @@ +import LogoSvg from '@/assets/logo.svg'; + +function Logo() { + return ; +} + +export default Logo; diff --git a/src/components/components/common/Container.tsx b/src/components/components/common/Container.tsx new file mode 100644 index 0000000..c9efc78 --- /dev/null +++ b/src/components/components/common/Container.tsx @@ -0,0 +1,8 @@ +import { Box, styled } from '@mui/material'; + +export const Container = styled(Box)(() => ({ + width: '100%', + maxWidth: '100vw', + height: '100vh', + margin: '0 auto', +})); diff --git a/src/components/components/common/FlexBox.tsx b/src/components/components/common/FlexBox.tsx new file mode 100644 index 0000000..e15bb8c --- /dev/null +++ b/src/components/components/common/FlexBox.tsx @@ -0,0 +1,21 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +// Define the props our component will accept +interface FlexBoxProps extends BoxProps { + direction?: 'row' | 'column'; + justify?: string; + align?: string; +} + +export const FlexBox = styled(Box, { + // Do not forward these custom props to the DOM element + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'justify' && prop !== 'align', +})( + ({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + justifyContent: justify, + alignItems: align, + }), +); diff --git a/src/components/components/common/Stack.tsx b/src/components/components/common/Stack.tsx new file mode 100644 index 0000000..0e00bfd --- /dev/null +++ b/src/components/components/common/Stack.tsx @@ -0,0 +1,19 @@ +import { Box, styled, type BoxProps } from '@mui/material'; + +interface StackProps extends BoxProps { + direction?: 'row' | 'column'; + spacing?: number; // Spacing factor (multiplied by theme.spacing) + align?: string; +} + +export const Stack = styled(Box, { + shouldForwardProp: (prop) => + prop !== 'direction' && prop !== 'spacing' && prop !== 'align', +})( + ({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({ + display: 'flex', + flexDirection: direction, + alignItems: align, + gap: theme.spacing(spacing), + }), +); diff --git a/src/features/authentication/components/CountryCodeAdornment.tsx b/src/features/authentication/components/CountryCodeAdornment.tsx new file mode 100644 index 0000000..a7593d1 --- /dev/null +++ b/src/features/authentication/components/CountryCodeAdornment.tsx @@ -0,0 +1,35 @@ +import { Box, Typography } from '@mui/material'; + +interface CountryCodeAdornmentProps { + show: boolean; +} + +/** + * An animated country code adornment that fades and slides into view. + * Its visibility is controlled by the `show` prop. + */ +export function CountryCodeAdornment({ show }: CountryCodeAdornmentProps) { + return ( + + theme.transitions.create(['width', 'opacity'], { + duration: theme.transitions.duration.short, + }), + // Prevent content from wrapping or spilling out during animation + overflow: 'hidden', + whiteSpace: 'nowrap', + }} + > + {/* This inner Box prevents the content from being squeezed during the transition */} + + + +41 + + + + ); +} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx new file mode 100644 index 0000000..dedb027 --- /dev/null +++ b/src/features/authentication/components/LoginForm.tsx @@ -0,0 +1,72 @@ +import { + Box, + Button, + InputAdornment, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CountryCodeAdornment } from './CountryCodeAdornment'; + +const isNumeric = (value: string) => /^\d*$/.test(value); + +export function LoginForm() { + const { t } = useTranslation('authentication'); + const [value, setValue] = useState(''); + const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + + // If the new value contains only digits (or is empty), it's a phone number + if (isNumeric(newValue)) { + setInputType('phone'); + } else { + setInputType('email'); + } + }; + + const showAdornment = inputType === 'phone' && value.length > 0; + + return ( + + + {t('loginForm.title')} + + {t('loginForm.description')} + + + + theme.transitions.create('margin'), + }} + > + + + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + + ); +} diff --git a/src/features/authentication/index.ts b/src/features/authentication/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/features/authentication/routes/LoginPage.tsx b/src/features/authentication/routes/LoginPage.tsx new file mode 100644 index 0000000..9177456 --- /dev/null +++ b/src/features/authentication/routes/LoginPage.tsx @@ -0,0 +1,30 @@ +import { FlexBox } from '@/components/components/common/FlexBox'; +import Logo from '@/components/Logo'; +import { Paper } from '@mui/material'; +import { LoginForm } from '../components/LoginForm'; + +export function LoginPage() { + return ( + + + + + + + ); +} diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx index 29df78d..ee1201e 100644 --- a/src/providers/CustomThemeProvider.tsx +++ b/src/providers/CustomThemeProvider.tsx @@ -25,6 +25,21 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ cssVariables: { colorSchemeSelector: 'class', }, + components: { + MuiTextField: { + defaultProps: { + variant: 'outlined', + fullWidth: true, + }, + }, + MuiButton: { + defaultProps: { + size: 'large', + fullWidth: true, + variant: 'contained', + }, + }, + }, spacing: 8, typography: typography, }); From 2190518c26c5387bb4efe0f13edeae7e7e96e754 Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Mon, 21 Jul 2025 18:22:25 +0330 Subject: [PATCH 03/32] feat:change countryocde component --- azure-pipelines.yml | 18 +- package-lock.json | 10 ++ package.json | 1 + .../components/CountryCodeAdornment.tsx | 35 ---- .../components/CountryCodeSelector.tsx | 163 ++++++++++++++++++ .../authentication/components/LoginForm.tsx | 30 +++- src/features/authentication/data/countries.ts | 15 ++ 7 files changed, 221 insertions(+), 51 deletions(-) delete mode 100644 src/features/authentication/components/CountryCodeAdornment.tsx create mode 100644 src/features/authentication/components/CountryCodeSelector.tsx create mode 100644 src/features/authentication/data/countries.ts diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7c90784..cc58a0d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,18 +4,18 @@ # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript trigger: -- develop + - develop pool: vmImage: ubuntu-latest steps: -- task: NodeTool@0 - inputs: - versionSpec: '20.x' - displayName: 'Install Node.js' + - task: NodeTool@0 + inputs: + versionSpec: '20.x' + displayName: 'Install Node.js' -- script: | - npm install - npm run build - displayName: 'npm install and build' + - script: | + npm install + npm run build + displayName: 'npm install and build' diff --git a/package-lock.json b/package-lock.json index 1204723..bf02d1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", + "iconsax-reactjs": "^0.0.8", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", @@ -3102,6 +3103,15 @@ "cross-fetch": "4.0.0" } }, + "node_modules/iconsax-reactjs": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/iconsax-reactjs/-/iconsax-reactjs-0.0.8.tgz", + "integrity": "sha512-cb+uTMxbkSFNbu8ZclX7BWQVfOWQt8+m/PsDjnsm/H+mcYrnfTYMjHxiof1FB43k7UAgt1ds+0oFeMVKdqyslw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index 666890c..563f7de 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", + "iconsax-reactjs": "^0.0.8", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", diff --git a/src/features/authentication/components/CountryCodeAdornment.tsx b/src/features/authentication/components/CountryCodeAdornment.tsx deleted file mode 100644 index a7593d1..0000000 --- a/src/features/authentication/components/CountryCodeAdornment.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Typography } from '@mui/material'; - -interface CountryCodeAdornmentProps { - show: boolean; -} - -/** - * An animated country code adornment that fades and slides into view. - * Its visibility is controlled by the `show` prop. - */ -export function CountryCodeAdornment({ show }: CountryCodeAdornmentProps) { - return ( - - theme.transitions.create(['width', 'opacity'], { - duration: theme.transitions.duration.short, - }), - // Prevent content from wrapping or spilling out during animation - overflow: 'hidden', - whiteSpace: 'nowrap', - }} - > - {/* This inner Box prevents the content from being squeezed during the transition */} - - - +41 - - - - ); -} diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx new file mode 100644 index 0000000..39a9c16 --- /dev/null +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -0,0 +1,163 @@ +import { + Box, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + TextField, + Typography, +} from '@mui/material'; +import { useEffect, useMemo, useRef, useState, type RefObject } from 'react'; +import { countries, type Country } from '../data/countries'; +import { ArrowDown2 } from 'iconsax-reactjs'; + +interface CountryCodeSelectorProps { + show: boolean; + value: string; + onChange: (newValue: string) => void; + menuAnchor: HTMLElement | null; + onCloseFocusRef: RefObject; +} + +/** + * An animated country code adornment that fades and slides into view. + * Its visibility is controlled by the `show` prop. + */ +export function CountryCodeSelector({ + show, + value, + onChange, + menuAnchor, + onCloseFocusRef, +}: CountryCodeSelectorProps) { + const [anchorEl, setAnchorEl] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const searchInputRef = useRef(null); + const open = Boolean(anchorEl); + const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto'; + + const handleClick = () => { + setAnchorEl(menuAnchor); + }; + + const handleClose = () => { + setTimeout(() => { + setAnchorEl(null); + }, 0); + setTimeout(() => { + onCloseFocusRef.current?.focus(); + }, 100); + setSearchTerm(''); // Reset search on close + }; + + const handleSelect = (country: Country) => { + onChange(country.phone); + handleClose(); + }; + + const handleMenuEntered = () => { + // Focus the input field after the menu has finished opening + searchInputRef.current?.focus(); + }; + + useEffect(() => { + // console.log(open); + }, [open]); + + const filteredCountries = useMemo( + () => + countries.filter( + (country) => + country.label.toLowerCase().includes(searchTerm.toLowerCase()) || + country.phone.includes(searchTerm), + ), + [searchTerm], + ); + + return ( + + theme.transitions.create(['width', 'opacity'], { + duration: theme.transitions.duration.standard, + }), + // Prevent content from wrapping or spilling out during animation + overflow: 'hidden', + whiteSpace: 'nowrap', + + // layout styles + display: 'flex', + alignItems: 'center', + gap: 0.25, + pl: show ? 0.25 : 0, + + '&:hover': { + cursor: 'pointer', + }, + }} + > + {/* This inner Box prevents the content from being squeezed during the transition */} + + + + {value} + + + + + setSearchTerm(e.target.value)} + /> + + + {filteredCountries.map((country) => ( + handleSelect(country)} + > + {country.flag} + + {country.phone} + + ))} + + + ); +} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx index dedb027..0fe6d22 100644 --- a/src/features/authentication/components/LoginForm.tsx +++ b/src/features/authentication/components/LoginForm.tsx @@ -6,16 +6,20 @@ import { TextField, Typography, } from '@mui/material'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { CountryCodeAdornment } from './CountryCodeAdornment'; +import { CountryCodeSelector } from './CountryCodeSelector'; +import { Google } from 'iconsax-reactjs'; const isNumeric = (value: string) => /^\d*$/.test(value); export function LoginForm() { const { t } = useTranslation('authentication'); const [value, setValue] = useState(''); + const [countryCode, setCountryCode] = useState('+41'); const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); + const textFieldRef = useRef(null); + const inputRef = useRef(null); const handleInputChange = (event: React.ChangeEvent) => { const newValue = event.target.value; @@ -41,21 +45,29 @@ export function LoginForm() { theme.transitions.create('margin'), }} > - + ), }, @@ -64,8 +76,12 @@ export function LoginForm() { /> - - + + ); diff --git a/src/features/authentication/data/countries.ts b/src/features/authentication/data/countries.ts new file mode 100644 index 0000000..18bb14b --- /dev/null +++ b/src/features/authentication/data/countries.ts @@ -0,0 +1,15 @@ +export interface Country { + code: string; + label: string; + phone: string; + flag: string; +} + +export const countries: readonly Country[] = [ + { code: 'CH', label: 'Switzerland', phone: '+41', flag: '🇨🇭' }, + { code: 'SA', label: 'Saudi Arabia', phone: '+966', flag: '🇸🇦' }, + { code: 'QA', label: 'Qatar', phone: '+974', flag: '🇶🇦' }, + { code: 'KW', label: 'Kuwait', phone: '+965', flag: '🇰🇼' }, + { code: 'BH', label: 'Bahrain', phone: '+973', flag: '🇧🇭' }, + { code: 'AE', label: 'United Arab Emirates', phone: '+971', flag: '🇦🇪' }, +]; From 83c3f05e68412a65a5a24fefd650cd3e653d5fd5 Mon Sep 17 00:00:00 2001 From: SajadMRjl Date: Mon, 21 Jul 2025 19:19:50 +0330 Subject: [PATCH 04/32] feat: add country flags to country code selection --- package-lock.json | 13 +++++++++++++ package.json | 1 + .../components/CountryCodeSelector.tsx | 12 +++++++++++- src/features/authentication/data/countries.ts | 13 ++++++------- src/features/authentication/routes/LoginPage.tsx | 5 +++-- src/providers/CustomThemeProvider.tsx | 2 +- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf02d1b..5dc7a94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "i18next-http-backend": "^3.0.2", "iconsax-reactjs": "^0.0.8", "react": "^19.1.0", + "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "stylis": "^4.3.6", @@ -3719,6 +3720,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-country-flag": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", + "integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index 563f7de..9f6c517 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "i18next-http-backend": "^3.0.2", "iconsax-reactjs": "^0.0.8", "react": "^19.1.0", + "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "stylis": "^4.3.6", diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx index 39a9c16..9532d82 100644 --- a/src/features/authentication/components/CountryCodeSelector.tsx +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -10,6 +10,7 @@ import { import { useEffect, useMemo, useRef, useState, type RefObject } from 'react'; import { countries, type Country } from '../data/countries'; import { ArrowDown2 } from 'iconsax-reactjs'; +import ReactCountryFlag from 'react-country-flag'; interface CountryCodeSelectorProps { show: boolean; @@ -152,7 +153,16 @@ export function CountryCodeSelector({ selected={country.phone === value} onClick={() => handleSelect(country)} > - {country.flag} + + + {country.phone} diff --git a/src/features/authentication/data/countries.ts b/src/features/authentication/data/countries.ts index 18bb14b..b26e16c 100644 --- a/src/features/authentication/data/countries.ts +++ b/src/features/authentication/data/countries.ts @@ -2,14 +2,13 @@ export interface Country { code: string; label: string; phone: string; - flag: string; } export const countries: readonly Country[] = [ - { code: 'CH', label: 'Switzerland', phone: '+41', flag: '🇨🇭' }, - { code: 'SA', label: 'Saudi Arabia', phone: '+966', flag: '🇸🇦' }, - { code: 'QA', label: 'Qatar', phone: '+974', flag: '🇶🇦' }, - { code: 'KW', label: 'Kuwait', phone: '+965', flag: '🇰🇼' }, - { code: 'BH', label: 'Bahrain', phone: '+973', flag: '🇧🇭' }, - { code: 'AE', label: 'United Arab Emirates', phone: '+971', flag: '🇦🇪' }, + { code: 'CH', label: 'Switzerland', phone: '+41' }, + { code: 'SA', label: 'Saudi Arabia', phone: '+966' }, + { code: 'QA', label: 'Qatar', phone: '+974' }, + { code: 'KW', label: 'Kuwait', phone: '+965' }, + { code: 'BH', label: 'Bahrain', phone: '+973' }, + { code: 'AE', label: 'United Arab Emirates', phone: '+971' }, ]; diff --git a/src/features/authentication/routes/LoginPage.tsx b/src/features/authentication/routes/LoginPage.tsx index 9177456..d136967 100644 --- a/src/features/authentication/routes/LoginPage.tsx +++ b/src/features/authentication/routes/LoginPage.tsx @@ -16,9 +16,10 @@ export function LoginPage() { > = ({ palette: lightPalette, }, dark: { - palette: darkPalette, + palette: darkPalette, }, }, cssVariables: { From d2efafa5a9f23d63fda51591102ab97f4363d9e8 Mon Sep 17 00:00:00 2001 From: Sajad Mirjalili Date: Tue, 22 Jul 2025 18:42:54 +0330 Subject: [PATCH 05/32] feat: add digits input, complete signin form --- package-lock.json | 11 + package.json | 1 + public/locales/fa/common.json | 7 +- src/components/components/DigitsInput.tsx | 106 +++++++ .../components/CountryCodeSelector.tsx | 264 +++++++++++------ .../authentication/components/LoginForm.tsx | 58 ++-- .../authentication/components/SmsOtpForm.tsx | 44 +++ src/features/authentication/data/countries.ts | 269 +++++++++++++++++- .../authentication/routes/LoginPage.tsx | 12 +- src/providers/CustomThemeProvider.tsx | 2 +- src/providers/RtlProvider.tsx | 19 +- src/utils/regexes/isNumeric.ts | 1 + 12 files changed, 652 insertions(+), 142 deletions(-) create mode 100644 src/components/components/DigitsInput.tsx create mode 100644 src/features/authentication/components/SmsOtpForm.tsx create mode 100644 src/utils/regexes/isNumeric.ts diff --git a/package-lock.json b/package-lock.json index 5dc7a94..eb709eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, @@ -3802,6 +3803,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtuoso": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.13.0.tgz", + "integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index 9f6c517..22e1fc7 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index 3f4cd0d..9c5b085 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -1,3 +1,8 @@ { - "helloWorld": "سلام دنیا" + "labels": { + "search": "جست و جو" + }, + "messages": { + "noResualtFound": "نتیجه ای یافت نشد." + } } diff --git a/src/components/components/DigitsInput.tsx b/src/components/components/DigitsInput.tsx new file mode 100644 index 0000000..de4f054 --- /dev/null +++ b/src/components/components/DigitsInput.tsx @@ -0,0 +1,106 @@ +import React, { + useRef, + useEffect, + type SetStateAction, + type Dispatch, + useState, + type KeyboardEvent, +} from 'react'; +import { TextField, Stack } from '@mui/material'; + +interface DigitInputProps { + onChange: Dispatch>; +} + +const DigitInput: React.FC = ({ onChange }) => { + const [code, setCode] = useState(['', '', '', '']); + const inputRefs = useRef>([]); + + useEffect(() => { + inputRefs.current[0]?.focus(); + }, []); + + const handleChange = (value: string, index: number) => { + if (!/^\d$/.test(value) && value !== '') return; + + const newCode = [...code]; + newCode[index] = value; + setCode(newCode); + onChange(newCode); + + if (value && index < 4 - 1) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleBackspace = ( + event: KeyboardEvent, + index: number, + ) => { + event.preventDefault(); + if (index >= 0) { + handleChange('', index); + inputRefs.current[index - 1]?.focus(); + } + }; + + const handlePaste = (event: React.ClipboardEvent) => { + event.preventDefault(); + const pastedData = event.clipboardData.getData('text').replace(/\D/g, ''); // Remove non-digit characters + const newCode = [...code]; + + pastedData.split('').forEach((digit, i) => { + if (i < code.length) { + newCode[i] = digit; + } + }); + + setCode(newCode); + onChange(newCode); + + // Focus the next empty input after the last pasted character + const lastIndex = Math.min(pastedData.length, code.length) - 1; + if (lastIndex >= 0 && inputRefs.current[lastIndex]) { + inputRefs.current[lastIndex]?.focus(); + } + }; + + return ( + + {code.map((digit, index) => ( + (inputRefs.current[index] = el)} + value={digit} + onChange={(e) => handleChange(e.target.value, index)} + onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)} + onPaste={(e) => handlePaste(e)} + slotProps={{ + htmlInput: { + maxLength: 1, + sx: { + height: '72px', + }, + style: { + textAlign: 'center', + fontSize: '48px', + }, + }, + }} + variant="standard" + size="medium" + sx={{ + width: '83px', + }} + /> + ))} + + ); +}; + +export default DigitInput; diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx index 9532d82..ea4d89d 100644 --- a/src/features/authentication/components/CountryCodeSelector.tsx +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -1,5 +1,7 @@ import { Box, + InputAdornment, + ListItem, ListItemIcon, ListItemText, Menu, @@ -7,11 +9,12 @@ import { TextField, Typography, } from '@mui/material'; -import { useEffect, useMemo, useRef, useState, type RefObject } from 'react'; +import { useMemo, useRef, useState, type RefObject } from 'react'; import { countries, type Country } from '../data/countries'; import { ArrowDown2 } from 'iconsax-reactjs'; import ReactCountryFlag from 'react-country-flag'; - +import { useTranslation } from 'react-i18next'; +import { Virtuoso } from 'react-virtuoso'; interface CountryCodeSelectorProps { show: boolean; value: string; @@ -33,9 +36,13 @@ export function CountryCodeSelector({ }: CountryCodeSelectorProps) { const [anchorEl, setAnchorEl] = useState(null); const [searchTerm, setSearchTerm] = useState(''); - const searchInputRef = useRef(null); const open = Boolean(anchorEl); + const searchInputRef = useRef(null); const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto'; + const { t, i18n } = useTranslation(); + + const selectedCountry = + countries.find((c) => c.phone === value) || countries[0]; const handleClick = () => { setAnchorEl(menuAnchor); @@ -61,10 +68,6 @@ export function CountryCodeSelector({ searchInputRef.current?.focus(); }; - useEffect(() => { - // console.log(open); - }, [open]); - const filteredCountries = useMemo( () => countries.filter( @@ -76,98 +79,175 @@ export function CountryCodeSelector({ ); return ( - - theme.transitions.create(['width', 'opacity'], { - duration: theme.transitions.duration.standard, - }), - // Prevent content from wrapping or spilling out during animation - overflow: 'hidden', - whiteSpace: 'nowrap', - - // layout styles - display: 'flex', - alignItems: 'center', - gap: 0.25, - pl: show ? 0.25 : 0, - - '&:hover': { - cursor: 'pointer', - }, + mx: 0, }} > - {/* This inner Box prevents the content from being squeezed during the transition */} - + + theme.transitions.create(['width', 'opacity'], { + duration: theme.transitions.duration.standard, + }), - - {value} - + {/* This inner Box prevents the content from being squeezed during the transition */} + - + {value} + + + + + - - setSearchTerm(e.target.value)} - /> - - - {filteredCountries.map((country) => ( - handleSelect(country)} + transition: { + onEntered: handleMenuEntered, + }, + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + - - - - - {country.phone} - - ))} - - + setSearchTerm(e.target.value)} + /> + + + {/* Can improve preformance with using virtual scrolling */} + + {filteredCountries.length === 0 ? ( + + + {t('messages.noResualtFound')} + + + ) : ( + filteredCountries.map((country) => ( + handleSelect(country)} + > + + + + + + {country.phone} + + + )) + )} + + + {/* virtual scrolling */} + {/* ( + + {t('messages.noResultFound')} + + ), + }} + initialTopMostItemIndex={countries.indexOf(selectedCountry)} + itemContent={(_, country) => ( + handleSelect(country)} + > + + + + + + {country.phone} + + + )} + /> */} + + + ); } diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx index 0fe6d22..0584f23 100644 --- a/src/features/authentication/components/LoginForm.tsx +++ b/src/features/authentication/components/LoginForm.tsx @@ -1,25 +1,18 @@ -import { - Box, - Button, - InputAdornment, - Stack, - TextField, - Typography, -} from '@mui/material'; +import { Box, Button, Stack, TextField, Typography } from '@mui/material'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CountryCodeSelector } from './CountryCodeSelector'; import { Google } from 'iconsax-reactjs'; - -const isNumeric = (value: string) => /^\d*$/.test(value); +import { isNumeric } from '@/utils/regexes/isNumeric'; export function LoginForm() { - const { t } = useTranslation('authentication'); + const { t, i18n } = useTranslation('authentication'); const [value, setValue] = useState(''); const [countryCode, setCountryCode] = useState('+41'); const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); const textFieldRef = useRef(null); const inputRef = useRef(null); + const dir = i18n.dir(); const handleInputChange = (event: React.ChangeEvent) => { const newValue = event.target.value; @@ -33,6 +26,11 @@ export function LoginForm() { } }; + const handleBlur = () => { + // setTouched(true); + // setError(validate(value, inputType)); + }; + const showAdornment = inputType === 'phone' && value.length > 0; return ( @@ -50,25 +48,29 @@ export function LoginForm() { label={t('loginForm.emailOrPhoneLabel')} value={value} onChange={handleInputChange} + onBlur={handleBlur} + // error={touched && !!error} + autoFocus slotProps={{ - htmlInput: { dir: 'ltr', sx: { lineHeight: 1.5 } }, + htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } }, input: { - // in en mode it placed in wrong end - endAdornment: ( - - - + startAdornment: dir === 'ltr' && ( + + ), + endAdornment: dir === 'rtl' && ( + ), }, }} diff --git a/src/features/authentication/components/SmsOtpForm.tsx b/src/features/authentication/components/SmsOtpForm.tsx new file mode 100644 index 0000000..ce03b0a --- /dev/null +++ b/src/features/authentication/components/SmsOtpForm.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next'; +import { Box, Button, Typography } from '@mui/material'; +import { Edit2 } from 'iconsax-reactjs'; +import DigitInput from '@/components/components/DigitsInput'; + +interface SmsOtpProps { + value: string; + type: 'phone' | 'email'; +} + +export function SmsOtpForm({ value, type }: SmsOtpProps) { + const { t } = useTranslation('authentication'); + + return ( + + + اعتبارسنجی + + + + + کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید. + + console.log(value)} /> + + + ); +} diff --git a/src/features/authentication/data/countries.ts b/src/features/authentication/data/countries.ts index b26e16c..2b05f9e 100644 --- a/src/features/authentication/data/countries.ts +++ b/src/features/authentication/data/countries.ts @@ -4,11 +4,268 @@ export interface Country { phone: string; } +export interface Country { + code: string; + label: string; + phone: string; +} + export const countries: readonly Country[] = [ - { code: 'CH', label: 'Switzerland', phone: '+41' }, - { code: 'SA', label: 'Saudi Arabia', phone: '+966' }, - { code: 'QA', label: 'Qatar', phone: '+974' }, - { code: 'KW', label: 'Kuwait', phone: '+965' }, - { code: 'BH', label: 'Bahrain', phone: '+973' }, - { code: 'AE', label: 'United Arab Emirates', phone: '+971' }, + { code: 'AF', label: 'country.afghanistan', phone: '+93' }, + { code: 'AX', label: 'country.aland_islands', phone: '+358' }, + { code: 'AL', label: 'country.albania', phone: '+355' }, + { code: 'DZ', label: 'country.algeria', phone: '+213' }, + { code: 'AS', label: 'country.american_samoa', phone: '+1684' }, + { code: 'AD', label: 'country.andorra', phone: '+376' }, + { code: 'AO', label: 'country.angola', phone: '+244' }, + { code: 'AI', label: 'country.anguilla', phone: '+1264' }, + { code: 'AQ', label: 'country.antarctica', phone: '+672' }, + { code: 'AG', label: 'country.antigua_and_barbuda', phone: '+1268' }, + { code: 'AR', label: 'country.argentina', phone: '+54' }, + { code: 'AM', label: 'country.armenia', phone: '+374' }, + { code: 'AW', label: 'country.aruba', phone: '+297' }, + { code: 'AU', label: 'country.australia', phone: '+61' }, + { code: 'AT', label: 'country.austria', phone: '+43' }, + { code: 'AZ', label: 'country.azerbaijan', phone: '+994' }, + { code: 'BS', label: 'country.bahamas', phone: '+1242' }, + { code: 'BH', label: 'country.bahrain', phone: '+973' }, + { code: 'BD', label: 'country.bangladesh', phone: '+880' }, + { code: 'BB', label: 'country.barbados', phone: '+1246' }, + { code: 'BY', label: 'country.belarus', phone: '+375' }, + { code: 'BE', label: 'country.belgium', phone: '+32' }, + { code: 'BZ', label: 'country.belize', phone: '+501' }, + { code: 'BJ', label: 'country.benin', phone: '+229' }, + { code: 'BM', label: 'country.bermuda', phone: '+1441' }, + { code: 'BT', label: 'country.bhutan', phone: '+975' }, + { code: 'BO', label: 'country.bolivia', phone: '+591' }, + { code: 'BA', label: 'country.bosnia_and_herzegovina', phone: '+387' }, + { code: 'BW', label: 'country.botswana', phone: '+267' }, + { code: 'BR', label: 'country.brazil', phone: '+55' }, + { + code: 'IO', + label: 'country.british_indian_ocean_territory', + phone: '+246', + }, + { code: 'VG', label: 'country.british_virgin_islands', phone: '+1284' }, + { code: 'BN', label: 'country.brunei', phone: '+673' }, + { code: 'BG', label: 'country.bulgaria', phone: '+359' }, + { code: 'BF', label: 'country.burkina_faso', phone: '+226' }, + { code: 'BI', label: 'country.burundi', phone: '+257' }, + { code: 'KH', label: 'country.cambodia', phone: '+855' }, + { code: 'CM', label: 'country.cameroon', phone: '+237' }, + { code: 'CA', label: 'country.canada', phone: '+1' }, + { code: 'CV', label: 'country.cape_verde', phone: '+238' }, + { code: 'KY', label: 'country.cayman_islands', phone: '+1345' }, + { code: 'CF', label: 'country.central_african_republic', phone: '+236' }, + { code: 'TD', label: 'country.chad', phone: '+235' }, + { code: 'CL', label: 'country.chile', phone: '+56' }, + { code: 'CN', label: 'country.china', phone: '+86' }, + { code: 'CX', label: 'country.christmas_island', phone: '+61' }, + { code: 'CC', label: 'country.cocos_keeling_islands', phone: '+61' }, + { code: 'CO', label: 'country.colombia', phone: '+57' }, + { code: 'KM', label: 'country.comoros', phone: '+269' }, + { code: 'CG', label: 'country.congo_brazzaville', phone: '+242' }, + { code: 'CD', label: 'country.congo_kinshasa', phone: '+243' }, + { code: 'CK', label: 'country.cook_islands', phone: '+682' }, + { code: 'CR', label: 'country.costa_rica', phone: '+506' }, + { code: 'CI', label: 'country.cote_divoire', phone: '+225' }, + { code: 'HR', label: 'country.croatia', phone: '+385' }, + { code: 'CU', label: 'country.cuba', phone: '+53' }, + { code: 'CW', label: 'country.curacao', phone: '+599' }, + { code: 'CY', label: 'country.cyprus', phone: '+357' }, + { code: 'CZ', label: 'country.czech_republic', phone: '+420' }, + { code: 'DK', label: 'country.denmark', phone: '+45' }, + { code: 'DJ', label: 'country.djibouti', phone: '+253' }, + { code: 'DM', label: 'country.dominica', phone: '+1767' }, + { code: 'DO', label: 'country.dominican_republic', phone: '+1' }, + { code: 'EC', label: 'country.ecuador', phone: '+593' }, + { code: 'EG', label: 'country.egypt', phone: '+20' }, + { code: 'SV', label: 'country.el_salvador', phone: '+503' }, + { code: 'GQ', label: 'country.equatorial_guinea', phone: '+240' }, + { code: 'ER', label: 'country.eritrea', phone: '+291' }, + { code: 'EE', label: 'country.estonia', phone: '+372' }, + { code: 'SZ', label: 'country.eswatini', phone: '+268' }, + { code: 'ET', label: 'country.ethiopia', phone: '+251' }, + { code: 'FK', label: 'country.falkland_islands', phone: '+500' }, + { code: 'FO', label: 'country.faroe_islands', phone: '+298' }, + { code: 'FJ', label: 'country.fiji', phone: '+679' }, + { code: 'FI', label: 'country.finland', phone: '+358' }, + { code: 'FR', label: 'country.france', phone: '+33' }, + { code: 'GF', label: 'country.french_guiana', phone: '+594' }, + { code: 'PF', label: 'country.french_polynesia', phone: '+689' }, + { code: 'GA', label: 'country.gabon', phone: '+241' }, + { code: 'GM', label: 'country.gambia', phone: '+220' }, + { code: 'GE', label: 'country.georgia', phone: '+995' }, + { code: 'DE', label: 'country.germany', phone: '+49' }, + { code: 'GH', label: 'country.ghana', phone: '+233' }, + { code: 'GI', label: 'country.gibraltar', phone: '+350' }, + { code: 'GR', label: 'country.greece', phone: '+30' }, + { code: 'GL', label: 'country.greenland', phone: '+299' }, + { code: 'GD', label: 'country.grenada', phone: '+1473' }, + { code: 'GP', label: 'country.guadeloupe', phone: '+590' }, + { code: 'GU', label: 'country.guam', phone: '+1671' }, + { code: 'GT', label: 'country.guatemala', phone: '+502' }, + { code: 'GG', label: 'country.guernsey', phone: '+44' }, + { code: 'GN', label: 'country.guinea', phone: '+224' }, + { code: 'GW', label: 'country.guinea_bissau', phone: '+245' }, + { code: 'GY', label: 'country.guyana', phone: '+592' }, + { code: 'HT', label: 'country.haiti', phone: '+509' }, + { code: 'HN', label: 'country.honduras', phone: '+504' }, + { code: 'HK', label: 'country.hong_kong', phone: '+852' }, + { code: 'HU', label: 'country.hungary', phone: '+36' }, + { code: 'IS', label: 'country.iceland', phone: '+354' }, + { code: 'IN', label: 'country.india', phone: '+91' }, + { code: 'ID', label: 'country.indonesia', phone: '+62' }, + { code: 'IR', label: 'country.iran', phone: '+98' }, + { code: 'IQ', label: 'country.iraq', phone: '+964' }, + { code: 'IE', label: 'country.ireland', phone: '+353' }, + { code: 'IM', label: 'country.isle_of_man', phone: '+44' }, + { code: 'IL', label: 'country.israel', phone: '+972' }, + { code: 'IT', label: 'country.italy', phone: '+39' }, + { code: 'JM', label: 'country.jamaica', phone: '+1876' }, + { code: 'JP', label: 'country.japan', phone: '+81' }, + { code: 'JE', label: 'country.jersey', phone: '+44' }, + { code: 'JO', label: 'country.jordan', phone: '+962' }, + { code: 'KZ', label: 'country.kazakhstan', phone: '+7' }, + { code: 'KE', label: 'country.kenya', phone: '+254' }, + { code: 'KI', label: 'country.kiribati', phone: '+686' }, + { code: 'XK', label: 'country.kosovo', phone: '+383' }, + { code: 'KW', label: 'country.kuwait', phone: '+965' }, + { code: 'KG', label: 'country.kyrgyzstan', phone: '+996' }, + { code: 'LA', label: 'country.laos', phone: '+856' }, + { code: 'LV', label: 'country.latvia', phone: '+371' }, + { code: 'LB', label: 'country.lebanon', phone: '+961' }, + { code: 'LS', label: 'country.lesotho', phone: '+266' }, + { code: 'LR', label: 'country.liberia', phone: '+231' }, + { code: 'LY', label: 'country.libya', phone: '+218' }, + { code: 'LI', label: 'country.liechtenstein', phone: '+423' }, + { code: 'LT', label: 'country.lithuania', phone: '+370' }, + { code: 'LU', label: 'country.luxembourg', phone: '+352' }, + { code: 'MO', label: 'country.macau', phone: '+853' }, + { code: 'MG', label: 'country.madagascar', phone: '+261' }, + { code: 'MW', label: 'country.malawi', phone: '+265' }, + { code: 'MY', label: 'country.malaysia', phone: '+60' }, + { code: 'MV', label: 'country.maldives', phone: '+960' }, + { code: 'ML', label: 'country.mali', phone: '+223' }, + { code: 'MT', label: 'country.malta', phone: '+356' }, + { code: 'MH', label: 'country.marshall_islands', phone: '+692' }, + { code: 'MQ', label: 'country.martinique', phone: '+596' }, + { code: 'MR', label: 'country.mauritania', phone: '+222' }, + { code: 'MU', label: 'country.mauritius', phone: '+230' }, + { code: 'YT', label: 'country.mayotte', phone: '+262' }, + { code: 'MX', label: 'country.mexico', phone: '+52' }, + { code: 'FM', label: 'country.micronesia', phone: '+691' }, + { code: 'MD', label: 'country.moldova', phone: '+373' }, + { code: 'MC', label: 'country.monaco', phone: '+377' }, + { code: 'MN', label: 'country.mongolia', phone: '+976' }, + { code: 'ME', label: 'country.montenegro', phone: '+382' }, + { code: 'MS', label: 'country.montserrat', phone: '+1664' }, + { code: 'MA', label: 'country.morocco', phone: '+212' }, + { code: 'MZ', label: 'country.mozambique', phone: '+258' }, + { code: 'MM', label: 'country.myanmar', phone: '+95' }, + { code: 'NA', label: 'country.namibia', phone: '+264' }, + { code: 'NR', label: 'country.nauru', phone: '+674' }, + { code: 'NP', label: 'country.nepal', phone: '+977' }, + { code: 'NL', label: 'country.netherlands', phone: '+31' }, + { code: 'NC', label: 'country.new_caledonia', phone: '+687' }, + { code: 'NZ', label: 'country.new_zealand', phone: '+64' }, + { code: 'NI', label: 'country.nicaragua', phone: '+505' }, + { code: 'NE', label: 'country.niger', phone: '+227' }, + { code: 'NG', label: 'country.nigeria', phone: '+234' }, + { code: 'NU', label: 'country.niue', phone: '+683' }, + { code: 'NF', label: 'country.norfolk_island', phone: '+672' }, + { code: 'KP', label: 'country.north_korea', phone: '+850' }, + { code: 'MK', label: 'country.north_macedonia', phone: '+389' }, + { code: 'MP', label: 'country.northern_mariana_islands', phone: '+1670' }, + { code: 'NO', label: 'country.norway', phone: '+47' }, + { code: 'OM', label: 'country.oman', phone: '+968' }, + { code: 'PK', label: 'country.pakistan', phone: '+92' }, + { code: 'PW', label: 'country.palau', phone: '+680' }, + { code: 'PS', label: 'country.palestine', phone: '+970' }, + { code: 'PA', label: 'country.panama', phone: '+507' }, + { code: 'PG', label: 'country.papua_new_guinea', phone: '+675' }, + { code: 'PY', label: 'country.paraguay', phone: '+595' }, + { code: 'PE', label: 'country.peru', phone: '+51' }, + { code: 'PH', label: 'country.philippines', phone: '+63' }, + { code: 'PN', label: 'country.pitcairn_islands', phone: '+64' }, + { code: 'PL', label: 'country.poland', phone: '+48' }, + { code: 'PT', label: 'country.portugal', phone: '+351' }, + { code: 'PR', label: 'country.puerto_rico', phone: '+1' }, + { code: 'QA', label: 'country.qatar', phone: '+974' }, + { code: 'RE', label: 'country.reunion', phone: '+262' }, + { code: 'RO', label: 'country.romania', phone: '+40' }, + { code: 'RU', label: 'country.russia', phone: '+7' }, + { code: 'RW', label: 'country.rwanda', phone: '+250' }, + { code: 'BL', label: 'country.saint_barthelemy', phone: '+590' }, + { code: 'SH', label: 'country.saint_helena', phone: '+290' }, + { code: 'KN', label: 'country.saint_kitts_and_nevis', phone: '+1869' }, + { code: 'LC', label: 'country.saint_lucia', phone: '+1758' }, + { code: 'MF', label: 'country.saint_martin', phone: '+590' }, + { code: 'PM', label: 'country.saint_pierre_and_miquelon', phone: '+508' }, + { + code: 'VC', + label: 'country.saint_vincent_and_the_grenadines', + phone: '+1784', + }, + { code: 'WS', label: 'country.samoa', phone: '+685' }, + { code: 'SM', label: 'country.san_marino', phone: '+378' }, + { code: 'ST', label: 'country.sao_tome_and_principe', phone: '+239' }, + { code: 'SA', label: 'country.saudi_arabia', phone: '+966' }, + { code: 'SN', label: 'country.senegal', phone: '+221' }, + { code: 'RS', label: 'country.serbia', phone: '+381' }, + { code: 'SC', label: 'country.seychelles', phone: '+248' }, + { code: 'SL', label: 'country.sierra_leone', phone: '+232' }, + { code: 'SG', label: 'country.singapore', phone: '+65' }, + { code: 'SX', label: 'country.sint_maarten', phone: '+1721' }, + { code: 'SK', label: 'country.slovakia', phone: '+421' }, + { code: 'SI', label: 'country.slovenia', phone: '+386' }, + { code: 'SB', label: 'country.solomon_islands', phone: '+677' }, + { code: 'SO', label: 'country.somalia', phone: '+252' }, + { code: 'ZA', label: 'country.south_africa', phone: '+27' }, + { + code: 'GS', + label: 'country.south_georgia_and_south_sandwich_islands', + phone: '+500', + }, + { code: 'KR', label: 'country.south_korea', phone: '+82' }, + { code: 'SS', label: 'country.south_sudan', phone: '+211' }, + { code: 'ES', label: 'country.spain', phone: '+34' }, + { code: 'LK', label: 'country.sri_lanka', phone: '+94' }, + { code: 'SD', label: 'country.sudan', phone: '+249' }, + { code: 'SR', label: 'country.suriname', phone: '+597' }, + { code: 'SJ', label: 'country.svalbard_and_jan_mayen', phone: '+47' }, + { code: 'SE', label: 'country.sweden', phone: '+46' }, + { code: 'CH', label: 'country.switzerland', phone: '+41' }, + { code: 'SY', label: 'country.syria', phone: '+963' }, + { code: 'TW', label: 'country.taiwan', phone: '+886' }, + { code: 'TJ', label: 'country.tajikistan', phone: '+992' }, + { code: 'TZ', label: 'country.tanzania', phone: '+255' }, + { code: 'TH', label: 'country.thailand', phone: '+66' }, + { code: 'TL', label: 'country.timor_leste', phone: '+670' }, + { code: 'TG', label: 'country.togo', phone: '+228' }, + { code: 'TK', label: 'country.tokelau', phone: '+690' }, + { code: 'TO', label: 'country.tonga', phone: '+676' }, + { code: 'TT', label: 'country.trinidad_and_tobago', phone: '+1868' }, + { code: 'TN', label: 'country.tunisia', phone: '+216' }, + { code: 'TR', label: 'country.turkey', phone: '+90' }, + { code: 'TM', label: 'country.turkmenistan', phone: '+993' }, + { code: 'TC', label: 'country.turks_and_caicos_islands', phone: '+1649' }, + { code: 'TV', label: 'country.tuvalu', phone: '+688' }, + { code: 'VI', label: 'country.us_virgin_islands', phone: '+1340' }, + { code: 'UG', label: 'country.uganda', phone: '+256' }, + { code: 'UA', label: 'country.ukraine', phone: '+380' }, + { code: 'AE', label: 'country.united_arab_emirates', phone: '+971' }, + { code: 'GB', label: 'country.united_kingdom', phone: '+44' }, + { code: 'US', label: 'country.united_states', phone: '+1' }, + { code: 'UY', label: 'country.uruguay', phone: '+598' }, + { code: 'UZ', label: 'country.uzbekistan', phone: '+998' }, + { code: 'VU', label: 'country.vanuatu', phone: '+678' }, + { code: 'VA', label: 'country.vatican_city', phone: '+39' }, + { code: 'VE', label: 'country.venezuela', phone: '+58' }, + { code: 'VN', label: 'country.vietnam', phone: '+84' }, + { code: 'WF', label: 'country.wallis_and_futuna', phone: '+681' }, + { code: 'EH', label: 'country.western_sahara', phone: '+212' }, + { code: 'YE', label: 'country.yemen', phone: '+967' }, + { code: 'ZM', label: 'country.zambia', phone: '+260' }, + { code: 'ZW', label: 'country.zimbabwe', phone: '+263' }, ]; diff --git a/src/features/authentication/routes/LoginPage.tsx b/src/features/authentication/routes/LoginPage.tsx index d136967..ada147d 100644 --- a/src/features/authentication/routes/LoginPage.tsx +++ b/src/features/authentication/routes/LoginPage.tsx @@ -1,9 +1,15 @@ import { FlexBox } from '@/components/components/common/FlexBox'; import Logo from '@/components/Logo'; import { Paper } from '@mui/material'; -import { LoginForm } from '../components/LoginForm'; +import { SmsOtpForm } from '../components/SmsOtpForm'; +import { useState } from 'react'; export function LoginPage() { + const [phoneOrEmail, setPhoneOrEmail] = useState( + 'sajadmirjalili82@gmail.com', + ); + const [type, setType] = useState<'phone' | 'email'>('phone'); + return ( - + {/* */} + ); diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx index 2759d8f..ee1201e 100644 --- a/src/providers/CustomThemeProvider.tsx +++ b/src/providers/CustomThemeProvider.tsx @@ -19,7 +19,7 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ palette: lightPalette, }, dark: { - palette: darkPalette, + palette: darkPalette, }, }, cssVariables: { diff --git a/src/providers/RtlProvider.tsx b/src/providers/RtlProvider.tsx index 7bbf52a..eb37994 100644 --- a/src/providers/RtlProvider.tsx +++ b/src/providers/RtlProvider.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { CacheProvider } from '@emotion/react'; import createCache from '@emotion/cache'; @@ -9,17 +9,14 @@ export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const { i18n } = useTranslation(); - const [cache, setCache] = useState(createCache({ key: 'css' })); - useEffect(() => { - const newDir = i18n.dir(i18n.language); - - const newCache = createCache({ - key: 'css', - stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [], + const cacheRtl = useMemo(() => { + const isRtl = i18n.dir(i18n.language) === 'rtl'; + return createCache({ + key: isRtl ? 'muirtl' : 'muiltr', + stylisPlugins: isRtl ? [rtlPlugin] : [], }); - setCache(newCache); - }, [i18n, i18n.language]); + }, [i18n]); - return {children}; + return {children}; }; diff --git a/src/utils/regexes/isNumeric.ts b/src/utils/regexes/isNumeric.ts new file mode 100644 index 0000000..10e385d --- /dev/null +++ b/src/utils/regexes/isNumeric.ts @@ -0,0 +1 @@ +export const isNumeric = (value: string) => /^\d*$/.test(value); From 2e10a5496cf1f34bf100d4560d11b5000b9a3ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 26 Jul 2025 13:26:33 +0330 Subject: [PATCH 06/32] chore: LoginRegisterForm and AuthenticationContainer created --- eslint.config.js | 8 +- public/locales/en/authentication.json | 12 + public/locales/en/country.json | 247 ++++++++++++++++++ public/locales/fa/authentication.json | 5 +- public/locales/fa/common.json | 1 + public/locales/fa/country.json | 182 +++++++++++++ src/App.tsx | 4 +- .../components/AuthenticationContainer.tsx | 25 ++ .../components/LoginRegiserForm.tsx | 138 ++++++++++ .../authentication/components/SmsOtpForm.tsx | 2 +- .../{LoginPage.tsx => AuthenticationPage.tsx} | 11 +- .../authentication/types/auth-types.ts | 3 + src/theme/colors.ts | 5 +- src/utils/regexes/isEmail.tsx | 2 + 14 files changed, 628 insertions(+), 17 deletions(-) create mode 100644 public/locales/en/authentication.json create mode 100644 public/locales/en/country.json create mode 100644 public/locales/fa/country.json create mode 100644 src/features/authentication/components/AuthenticationContainer.tsx create mode 100644 src/features/authentication/components/LoginRegiserForm.tsx rename src/features/authentication/routes/{LoginPage.tsx => AuthenticationPage.tsx} (68%) create mode 100644 src/features/authentication/types/auth-types.ts create mode 100644 src/utils/regexes/isEmail.tsx diff --git a/eslint.config.js b/eslint.config.js index 75e9038..45e6782 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,8 +34,12 @@ export default tseslint.config( ...tseslint.configs.recommended.rules, ...reactHooks.configs.recommended.rules, ...prettierConfig.rules, - 'prettier/prettier': 'error', - 'linebreak-style': ['error', 'unix'], + 'prettier/prettier': [ + 'error', + { + "endOfLine": "auto" + } + ], 'react-refresh/only-export-components': 'warn', '@typescript-eslint/no-explicit-any': 'warn', }, diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json new file mode 100644 index 0000000..1d844c3 --- /dev/null +++ b/public/locales/en/authentication.json @@ -0,0 +1,12 @@ +{ + "loginForm": { + "title": "Login/Register", + "description": "Please enter your email/password to start", + "emailOrPhoneLabel": "Email/Password", + "submitButton": "Login/Register", + "loginWithGoogle": "Login with google", + "emailIsInvalid": "Email is invalid", + "phoneNumberIsInvalid": "Phone number is invalid", + "thisFieldIsRequired": "This field is requried" + } +} diff --git a/public/locales/en/country.json b/public/locales/en/country.json new file mode 100644 index 0000000..73a26ff --- /dev/null +++ b/public/locales/en/country.json @@ -0,0 +1,247 @@ +{ + "country.afghanistan": "Afghanistan", + "country.aland_islands": "Aland islands", + "country.albania": "Albania", + "country.algeria": "Algeria", + "country.american_samoa": "American samoa", + "country.andorra": "Andorra", + "country.angola": "Angola", + "country.anguilla": "Anguilla", + "country.antarctica": "Antarctica", + "country.antigua_and_barbuda": "Antigua and barbuda", + "country.argentina": "Argentina", + "country.armenia": "Armenia", + "country.aruba": "Aruba", + "country.australia": "Australia", + "country.austria": "Austria", + "country.azerbaijan": "Azerbaijan", + "country.bahamas": "Bahamas", + "country.bahrain": "Bahrain", + "country.bangladesh": "Bangladesh", + "country.barbados": "Barbados", + "country.belarus": "Belarus", + "country.belgium": "Belgium", + "country.belize": "Belize", + "country.benin": "Benin", + "country.bermuda": "Bermuda", + "country.bhutan": "Bhutan", + "country.bolivia": "Bolivia", + "country.bosnia_and_herzegovina": "Bosnia and herzegovina", + "country.botswana": "Botswana", + "country.brazil": "Brazil", + "country.british_indian_ocean_territory": "British indian ocean territory", + "country.british_virgin_islands": "British virgin islands", + "country.brunei": "Brunei", + "country.bulgaria": "Bulgaria", + "country.burkina_faso": "Burkina faso", + "country.burundi": "Burundi", + "country.cambodia": "Cambodia", + "country.cameroon": "Cameroon", + "country.canada": "Canada", + "country.cape_verde": "Cape verde", + "country.cayman_islands": "Cayman islands", + "country.central_african_republic": "Central african republic", + "country.chad": "Chad", + "country.chile": "Chile", + "country.china": "China", + "country.christmas_island": "Christmas island", + "country.cocos_keeling_islands": "Cocos keeling islands", + "country.colombia": "Colombia", + "country.comoros": "Comoros", + "country.congo_brazzaville": "Congo brazzaville", + "country.congo_kinshasa": "Congo kinshasa", + "country.cook_islands": "Cook islands", + "country.costa_rica": "Costa rica", + "country.cote_divoire": "Cote divoire", + "country.croatia": "Croatia", + "country.cuba": "Cuba", + "country.curacao": "Curacao", + "country.cyprus": "Cyprus", + "country.czech_republic": "Czech republic", + "country.denmark": "Denmark", + "country.djibouti": "Djibouti", + "country.dominica": "Dominica", + "country.dominican_republic": "Dominican republic", + "country.ecuador": "Ecuador", + "country.egypt": "Egypt", + "country.el_salvador": "El salvador", + "country.equatorial_guinea": "Equatorial guinea", + "country.eritrea": "Eritrea", + "country.estonia": "Estonia", + "country.eswatini": "Eswatini", + "country.ethiopia": "Ethiopia", + "country.falkland_islands": "Falkland islands", + "country.faroe_islands": "Faroe islands", + "country.fiji": "Fiji", + "country.finland": "Finland", + "country.france": "France", + "country.french_guiana": "French guiana", + "country.french_polynesia": "French polynesia", + "country.gabon": "Gabon", + "country.gambia": "Gambia", + "country.georgia": "Georgia", + "country.germany": "Germany", + "country.ghana": "Ghana", + "country.gibraltar": "Gibraltar", + "country.greece": "Greece", + "country.greenland": "Greenland", + "country.grenada": "Grenada", + "country.guadeloupe": "Guadeloupe", + "country.guam": "Guam", + "country.guatemala": "Guatemala", + "country.guernsey": "Guernsey", + "country.guinea": "Guinea", + "country.guinea_bissau": "Guinea bissau", + "country.guyana": "Guyana", + "country.haiti": "Haiti", + "country.honduras": "Honduras", + "country.hong_kong": "Hong kong", + "country.hungary": "Hungary", + "country.iceland": "Iceland", + "country.india": "India", + "country.indonesia": "Indonesia", + "country.iran": "Iran", + "country.iraq": "Iraq", + "country.ireland": "Ireland", + "country.isle_of_man": "Isle of man", + "country.israel": "Israel", + "country.italy": "Italy", + "country.jamaica": "Jamaica", + "country.japan": "Japan", + "country.jersey": "Jersey", + "country.jordan": "Jordan", + "country.kazakhstan": "Kazakhstan", + "country.kenya": "Kenya", + "country.kiribati": "Kiribati", + "country.kosovo": "Kosovo", + "country.kuwait": "Kuwait", + "country.kyrgyzstan": "Kyrgyzstan", + "country.laos": "Laos", + "country.latvia": "Latvia", + "country.lebanon": "Lebanon", + "country.lesotho": "Lesotho", + "country.liberia": "Liberia", + "country.libya": "Libya", + "country.liechtenstein": "Liechtenstein", + "country.lithuania": "Lithuania", + "country.luxembourg": "Luxembourg", + "country.macau": "Macau", + "country.madagascar": "Madagascar", + "country.malawi": "Malawi", + "country.malaysia": "Malaysia", + "country.maldives": "Maldives", + "country.mali": "Mali", + "country.malta": "Malta", + "country.marshall_islands": "Marshall islands", + "country.martinique": "Martinique", + "country.mauritania": "Mauritania", + "country.mauritius": "Mauritius", + "country.mayotte": "Mayotte", + "country.mexico": "Mexico", + "country.micronesia": "Micronesia", + "country.moldova": "Moldova", + "country.monaco": "Monaco", + "country.mongolia": "Mongolia", + "country.montenegro": "Montenegro", + "country.montserrat": "Montserrat", + "country.morocco": "Morocco", + "country.mozambique": "Mozambique", + "country.myanmar": "Myanmar", + "country.namibia": "Namibia", + "country.nauru": "Nauru", + "country.nepal": "Nepal", + "country.netherlands": "Netherlands", + "country.new_caledonia": "New caledonia", + "country.new_zealand": "New zealand", + "country.nicaragua": "Nicaragua", + "country.niger": "Niger", + "country.nigeria": "Nigeria", + "country.niue": "Niue", + "country.norfolk_island": "Norfolk island", + "country.north_korea": "North korea", + "country.north_macedonia": "North macedonia", + "country.northern_mariana_islands": "Northern mariana islands", + "country.norway": "Norway", + "country.oman": "Oman", + "country.pakistan": "Pakistan", + "country.palau": "Palau", + "country.palestine": "Palestine", + "country.panama": "Panama", + "country.papua_new_guinea": "Papua new guinea", + "country.paraguay": "Paraguay", + "country.peru": "Peru", + "country.philippines": "Philippines", + "country.pitcairn_islands": "Pitcairn islands", + "country.poland": "Poland", + "country.portugal": "Portugal", + "country.puerto_rico": "Puerto rico", + "country.qatar": "Qatar", + "country.reunion": "Reunion", + "country.romania": "Romania", + "country.russia": "Russia", + "country.rwanda": "Rwanda", + "country.saint_barthelemy": "Saint barthelemy", + "country.saint_helena": "Saint helena", + "country.saint_kitts_and_nevis": "Saint kitts and nevis", + "country.saint_lucia": "Saint lucia", + "country.saint_martin": "Saint martin", + "country.saint_pierre_and_miquelon": "Saint pierre and miquelon", + "country.saint_vincent_and_the_grenadines": "Saint vincent and the grenadines", + "country.samoa": "Samoa", + "country.san_marino": "San marino", + "country.sao_tome_and_principe": "Sao tome and principe", + "country.saudi_arabia": "Saudi arabia", + "country.senegal": "Senegal", + "country.serbia": "Serbia", + "country.seychelles": "Seychelles", + "country.sierra_leone": "Sierra leone", + "country.singapore": "Singapore", + "country.sint_maarten": "Sint maarten", + "country.slovakia": "Slovakia", + "country.slovenia": "Slovenia", + "country.solomon_islands": "Solomon islands", + "country.somalia": "Somalia", + "country.south_africa": "South africa", + "country.south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands", + "country.south_korea": "South korea", + "country.south_sudan": "South sudan", + "country.spain": "Spain", + "country.sri_lanka": "Sri lanka", + "country.sudan": "Sudan", + "country.suriname": "Suriname", + "country.svalbard_and_jan_mayen": "Svalbard and jan mayen", + "country.sweden": "Sweden", + "country.switzerland": "Switzerland", + "country.syria": "Syria", + "country.taiwan": "Taiwan", + "country.tajikistan": "Tajikistan", + "country.tanzania": "Tanzania", + "country.thailand": "Thailand", + "country.timor_leste": "Timor leste", + "country.togo": "Togo", + "country.tokelau": "Tokelau", + "country.tonga": "Tonga", + "country.trinidad_and_tobago": "Trinidad and tobago", + "country.tunisia": "Tunisia", + "country.turkey": "Turkey", + "country.turkmenistan": "Turkmenistan", + "country.turks_and_caicos_islands": "Turks and caicos islands", + "country.tuvalu": "Tuvalu", + "country.us_virgin_islands": "Us virgin islands", + "country.uganda": "Uganda", + "country.ukraine": "Ukraine", + "country.united_arab_emirates": "United arab emirates", + "country.united_kingdom": "United kingdom", + "country.united_states": "United states", + "country.uruguay": "Uruguay", + "country.uzbekistan": "Uzbekistan", + "country.vanuatu": "Vanuatu", + "country.vatican_city": "Vatican city", + "country.venezuela": "Venezuela", + "country.vietnam": "Vietnam", + "country.wallis_and_futuna": "Wallis and futuna", + "country.western_sahara": "Western sahara", + "country.yemen": "Yemen", + "country.zambia": "Zambia", + "country.zimbabwe": "Zimbabwe" + } \ No newline at end of file diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index d38deda..12d3a06 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -4,6 +4,9 @@ "description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.", "emailOrPhoneLabel": "شماره موبایل/ایمیل", "submitButton": "ورود/ثبت‌نام", - "loginWithGoogle": "ورود با گوگل" + "loginWithGoogle": "ورود با گوگل", + "emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد", + "phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد", + "thisFieldIsRequired": "این فیلد الزامی است" } } diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index 9c5b085..eb50682 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -2,6 +2,7 @@ "labels": { "search": "جست و جو" }, + "country" "messages": { "noResualtFound": "نتیجه ای یافت نشد." } diff --git a/public/locales/fa/country.json b/public/locales/fa/country.json new file mode 100644 index 0000000..d77b3aa --- /dev/null +++ b/public/locales/fa/country.json @@ -0,0 +1,182 @@ +{ + "country.afghanistan": "افغانستان", + "country.aland_islands": "جزایر آلند", + "country.albania": "آلبانی", + "country.algeria": "الجزایر", + "country.american_samoa": "ساموای آمریکایی", + "country.andorra": "آندورا", + "country.angola": "آنگولا", + "country.anguilla": "آنگویلا", + "country.antarctica": "جنوبگان", + "country.antigua_and_barbuda": "آنتیگوا و باربودا", + "country.argentina": "آرژانتین", + "country.armenia": "ارمنستان", + "country.aruba": "آروبا", + "country.australia": "استرالیا", + "country.austria": "اتریش", + "country.azerbaijan": "آذربایجان", + "country.bahamas": "باهاما", + "country.bahrain": "بحرین", + "country.bangladesh": "بنگلادش", + "country.barbados": "باربادوس", + "country.belarus": "بلاروس", + "country.belgium": "بلژیک", + "country.belize": "بلیز", + "country.benin": "بنین", + "country.bermuda": "برمودا", + "country.bhutan": "بوتان", + "country.bolivia": "بولیوی", + "country.bosnia_and_herzegovina": "بوسنی و هرزگوین", + "country.botswana": "بوتسوانا", + "country.brazil": "برزیل", + "country.british_virgin_islands": "جزایر ویرجین بریتانیا", + "country.brunei": "برونئی", + "country.bulgaria": "بلغارستان", + "country.burkina_faso": "بورکینافاسو", + "country.burundi": "بوروندی", + "country.cambodia": "کامبوج", + "country.cameroon": "کامرون", + "country.canada": "کانادا", + "country.cape_verde": "کیپ ورد", + "country.cayman_islands": "جزایر کیمن", + "country.central_african_republic": "جمهوری آفریقای مرکزی", + "country.chad": "چاد", + "country.chile": "شیلی", + "country.china": "چین", + "country.colombia": "کلمبیا", + "country.comoros": "کومور", + "country.costa_rica": "کاستاریکا", + "country.cote_divoire": "ساحل عاج", + "country.croatia": "کرواسی", + "country.cuba": "کوبا", + "country.cyprus": "قبرس", + "country.czech_republic": "جمهوری چک", + "country.denmark": "دانمارک", + "country.djibouti": "جیبوتی", + "country.dominica": "دومینیکا", + "country.dominican_republic": "جمهوری دومینیکن", + "country.ecuador": "اکوادور", + "country.egypt": "مصر", + "country.el_salvador": "السالوادور", + "country.equatorial_guinea": "گینه استوایی", + "country.eritrea": "اریتره", + "country.estonia": "استونی", + "country.eswatini": "سوازیلند", + "country.ethiopia": "اتیوپی", + "country.fiji": "فیجی", + "country.finland": "فنلاند", + "country.france": "فرانسه", + "country.gabon": "گابن", + "country.gambia": "گامبیا", + "country.georgia": "گرجستان", + "country.germany": "آلمان", + "country.ghana": "غنا", + "country.greece": "یونان", + "country.guatemala": "گواتمالا", + "country.guinea": "گینه", + "country.guinea_bissau": "گینه بیسائو", + "country.guyana": "گویان", + "country.haiti": "هائیتی", + "country.honduras": "هندوراس", + "country.hungary": "مجارستان", + "country.iceland": "ایسلند", + "country.india": "هندوستان", + "country.indonesia": "اندونزی", + "country.iran": "ایران", + "country.iraq": "عراق", + "country.ireland": "ایرلند", + "country.israel": "اسرائیل", + "country.italy": "ایتالیا", + "country.jamaica": "جامائیکا", + "country.japan": "ژاپن", + "country.jordan": "اردن", + "country.kazakhstan": "قزاقستان", + "country.kenya": "کنیا", + "country.kuwait": "کویت", + "country.kyrgyzstan": "قرقیزستان", + "country.laos": "لائوس", + "country.latvia": "لتونی", + "country.lebanon": "لبنان", + "country.lesotho": "لسوتو", + "country.liberia": "لیبریا", + "country.libya": "لیبی", + "country.luxembourg": "لوکزامبورگ", + "country.malaysia": "مالزی", + "country.maldives": "مالدیو", + "country.mali": "مالی", + "country.malta": "مالت", + "country.mauritania": "موریتانی", + "country.mauritius": "موریس", + "country.mexico": "مکزیک", + "country.moldova": "مولداوی", + "country.monaco": "موناکو", + "country.mongolia": "مغولستان", + "country.morocco": "مراکش", + "country.mozambique": "موزامبیک", + "country.myanmar": "میانمار", + "country.namibia": "نامیبیا", + "country.nepal": "نپال", + "country.netherlands": "هلند", + "country.new_zealand": "نیوزیلند", + "country.nicaragua": "نیکاراگوئه", + "country.niger": "نیجر", + "country.nigeria": "نیجریه", + "country.north_korea": "کره شمالی", + "country.north_macedonia": "مقدونیه", + "country.norway": "نروژ", + "country.oman": "عمان", + "country.pakistan": "پاکستان", + "country.palau": "پالائو", + "country.panama": "پاناما", + "country.papua_new_guinea": "پاپوآ گینه نو", + "country.paraguay": "پاراگوئه", + "country.peru": "پرو", + "country.philippines": "فیلیپین", + "country.poland": "لهستان", + "country.portugal": "پرتغال", + "country.qatar": "قطر", + "country.romania": "رومانی", + "country.russia": "روسیه", + "country.rwanda": "رواندا", + "country.saudi_arabia": "عربستان سعودی", + "country.senegal": "سنگال", + "country.serbia": "صربستان", + "country.seychelles": "سیشل", + "country.sierra_leone": "سیرالئون", + "country.singapore": "سنگاپور", + "country.south_africa": "آفریقای جنوبی", + "country.south_korea": "کره جنوبی", + "country.south_sudan": "سودان جنوبی", + "country.spain": "اسپانیا", + "country.sri_lanka": "سری‌لانکا", + "country.sudan": "سودان", + "country.suriname": "سورینام", + "country.sweden": "سوئد", + "country.switzerland": "سوئیس", + "country.syria": "سوریه", + "country.taiwan": "تایوان", + "country.tajikistan": "تاجیکستان", + "country.tanzania": "تانزانیا", + "country.thailand": "تایلند", + "country.timor_leste": "تیمور شرقی", + "country.togo": "توگو", + "country.tonga": "تونگا", + "country.trinidad_and_tobago": "ترینیداد و توباگو", + "country.tunisia": "تونس", + "country.turkey": "ترکیه", + "country.turkmenistan": "ترکمنستان", + "country.tuvalu": "تووالو", + "country.uganda": "اوگاندا", + "country.ukraine": "اوکراین", + "country.united_arab_emirates": "امارات متحده عربی", + "country.united_kingdom": "انگلستان", + "country.united_states": "ایالات متحده آمریکا", + "country.uruguay": "اروگوئه", + "country.uzbekistan": "ازبکستان", + "country.vanuatu": "وانواتو", + "country.venezuela": "ونزوئلا", + "country.vietnam": "ویتنام", + "country.yemen": "یمن", + "country.zambia": "زامبیا", + "country.zimbabwe": "زیمبابوه" + } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 1fcbd6a..0d50227 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,14 @@ import { CssBaseline } from '@mui/material'; import './App.css'; import { LanguageManager } from './components/LanguageManager'; -import { LoginPage } from './features/authentication/routes/LoginPage'; +import { AuthenticationPage } from './features/authentication/routes/AuthenticationPage'; function App() { return ( <> - + ); } diff --git a/src/features/authentication/components/AuthenticationContainer.tsx b/src/features/authentication/components/AuthenticationContainer.tsx new file mode 100644 index 0000000..5bca25c --- /dev/null +++ b/src/features/authentication/components/AuthenticationContainer.tsx @@ -0,0 +1,25 @@ +import React, { useState, type JSX } from 'react'; +import { LoginRegisterForm } from './LoginRegiserForm'; +import type { AuthMode, AuthType } from '../types/auth-types'; + +export const AuthenticationContainer = (): JSX.Element => { + const [authMode, setAuthMode] = useState('login'); + const [authType, setAuthType] = useState('phone'); + const [currentStep, setCurrentStep] = useState< + 'emailOrPassword' | 'verify' | 'enterPassword' + >('emailOrPassword'); + + const handleLoginRegister = (value: string) => {}; + + return ( + <> + {currentStep === 'emailOrPassword' && ( + + )} + + ); +}; diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/LoginRegiserForm.tsx new file mode 100644 index 0000000..c399826 --- /dev/null +++ b/src/features/authentication/components/LoginRegiserForm.tsx @@ -0,0 +1,138 @@ +import { Box, Button, Stack, TextField, Typography } from '@mui/material'; +import { useRef, useState, type Dispatch } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CountryCodeSelector } from './CountryCodeSelector'; +import { Google } from 'iconsax-reactjs'; +import { isNumeric } from '@/utils/regexes/isNumeric'; +import type { AuthMode, AuthType } from '../types/auth-types'; +import { isEmail } from '@/utils/regexes/isEmail'; + +export interface LoginRegisterFormProps { + authType: AuthType; + setAuthType: Dispatch; + onLoginRegisterSubmit: (value: string) => void; +} + +export function LoginRegisterForm({ + authType, + setAuthType, +}: LoginRegisterFormProps) { + const { t, i18n } = useTranslation('authentication'); + const [value, setValue] = useState(''); + const [countryCode, setCountryCode] = useState('+98'); + const textFieldRef = useRef(null); + const inputRef = useRef(null); + const dir = i18n.dir(); + const [error, setError] = useState(); + const [touched, setTouched] = useState(false); + const inputError: boolean = touched && !!error; + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + + // If the new value contains only digits (or is empty), it's a phone number + if (isNumeric(newValue)) { + setAuthType('phone'); + } else { + setAuthType('email'); + } + }; + + const handleBlur = () => { + setTouched(true); + validateInput(value, authType); + }; + + const validateInput = (value: string, authType: AuthType) => { + if (!value) { + setError(t('loginForm.thisFieldIsRequired')); + } else if (authType === 'email' && !isEmail(value)) { + setError(t('loginForm.emailIsInvalid')); + } else if (authType === 'phone' && false /* TODO */) { + setError(t('loginForm.emailIsInvalid')); + } else { + setError(undefined); + } + }; + + const isInputValid = (value: string, authType: AuthType): boolean => { + if (!value) { + return false; + } + + if (authType === 'email' && !isEmail(value)) { + return false; + } + + if (authType === 'phone' && false /* TODO */) { + return false; + } + + return true; + }; + + const handleSubmit = () => { + if (isInputValid(value, authType)) { + } else { + inputRef.current?.focus(); + validateInput(value, authType); + } + }; + + const showAdornment = authType === 'phone' && value.length > 0; + + return ( + + + {t('loginForm.title')} + + {t('loginForm.description')} + + + + + ), + endAdornment: dir === 'rtl' && ( + + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + + ); +} diff --git a/src/features/authentication/components/SmsOtpForm.tsx b/src/features/authentication/components/SmsOtpForm.tsx index ce03b0a..9ca0790 100644 --- a/src/features/authentication/components/SmsOtpForm.tsx +++ b/src/features/authentication/components/SmsOtpForm.tsx @@ -16,7 +16,7 @@ export function SmsOtpForm({ value, type }: SmsOtpProps) { ( - 'sajadmirjalili82@gmail.com', - ); - const [type, setType] = useState<'phone' | 'email'>('phone'); - +export function AuthenticationPage() { return ( - {/* */} - + ); diff --git a/src/features/authentication/types/auth-types.ts b/src/features/authentication/types/auth-types.ts new file mode 100644 index 0000000..3d8d2ad --- /dev/null +++ b/src/features/authentication/types/auth-types.ts @@ -0,0 +1,3 @@ +export type AuthType = 'email' | 'phone'; + +export type AuthMode = 'register' | 'login'; diff --git a/src/theme/colors.ts b/src/theme/colors.ts index 58bf0af..ce51d30 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -1,11 +1,10 @@ +import { blue } from '@mui/material/colors'; import type { Palette } from './color.type'; export const PALETTE: Palette = { primary: { light: { - main: '#212121', - dark: '#000000', - light: '#616161', + main: blue.A400, contrastText: '#FFFFFF', }, // TODO diff --git a/src/utils/regexes/isEmail.tsx b/src/utils/regexes/isEmail.tsx new file mode 100644 index 0000000..b50768d --- /dev/null +++ b/src/utils/regexes/isEmail.tsx @@ -0,0 +1,2 @@ +export const isEmail = (value: string) => + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value); From 62747f6ca8292f82e50b6d401602ac12efd8ddba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 26 Jul 2025 13:42:18 +0330 Subject: [PATCH 07/32] chore: phone number validation added --- package-lock.json | 7 + package.json | 1 + public/locales/en/common.json | 249 +++++++++++++++++- public/locales/fa/common.json | 183 ++++++++++++- .../components/CountryCodeSelector.tsx | 3 +- .../authentication/components/LoginForm.tsx | 90 ------- .../components/LoginRegiserForm.tsx | 24 +- 7 files changed, 451 insertions(+), 106 deletions(-) delete mode 100644 src/features/authentication/components/LoginForm.tsx diff --git a/package-lock.json b/package-lock.json index eb709eb..9e9436f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "iconsax-reactjs": "^0.0.8", + "libphonenumber-js": "^1.12.10", "react": "^19.1.0", "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", @@ -3306,6 +3307,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.10", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz", + "integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", diff --git a/package.json b/package.json index 22e1fc7..91e1810 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "iconsax-reactjs": "^0.0.8", + "libphonenumber-js": "^1.12.10", "react": "^19.1.0", "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index bc4b6ab..72b36d8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,3 +1,250 @@ { - "helloWorld": "hello world" + "helloWorld": "hello world", + "country": { + "afghanistan": "Afghanistan", + "aland_islands": "Aland islands", + "albania": "Albania", + "algeria": "Algeria", + "american_samoa": "American samoa", + "andorra": "Andorra", + "angola": "Angola", + "anguilla": "Anguilla", + "antarctica": "Antarctica", + "antigua_and_barbuda": "Antigua and barbuda", + "argentina": "Argentina", + "armenia": "Armenia", + "aruba": "Aruba", + "australia": "Australia", + "austria": "Austria", + "azerbaijan": "Azerbaijan", + "bahamas": "Bahamas", + "bahrain": "Bahrain", + "bangladesh": "Bangladesh", + "barbados": "Barbados", + "belarus": "Belarus", + "belgium": "Belgium", + "belize": "Belize", + "benin": "Benin", + "bermuda": "Bermuda", + "bhutan": "Bhutan", + "bolivia": "Bolivia", + "bosnia_and_herzegovina": "Bosnia and herzegovina", + "botswana": "Botswana", + "brazil": "Brazil", + "british_indian_ocean_territory": "British indian ocean territory", + "british_virgin_islands": "British virgin islands", + "brunei": "Brunei", + "bulgaria": "Bulgaria", + "burkina_faso": "Burkina faso", + "burundi": "Burundi", + "cambodia": "Cambodia", + "cameroon": "Cameroon", + "canada": "Canada", + "cape_verde": "Cape verde", + "cayman_islands": "Cayman islands", + "central_african_republic": "Central african republic", + "chad": "Chad", + "chile": "Chile", + "china": "China", + "christmas_island": "Christmas island", + "cocos_keeling_islands": "Cocos keeling islands", + "colombia": "Colombia", + "comoros": "Comoros", + "congo_brazzaville": "Congo brazzaville", + "congo_kinshasa": "Congo kinshasa", + "cook_islands": "Cook islands", + "costa_rica": "Costa rica", + "cote_divoire": "Cote divoire", + "croatia": "Croatia", + "cuba": "Cuba", + "curacao": "Curacao", + "cyprus": "Cyprus", + "czech_republic": "Czech republic", + "denmark": "Denmark", + "djibouti": "Djibouti", + "dominica": "Dominica", + "dominican_republic": "Dominican republic", + "ecuador": "Ecuador", + "egypt": "Egypt", + "el_salvador": "El salvador", + "equatorial_guinea": "Equatorial guinea", + "eritrea": "Eritrea", + "estonia": "Estonia", + "eswatini": "Eswatini", + "ethiopia": "Ethiopia", + "falkland_islands": "Falkland islands", + "faroe_islands": "Faroe islands", + "fiji": "Fiji", + "finland": "Finland", + "france": "France", + "french_guiana": "French guiana", + "french_polynesia": "French polynesia", + "gabon": "Gabon", + "gambia": "Gambia", + "georgia": "Georgia", + "germany": "Germany", + "ghana": "Ghana", + "gibraltar": "Gibraltar", + "greece": "Greece", + "greenland": "Greenland", + "grenada": "Grenada", + "guadeloupe": "Guadeloupe", + "guam": "Guam", + "guatemala": "Guatemala", + "guernsey": "Guernsey", + "guinea": "Guinea", + "guinea_bissau": "Guinea bissau", + "guyana": "Guyana", + "haiti": "Haiti", + "honduras": "Honduras", + "hong_kong": "Hong kong", + "hungary": "Hungary", + "iceland": "Iceland", + "india": "India", + "indonesia": "Indonesia", + "iran": "Iran", + "iraq": "Iraq", + "ireland": "Ireland", + "isle_of_man": "Isle of man", + "israel": "Israel", + "italy": "Italy", + "jamaica": "Jamaica", + "japan": "Japan", + "jersey": "Jersey", + "jordan": "Jordan", + "kazakhstan": "Kazakhstan", + "kenya": "Kenya", + "kiribati": "Kiribati", + "kosovo": "Kosovo", + "kuwait": "Kuwait", + "kyrgyzstan": "Kyrgyzstan", + "laos": "Laos", + "latvia": "Latvia", + "lebanon": "Lebanon", + "lesotho": "Lesotho", + "liberia": "Liberia", + "libya": "Libya", + "liechtenstein": "Liechtenstein", + "lithuania": "Lithuania", + "luxembourg": "Luxembourg", + "macau": "Macau", + "madagascar": "Madagascar", + "malawi": "Malawi", + "malaysia": "Malaysia", + "maldives": "Maldives", + "mali": "Mali", + "malta": "Malta", + "marshall_islands": "Marshall islands", + "martinique": "Martinique", + "mauritania": "Mauritania", + "mauritius": "Mauritius", + "mayotte": "Mayotte", + "mexico": "Mexico", + "micronesia": "Micronesia", + "moldova": "Moldova", + "monaco": "Monaco", + "mongolia": "Mongolia", + "montenegro": "Montenegro", + "montserrat": "Montserrat", + "morocco": "Morocco", + "mozambique": "Mozambique", + "myanmar": "Myanmar", + "namibia": "Namibia", + "nauru": "Nauru", + "nepal": "Nepal", + "netherlands": "Netherlands", + "new_caledonia": "New caledonia", + "new_zealand": "New zealand", + "nicaragua": "Nicaragua", + "niger": "Niger", + "nigeria": "Nigeria", + "niue": "Niue", + "norfolk_island": "Norfolk island", + "north_korea": "North korea", + "north_macedonia": "North macedonia", + "northern_mariana_islands": "Northern mariana islands", + "norway": "Norway", + "oman": "Oman", + "pakistan": "Pakistan", + "palau": "Palau", + "palestine": "Palestine", + "panama": "Panama", + "papua_new_guinea": "Papua new guinea", + "paraguay": "Paraguay", + "peru": "Peru", + "philippines": "Philippines", + "pitcairn_islands": "Pitcairn islands", + "poland": "Poland", + "portugal": "Portugal", + "puerto_rico": "Puerto rico", + "qatar": "Qatar", + "reunion": "Reunion", + "romania": "Romania", + "russia": "Russia", + "rwanda": "Rwanda", + "saint_barthelemy": "Saint barthelemy", + "saint_helena": "Saint helena", + "saint_kitts_and_nevis": "Saint kitts and nevis", + "saint_lucia": "Saint lucia", + "saint_martin": "Saint martin", + "saint_pierre_and_miquelon": "Saint pierre and miquelon", + "saint_vincent_and_the_grenadines": "Saint vincent and the grenadines", + "samoa": "Samoa", + "san_marino": "San marino", + "sao_tome_and_principe": "Sao tome and principe", + "saudi_arabia": "Saudi arabia", + "senegal": "Senegal", + "serbia": "Serbia", + "seychelles": "Seychelles", + "sierra_leone": "Sierra leone", + "singapore": "Singapore", + "sint_maarten": "Sint maarten", + "slovakia": "Slovakia", + "slovenia": "Slovenia", + "solomon_islands": "Solomon islands", + "somalia": "Somalia", + "south_africa": "South africa", + "south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands", + "south_korea": "South korea", + "south_sudan": "South sudan", + "spain": "Spain", + "sri_lanka": "Sri lanka", + "sudan": "Sudan", + "suriname": "Suriname", + "svalbard_and_jan_mayen": "Svalbard and jan mayen", + "sweden": "Sweden", + "switzerland": "Switzerland", + "syria": "Syria", + "taiwan": "Taiwan", + "tajikistan": "Tajikistan", + "tanzania": "Tanzania", + "thailand": "Thailand", + "timor_leste": "Timor leste", + "togo": "Togo", + "tokelau": "Tokelau", + "tonga": "Tonga", + "trinidad_and_tobago": "Trinidad and tobago", + "tunisia": "Tunisia", + "turkey": "Turkey", + "turkmenistan": "Turkmenistan", + "turks_and_caicos_islands": "Turks and caicos islands", + "tuvalu": "Tuvalu", + "us_virgin_islands": "Us virgin islands", + "uganda": "Uganda", + "ukraine": "Ukraine", + "united_arab_emirates": "United arab emirates", + "united_kingdom": "United kingdom", + "united_states": "United states", + "uruguay": "Uruguay", + "uzbekistan": "Uzbekistan", + "vanuatu": "Vanuatu", + "vatican_city": "Vatican city", + "venezuela": "Venezuela", + "vietnam": "Vietnam", + "wallis_and_futuna": "Wallis and futuna", + "western_sahara": "Western sahara", + "yemen": "Yemen", + "zambia": "Zambia", + "zimbabwe": "Zimbabwe" + } } diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index eb50682..71ba38f 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -2,7 +2,188 @@ "labels": { "search": "جست و جو" }, - "country" + "country": { + "afghanistan": "افغانستان", + "aland_islands": "جزایر آلند", + "albania": "آلبانی", + "algeria": "الجزایر", + "american_samoa": "ساموای آمریکایی", + "andorra": "آندورا", + "angola": "آنگولا", + "anguilla": "آنگویلا", + "antarctica": "جنوبگان", + "antigua_and_barbuda": "آنتیگوا و باربودا", + "argentina": "آرژانتین", + "armenia": "ارمنستان", + "aruba": "آروبا", + "australia": "استرالیا", + "austria": "اتریش", + "azerbaijan": "آذربایجان", + "bahamas": "باهاما", + "bahrain": "بحرین", + "bangladesh": "بنگلادش", + "barbados": "باربادوس", + "belarus": "بلاروس", + "belgium": "بلژیک", + "belize": "بلیز", + "benin": "بنین", + "bermuda": "برمودا", + "bhutan": "بوتان", + "bolivia": "بولیوی", + "bosnia_and_herzegovina": "بوسنی و هرزگوین", + "botswana": "بوتسوانا", + "brazil": "برزیل", + "british_virgin_islands": "جزایر ویرجین بریتانیا", + "brunei": "برونئی", + "bulgaria": "بلغارستان", + "burkina_faso": "بورکینافاسو", + "burundi": "بوروندی", + "cambodia": "کامبوج", + "cameroon": "کامرون", + "canada": "کانادا", + "cape_verde": "کیپ ورد", + "cayman_islands": "جزایر کیمن", + "central_african_republic": "جمهوری آفریقای مرکزی", + "chad": "چاد", + "chile": "شیلی", + "china": "چین", + "colombia": "کلمبیا", + "comoros": "کومور", + "costa_rica": "کاستاریکا", + "cote_divoire": "ساحل عاج", + "croatia": "کرواسی", + "cuba": "کوبا", + "cyprus": "قبرس", + "czech_republic": "جمهوری چک", + "denmark": "دانمارک", + "djibouti": "جیبوتی", + "dominica": "دومینیکا", + "dominican_republic": "جمهوری دومینیکن", + "ecuador": "اکوادور", + "egypt": "مصر", + "el_salvador": "السالوادور", + "equatorial_guinea": "گینه استوایی", + "eritrea": "اریتره", + "estonia": "استونی", + "eswatini": "سوازیلند", + "ethiopia": "اتیوپی", + "fiji": "فیجی", + "finland": "فنلاند", + "france": "فرانسه", + "gabon": "گابن", + "gambia": "گامبیا", + "georgia": "گرجستان", + "germany": "آلمان", + "ghana": "غنا", + "greece": "یونان", + "guatemala": "گواتمالا", + "guinea": "گینه", + "guinea_bissau": "گینه بیسائو", + "guyana": "گویان", + "haiti": "هائیتی", + "honduras": "هندوراس", + "hungary": "مجارستان", + "iceland": "ایسلند", + "india": "هندوستان", + "indonesia": "اندونزی", + "iran": "ایران", + "iraq": "عراق", + "ireland": "ایرلند", + "israel": "اسرائیل", + "italy": "ایتالیا", + "jamaica": "جامائیکا", + "japan": "ژاپن", + "jordan": "اردن", + "kazakhstan": "قزاقستان", + "kenya": "کنیا", + "kuwait": "کویت", + "kyrgyzstan": "قرقیزستان", + "laos": "لائوس", + "latvia": "لتونی", + "lebanon": "لبنان", + "lesotho": "لسوتو", + "liberia": "لیبریا", + "libya": "لیبی", + "luxembourg": "لوکزامبورگ", + "malaysia": "مالزی", + "maldives": "مالدیو", + "mali": "مالی", + "malta": "مالت", + "mauritania": "موریتانی", + "mauritius": "موریس", + "mexico": "مکزیک", + "moldova": "مولداوی", + "monaco": "موناکو", + "mongolia": "مغولستان", + "morocco": "مراکش", + "mozambique": "موزامبیک", + "myanmar": "میانمار", + "namibia": "نامیبیا", + "nepal": "نپال", + "netherlands": "هلند", + "new_zealand": "نیوزیلند", + "nicaragua": "نیکاراگوئه", + "niger": "نیجر", + "nigeria": "نیجریه", + "north_korea": "کره شمالی", + "north_macedonia": "مقدونیه", + "norway": "نروژ", + "oman": "عمان", + "pakistan": "پاکستان", + "palau": "پالائو", + "panama": "پاناما", + "papua_new_guinea": "پاپوآ گینه نو", + "paraguay": "پاراگوئه", + "peru": "پرو", + "philippines": "فیلیپین", + "poland": "لهستان", + "portugal": "پرتغال", + "qatar": "قطر", + "romania": "رومانی", + "russia": "روسیه", + "rwanda": "رواندا", + "saudi_arabia": "عربستان سعودی", + "senegal": "سنگال", + "serbia": "صربستان", + "seychelles": "سیشل", + "sierra_leone": "سیرالئون", + "singapore": "سنگاپور", + "south_africa": "آفریقای جنوبی", + "south_korea": "کره جنوبی", + "south_sudan": "سودان جنوبی", + "spain": "اسپانیا", + "sri_lanka": "سری‌لانکا", + "sudan": "سودان", + "suriname": "سورینام", + "sweden": "سوئد", + "switzerland": "سوئیس", + "syria": "سوریه", + "taiwan": "تایوان", + "tajikistan": "تاجیکستان", + "tanzania": "تانزانیا", + "thailand": "تایلند", + "timor_leste": "تیمور شرقی", + "togo": "توگو", + "tonga": "تونگا", + "trinidad_and_tobago": "ترینیداد و توباگو", + "tunisia": "تونس", + "turkey": "ترکیه", + "turkmenistan": "ترکمنستان", + "tuvalu": "تووالو", + "uganda": "اوگاندا", + "ukraine": "اوکراین", + "united_arab_emirates": "امارات متحده عربی", + "united_kingdom": "انگلستان", + "united_states": "ایالات متحده آمریکا", + "uruguay": "اروگوئه", + "uzbekistan": "ازبکستان", + "vanuatu": "وانواتو", + "venezuela": "ونزوئلا", + "vietnam": "ویتنام", + "yemen": "یمن", + "zambia": "زامبیا", + "zimbabwe": "زیمبابوه" + }, "messages": { "noResualtFound": "نتیجه ای یافت نشد." } diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx index ea4d89d..6028d6b 100644 --- a/src/features/authentication/components/CountryCodeSelector.tsx +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -72,6 +72,7 @@ export function CountryCodeSelector({ () => countries.filter( (country) => + t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) || country.label.toLowerCase().includes(searchTerm.toLowerCase()) || country.phone.includes(searchTerm), ), @@ -205,7 +206,7 @@ export function CountryCodeSelector({ }} /> - + {country.phone} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx deleted file mode 100644 index 0584f23..0000000 --- a/src/features/authentication/components/LoginForm.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Box, Button, Stack, TextField, Typography } from '@mui/material'; -import { useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { CountryCodeSelector } from './CountryCodeSelector'; -import { Google } from 'iconsax-reactjs'; -import { isNumeric } from '@/utils/regexes/isNumeric'; - -export function LoginForm() { - const { t, i18n } = useTranslation('authentication'); - const [value, setValue] = useState(''); - const [countryCode, setCountryCode] = useState('+41'); - const [inputType, setInputType] = useState<'phone' | 'email'>('phone'); - const textFieldRef = useRef(null); - const inputRef = useRef(null); - const dir = i18n.dir(); - - const handleInputChange = (event: React.ChangeEvent) => { - const newValue = event.target.value; - setValue(newValue); - - // If the new value contains only digits (or is empty), it's a phone number - if (isNumeric(newValue)) { - setInputType('phone'); - } else { - setInputType('email'); - } - }; - - const handleBlur = () => { - // setTouched(true); - // setError(validate(value, inputType)); - }; - - const showAdornment = inputType === 'phone' && value.length > 0; - - return ( - - - {t('loginForm.title')} - - {t('loginForm.description')} - - - - - ), - endAdornment: dir === 'rtl' && ( - - ), - }, - }} - sx={{ my: 4 }} - /> - - - - - - - ); -} diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/LoginRegiserForm.tsx index c399826..233eb66 100644 --- a/src/features/authentication/components/LoginRegiserForm.tsx +++ b/src/features/authentication/components/LoginRegiserForm.tsx @@ -6,6 +6,7 @@ import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; import type { AuthMode, AuthType } from '../types/auth-types'; import { isEmail } from '@/utils/regexes/isEmail'; +import parsePhoneNumberFromString from 'libphonenumber-js'; export interface LoginRegisterFormProps { authType: AuthType; @@ -49,13 +50,19 @@ export function LoginRegisterForm({ setError(t('loginForm.thisFieldIsRequired')); } else if (authType === 'email' && !isEmail(value)) { setError(t('loginForm.emailIsInvalid')); - } else if (authType === 'phone' && false /* TODO */) { - setError(t('loginForm.emailIsInvalid')); + } else if (authType === 'phone' && !isPhoneValid(countryCode, value)) { + setError(t('loginForm.phoneNumberIsInvalid')); } else { setError(undefined); } }; + const isPhoneValid = (code: string, phone: string) => { + const phoneNumber = parsePhoneNumberFromString(code + phone); + + return phoneNumber && phoneNumber.isValid(); + }; + const isInputValid = (value: string, authType: AuthType): boolean => { if (!value) { return false; @@ -65,7 +72,7 @@ export function LoginRegisterForm({ return false; } - if (authType === 'phone' && false /* TODO */) { + if (authType === 'phone' && !isPhoneValid(countryCode, value)) { return false; } @@ -104,16 +111,7 @@ export function LoginRegisterForm({ slotProps={{ htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } }, input: { - startAdornment: dir === 'ltr' && ( - - ), - endAdornment: dir === 'rtl' && ( + endAdornment: ( Date: Sat, 26 Jul 2025 16:53:01 +0330 Subject: [PATCH 08/32] multiple messages for phone login and signup added --- public/locales/en/authentication.json | 7 +- public/locales/fa/authentication.json | 7 ++ .../components/AuthenticationContainer.tsx | 27 ++++++- .../components/LoginRegiserForm.tsx | 19 +++-- .../components/OtpVerifyForm.tsx | 73 +++++++++++++++++++ .../authentication/components/SmsOtpForm.tsx | 2 +- 6 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 src/features/authentication/components/OtpVerifyForm.tsx diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index 1d844c3..8cda9bb 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -7,6 +7,11 @@ "loginWithGoogle": "Login with google", "emailIsInvalid": "Email is invalid", "phoneNumberIsInvalid": "Phone number is invalid", - "thisFieldIsRequired": "This field is requried" + "thisFieldIsRequired": "This field is requried", + "verify": { + "verify": "Verify", + "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.", + "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account." + } } } diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index 12d3a06..e1dbe20 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -8,5 +8,12 @@ "emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد", "phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد", "thisFieldIsRequired": "این فیلد الزامی است" + }, + "verify": { + "verify": "اعتبارسنجی", + "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.", + "confirmAndLogin": "تایید و ورود", + "confirmAndContinue": "تایید و ادامه", + "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید." } } diff --git a/src/features/authentication/components/AuthenticationContainer.tsx b/src/features/authentication/components/AuthenticationContainer.tsx index 5bca25c..8b8ed9c 100644 --- a/src/features/authentication/components/AuthenticationContainer.tsx +++ b/src/features/authentication/components/AuthenticationContainer.tsx @@ -1,25 +1,46 @@ import React, { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; import type { AuthMode, AuthType } from '../types/auth-types'; +import { OtpVerifyForm } from './OtpVerifyForm'; export const AuthenticationContainer = (): JSX.Element => { - const [authMode, setAuthMode] = useState('login'); + const [authMode, setAuthMode] = useState('register'); const [authType, setAuthType] = useState('phone'); const [currentStep, setCurrentStep] = useState< 'emailOrPassword' | 'verify' | 'enterPassword' - >('emailOrPassword'); + >('verify'); + const [loginRegisterValue, setLoginRegisterValue] = + useState('9152814093'); - const handleLoginRegister = (value: string) => {}; + const handleLoginRegister = (value: string) => { + setLoginRegisterValue(value); + setCurrentStep('verify'); + }; + + const handleEditValue = () => { + setCurrentStep('emailOrPassword'); + }; return ( <> {currentStep === 'emailOrPassword' && ( )} + + {currentStep === 'verify' && ( + + )} ); }; diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/LoginRegiserForm.tsx index 233eb66..edf9296 100644 --- a/src/features/authentication/components/LoginRegiserForm.tsx +++ b/src/features/authentication/components/LoginRegiserForm.tsx @@ -9,17 +9,21 @@ import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; export interface LoginRegisterFormProps { + loginRegisterValue: string; + setLoginRegisterValue: Dispatch; authType: AuthType; setAuthType: Dispatch; onLoginRegisterSubmit: (value: string) => void; } export function LoginRegisterForm({ + loginRegisterValue, + setLoginRegisterValue, authType, setAuthType, + onLoginRegisterSubmit, }: LoginRegisterFormProps) { const { t, i18n } = useTranslation('authentication'); - const [value, setValue] = useState(''); const [countryCode, setCountryCode] = useState('+98'); const textFieldRef = useRef(null); const inputRef = useRef(null); @@ -30,7 +34,7 @@ export function LoginRegisterForm({ const handleInputChange = (event: React.ChangeEvent) => { const newValue = event.target.value; - setValue(newValue); + setLoginRegisterValue(newValue); // If the new value contains only digits (or is empty), it's a phone number if (isNumeric(newValue)) { @@ -42,7 +46,7 @@ export function LoginRegisterForm({ const handleBlur = () => { setTouched(true); - validateInput(value, authType); + validateInput(loginRegisterValue, authType); }; const validateInput = (value: string, authType: AuthType) => { @@ -80,14 +84,15 @@ export function LoginRegisterForm({ }; const handleSubmit = () => { - if (isInputValid(value, authType)) { + if (isInputValid(loginRegisterValue, authType)) { + onLoginRegisterSubmit(loginRegisterValue); } else { inputRef.current?.focus(); - validateInput(value, authType); + validateInput(loginRegisterValue, authType); } }; - const showAdornment = authType === 'phone' && value.length > 0; + const showAdornment = authType === 'phone' && loginRegisterValue.length > 0; return ( @@ -102,7 +107,7 @@ export function LoginRegisterForm({ ref={textFieldRef} inputRef={inputRef} label={t('loginForm.emailOrPhoneLabel')} - value={value} + value={loginRegisterValue} onChange={handleInputChange} onBlur={handleBlur} error={inputError} diff --git a/src/features/authentication/components/OtpVerifyForm.tsx b/src/features/authentication/components/OtpVerifyForm.tsx new file mode 100644 index 0000000..da80299 --- /dev/null +++ b/src/features/authentication/components/OtpVerifyForm.tsx @@ -0,0 +1,73 @@ +import { useTranslation } from 'react-i18next'; +import { Box, Button, Typography } from '@mui/material'; +import { Edit2 } from 'iconsax-reactjs'; +import DigitInput from '@/components/components/DigitsInput'; +import type { AuthMode, AuthType } from '../types/auth-types'; + +interface OtpVerifyFormProps { + value: string; + authType: AuthType; + authMode: AuthMode; + onEditValue: () => void; +} + +export function OtpVerifyForm({ + value, + authType, + authMode, + onEditValue, +}: OtpVerifyFormProps) { + const { t } = useTranslation('authentication'); + + const otpMessage = (): string => { + if (authType === 'phone' && authMode === 'login') { + return t( + 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt', + ); + } else if (authType === 'phone' && authMode === 'register') { + return t( + 'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount', + ); + } + + return ''; + }; + + return ( + + + {t('verify.verify')} + + + + + + {otpMessage()} + + + console.log(value)} /> + + + ); +} diff --git a/src/features/authentication/components/SmsOtpForm.tsx b/src/features/authentication/components/SmsOtpForm.tsx index 9ca0790..ce03b0a 100644 --- a/src/features/authentication/components/SmsOtpForm.tsx +++ b/src/features/authentication/components/SmsOtpForm.tsx @@ -16,7 +16,7 @@ export function SmsOtpForm({ value, type }: SmsOtpProps) { Date: Sun, 27 Jul 2025 15:05:46 +0330 Subject: [PATCH 09/32] feat: OTP verify status and status messages added --- public/locales/en/authentication.json | 8 ++- public/locales/fa/authentication.json | 7 +- src/components/Toast.tsx | 23 ++++++ src/components/components/DigitsInput.tsx | 15 +++- .../components/AuthenticationContainer.tsx | 21 ++++-- .../components/OtpVerifyForm.tsx | 71 ++++++++++++++++++- src/theme/colors.ts | 14 ++-- 7 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 src/components/Toast.tsx diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index 8cda9bb..18287d4 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -11,7 +11,13 @@ "verify": { "verify": "Verify", "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.", - "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account." + "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.", + "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.", + "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.", + "theVerificationCodeIsIncorrect": "The verification code is incorrect.", + "youHaveSuccessfullyLoggedIn": "You have successfully logged in", + "youHaveSuccessfullySignedIn": "You have successfully signed in" } } } + \ No newline at end of file diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index e1dbe20..72a9536 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -14,6 +14,11 @@ "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.", "confirmAndLogin": "تایید و ورود", "confirmAndContinue": "تایید و ادامه", - "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید." + "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید.", + "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "کد تایید ۴ رقمی به شماره ایمیل شما ارسال شد. لطفا آن را وارد کنید.", + "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.", + "theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد", + "youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید", + "youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد" } } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..947194f --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,23 @@ +import { Alert, Snackbar, type AlertColor } from '@mui/material'; +import React, { type PropsWithChildren } from 'react'; + +export interface ToastProps extends PropsWithChildren { + color: AlertColor | undefined; + open: boolean; + onClose: () => void; +} + +export const Toast = ({ color, open, onClose, children }: ToastProps) => { + return ( + + + {children} + + + ); +}; diff --git a/src/components/components/DigitsInput.tsx b/src/components/components/DigitsInput.tsx index de4f054..90d8b36 100644 --- a/src/components/components/DigitsInput.tsx +++ b/src/components/components/DigitsInput.tsx @@ -9,10 +9,16 @@ import React, { import { TextField, Stack } from '@mui/material'; interface DigitInputProps { + error: boolean; + success: boolean; onChange: Dispatch>; } -const DigitInput: React.FC = ({ onChange }) => { +const DigitInput: React.FC = ({ + onChange, + error, + success, +}) => { const [code, setCode] = useState(['', '', '', '']); const inputRefs = useRef>([]); @@ -74,6 +80,8 @@ const DigitInput: React.FC = ({ onChange }) => { > {code.map((digit, index) => ( (inputRefs.current[index] = el)} value={digit} @@ -85,6 +93,11 @@ const DigitInput: React.FC = ({ onChange }) => { maxLength: 1, sx: { height: '72px', + color: error + ? 'error.main' + : success + ? 'success.main' + : 'text.primary', }, style: { textAlign: 'center', diff --git a/src/features/authentication/components/AuthenticationContainer.tsx b/src/features/authentication/components/AuthenticationContainer.tsx index 8b8ed9c..5148e47 100644 --- a/src/features/authentication/components/AuthenticationContainer.tsx +++ b/src/features/authentication/components/AuthenticationContainer.tsx @@ -2,21 +2,31 @@ import React, { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; import type { AuthMode, AuthType } from '../types/auth-types'; import { OtpVerifyForm } from './OtpVerifyForm'; +import { isNumeric } from '@/utils/regexes/isNumeric'; export const AuthenticationContainer = (): JSX.Element => { - const [authMode, setAuthMode] = useState('register'); + const [authMode, setAuthMode] = useState('login'); const [authType, setAuthType] = useState('phone'); const [currentStep, setCurrentStep] = useState< - 'emailOrPassword' | 'verify' | 'enterPassword' - >('verify'); - const [loginRegisterValue, setLoginRegisterValue] = - useState('9152814093'); + 'emailOrPassword' | 'verify' | 'enterPassword' | 'addPhoneNumber' + >('emailOrPassword'); + const [loginRegisterValue, setLoginRegisterValue] = useState(''); const handleLoginRegister = (value: string) => { setLoginRegisterValue(value); + setAuthType(isNumeric(value) ? 'phone' : 'email'); setCurrentStep('verify'); }; + const handleOTPVerfied = (otpCode: string) => { + console.log(otpCode); + + if (authMode === 'register' && authType === 'email') { + setAuthType('phone'); + setCurrentStep('addPhoneNumber'); + } + }; + const handleEditValue = () => { setCurrentStep('emailOrPassword'); }; @@ -35,6 +45,7 @@ export const AuthenticationContainer = (): JSX.Element => { {currentStep === 'verify' && ( void; + onOTPVerified: (otpCode: string) => void; } export function OtpVerifyForm({ @@ -16,9 +19,38 @@ export function OtpVerifyForm({ authType, authMode, onEditValue, + onOTPVerified, }: OtpVerifyFormProps) { + const [otpCode, setOtpCode] = useState(''); + const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); + const [verifyStatus, setVerifyStatus] = useState< + 'loading' | 'success' | 'failed' + >(); + const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); const { t } = useTranslation('authentication'); + const handleDigitInputChange = (value: string[]) => { + const formattedValue = value.filter((char) => char !== '').join(''); + + setOtpCode(formattedValue); + }; + + const handleVerifyOTP = () => { + if (!otpCode || otpCode.length < 4) { + setOtpDigitInvalid(true); + } else { + setOtpDigitInvalid(false); + setVerifyStatus('loading'); + // Change setTimeout to api call + + setTimeout(() => { + setVerifyAlertOpen(true); + setVerifyStatus('success'); + onOTPVerified(otpCode); + }, 1000); + } + }; + const otpMessage = (): string => { if (authType === 'phone' && authMode === 'login') { return t( @@ -28,6 +60,26 @@ export function OtpVerifyForm({ return t( 'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount', ); + } else if (authType === 'email' && authMode === 'login') { + return t( + 'verify.a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt', + ); + } else if (authType === 'email' && authMode === 'register') { + return t( + 'verify.thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount', + ); + } + + return ''; + }; + + const verifyAlertMessage = (): string => { + if (verifyStatus === 'failed') { + return t('verify.theVerificationCodeIsIncorrect'); + } else if (verifyStatus === 'success' && authMode === 'register') { + return t('verify.youHaveSuccessfullySignedIn'); + } else if (verifyStatus === 'success' && authMode === 'login') { + return t('verify.youHaveSuccessfullyLoggedIn'); } return ''; @@ -35,6 +87,14 @@ export function OtpVerifyForm({ return ( + setVerifyAlertOpen(false)} + color={verifyStatus === 'failed' ? 'error' : 'success'} + > + {verifyAlertMessage()} + + - console.log(value)} /> - + + ); +}; diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx index ee1201e..374cea1 100644 --- a/src/providers/CustomThemeProvider.tsx +++ b/src/providers/CustomThemeProvider.tsx @@ -46,7 +46,7 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({ }, [i18n]); return ( - + {children} ); From 3f7242742e625f71bcf03f60ea3459dccb61352a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Mon, 28 Jul 2025 13:02:24 +0330 Subject: [PATCH 13/32] feat: otp resend timer and logic added --- public/locales/en/authentication.json | 5 +- public/locales/fa/authentication.json | 5 +- .../components/AuthenticationCard.tsx | 18 +++ .../components/CompleteSignUp.tsx | 7 +- .../components/LoginRegiserForm.tsx | 14 +- .../components/OtpVerifyForm.tsx | 142 +++++++++++++----- .../authentication/components/SmsOtpForm.tsx | 44 ------ .../routes/AuthenticationPage.tsx | 12 +- 8 files changed, 143 insertions(+), 104 deletions(-) create mode 100644 src/features/authentication/components/AuthenticationCard.tsx delete mode 100644 src/features/authentication/components/SmsOtpForm.tsx diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index ef0caca..7b70125 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -16,7 +16,10 @@ "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.", "theVerificationCodeIsIncorrect": "The verification code is incorrect.", "youHaveSuccessfullyLoggedIn": "You have successfully logged in", - "youHaveSuccessfullySignedIn": "You have successfully signed in" + "youHaveSuccessfullySignedIn": "You have successfully signed in", + "resendCodeIn": "Resend code in", + "moreMinute": "minute", + "resendCode": "Resend code" }, "completeSignUp": { "completeSignUp": "Complete Sign Up", diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index ad86fc7..224d34d 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -19,7 +19,10 @@ "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.", "theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد", "youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید", - "youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد" + "youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد", + "resendCodeIn": "ارسال مجدد کد تا", + "moreMinute": "دقیقه دیگر", + "resendCode": "ارسال مجدد" }, "completeSignUp": { "completeSignUp": "تکمیل ثبت نام", diff --git a/src/features/authentication/components/AuthenticationCard.tsx b/src/features/authentication/components/AuthenticationCard.tsx new file mode 100644 index 0000000..9e82632 --- /dev/null +++ b/src/features/authentication/components/AuthenticationCard.tsx @@ -0,0 +1,18 @@ +import { Paper } from '@mui/material'; +import React, { type PropsWithChildren } from 'react'; + +// Beacuse in the otp verify there is a element outside of the authentication card +export const AuthenticationCard = ({ children }: PropsWithChildren) => { + return ( + + {children} + + ); +}; diff --git a/src/features/authentication/components/CompleteSignUp.tsx b/src/features/authentication/components/CompleteSignUp.tsx index ccf8eb6..098396b 100644 --- a/src/features/authentication/components/CompleteSignUp.tsx +++ b/src/features/authentication/components/CompleteSignUp.tsx @@ -1,8 +1,9 @@ -import { Box, Button, TextField, Typography } from '@mui/material'; +import { Box, Button, Paper, TextField, Typography } from '@mui/material'; import parsePhoneNumberFromString from 'libphonenumber-js'; import React, { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { CountryCodeSelector } from './CountryCodeSelector'; +import { AuthenticationCard } from './AuthenticationCard'; export interface CompleteSignUpProps { email: string; @@ -59,7 +60,7 @@ export const CompleteSignUp = ({ }; return ( - + {t('completeSignUp.completeSignUp')} @@ -101,6 +102,6 @@ export const CompleteSignUp = ({ - + ); }; diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/LoginRegiserForm.tsx index edf9296..a851d14 100644 --- a/src/features/authentication/components/LoginRegiserForm.tsx +++ b/src/features/authentication/components/LoginRegiserForm.tsx @@ -1,4 +1,11 @@ -import { Box, Button, Stack, TextField, Typography } from '@mui/material'; +import { + Box, + Button, + Paper, + Stack, + TextField, + Typography, +} from '@mui/material'; import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { CountryCodeSelector } from './CountryCodeSelector'; @@ -7,6 +14,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric'; import type { AuthMode, AuthType } from '../types/auth-types'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; +import { AuthenticationCard } from './AuthenticationCard'; export interface LoginRegisterFormProps { loginRegisterValue: string; @@ -95,7 +103,7 @@ export function LoginRegisterForm({ const showAdornment = authType === 'phone' && loginRegisterValue.length > 0; return ( - + {t('loginForm.title')} @@ -136,6 +144,6 @@ export function LoginRegisterForm({ {t('loginForm.loginWithGoogle')} - + ); } diff --git a/src/features/authentication/components/OtpVerifyForm.tsx b/src/features/authentication/components/OtpVerifyForm.tsx index 0748f42..f958bfc 100644 --- a/src/features/authentication/components/OtpVerifyForm.tsx +++ b/src/features/authentication/components/OtpVerifyForm.tsx @@ -1,10 +1,11 @@ import { useTranslation } from 'react-i18next'; -import { Alert, Box, Button, Snackbar, Typography } from '@mui/material'; +import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; import type { AuthMode, AuthType } from '../types/auth-types'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; +import { AuthenticationCard } from './AuthenticationCard'; interface OtpVerifyFormProps { value: string; @@ -28,6 +29,42 @@ export function OtpVerifyForm({ useState(false); const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); const { t } = useTranslation('authentication'); + const [resendTimer, setResendTimer] = useState(120); + const [canResend, setCanResend] = useState(false); + const [resendLoading, setResendLoading] = useState(false); + + useEffect(() => { + let interval: NodeJS.Timeout; + if (resendTimer > 0) { + interval = setInterval(() => { + setResendTimer((prev) => prev - 1); + }, 1000); + } else { + setCanResend(true); + } + + return () => clearInterval(interval); + }, [resendTimer]); + + const handleResendOTPCode = () => { + setResendLoading(true); + + // TODO: Call API here instead of settimeout + + setTimeout(() => { + console.log('resended'); + + setResendTimer(120); + setCanResend(false); + setResendLoading(false); + }, 1000); + }; + + const formatTime = (seconds: number) => { + const min = Math.floor(seconds / 60); + const sec = seconds % 60; + return `${min}:${sec.toString().padStart(2, '0')}`; + }; const handleDigitInputChange = (value: string[]) => { const formattedValue = value.filter((char) => char !== '').join(''); @@ -87,53 +124,76 @@ export function OtpVerifyForm({ }; return ( - - setVerifyAlertOpen(false)} - color={verifyStatus === 'failed' ? 'error' : 'success'} - > - {verifyAlertMessage()} - + + + setVerifyAlertOpen(false)} + color={verifyStatus === 'failed' ? 'error' : 'success'} + > + {verifyAlertMessage()} + - + {t('verify.verify')} + + + + + + {otpMessage()} + + + handleDigitInputChange(value as string[])} + /> + + + + + - {t('verify.verify')} + {t('verify.resendCodeIn')} - - - - {otpMessage()} - - - handleDigitInputChange(value as string[])} - /> - - - + + ); } diff --git a/src/features/authentication/components/SmsOtpForm.tsx b/src/features/authentication/components/SmsOtpForm.tsx deleted file mode 100644 index ce03b0a..0000000 --- a/src/features/authentication/components/SmsOtpForm.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { Box, Button, Typography } from '@mui/material'; -import { Edit2 } from 'iconsax-reactjs'; -import DigitInput from '@/components/components/DigitsInput'; - -interface SmsOtpProps { - value: string; - type: 'phone' | 'email'; -} - -export function SmsOtpForm({ value, type }: SmsOtpProps) { - const { t } = useTranslation('authentication'); - - return ( - - - اعتبارسنجی - - - - - کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید. - - console.log(value)} /> - - - ); -} diff --git a/src/features/authentication/routes/AuthenticationPage.tsx b/src/features/authentication/routes/AuthenticationPage.tsx index 158d0a8..b23f6c5 100644 --- a/src/features/authentication/routes/AuthenticationPage.tsx +++ b/src/features/authentication/routes/AuthenticationPage.tsx @@ -1,7 +1,6 @@ import { FlexBox } from '@/components/components/common/FlexBox'; import Logo from '@/components/Logo'; import { Paper } from '@mui/material'; -import { SmsOtpForm } from '../components/SmsOtpForm'; import { useState } from 'react'; import { AuthenticationContainer } from '../components/AuthenticationContainer'; @@ -17,16 +16,7 @@ export function AuthenticationPage() { }} > - - - + ); } From ab223af15b37106f87baa112c9232bf6c73c48c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Mon, 28 Jul 2025 16:43:57 +0330 Subject: [PATCH 14/32] chore: EnterPasswordForm component created --- .../components/AuthenticationContainer.tsx | 11 +++++++++-- .../components/EnterPasswordForm.tsx | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/features/authentication/components/EnterPasswordForm.tsx diff --git a/src/features/authentication/components/AuthenticationContainer.tsx b/src/features/authentication/components/AuthenticationContainer.tsx index ed1f8c8..fc2055f 100644 --- a/src/features/authentication/components/AuthenticationContainer.tsx +++ b/src/features/authentication/components/AuthenticationContainer.tsx @@ -4,6 +4,7 @@ import type { AuthMode, AuthType } from '../types/auth-types'; import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; +import { EnterPasswordForm } from './EnterPasswordForm'; export const AuthenticationContainer = (): JSX.Element => { const [authMode, setAuthMode] = useState('register'); @@ -27,7 +28,6 @@ export const AuthenticationContainer = (): JSX.Element => { const handleOTPVerfied = (otpCode: string) => { if (authMode === 'register' && authType === 'email') { - setAuthType('phone'); setCurrentStep('addPhoneNumber'); } }; @@ -70,6 +70,13 @@ export const AuthenticationContainer = (): JSX.Element => { /> )} + {currentStep === 'enterPassword' && ( + {}} + emailOrPhone={loginRegisterValue} + /> + )} + {currentStep === 'addPhoneNumber' && ( { diff --git a/src/features/authentication/components/EnterPasswordForm.tsx b/src/features/authentication/components/EnterPasswordForm.tsx new file mode 100644 index 0000000..72ce370 --- /dev/null +++ b/src/features/authentication/components/EnterPasswordForm.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { AuthenticationCard } from './AuthenticationCard'; + +export interface EnterPasswordFormProps { + onLoginWithPassword: () => void; + emailOrPhone: string; +} + +export const EnterPasswordForm = ({ + onLoginWithPassword, + emailOrPhone, +}: EnterPasswordFormProps) => { + return ; +}; From bd34468332e1d1e3a746c563ae3b029764b01b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Tue, 29 Jul 2025 13:06:39 +0330 Subject: [PATCH 15/32] feat: login with password form added --- public/locales/en/authentication.json | 45 +++--- public/locales/fa/authentication.json | 7 + .../components/AuthenticationCard.tsx | 2 +- .../components/AuthenticationContainer.tsx | 16 +- .../components/EnterPasswordForm.tsx | 140 +++++++++++++++++- 5 files changed, 183 insertions(+), 27 deletions(-) diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index 7b70125..2414a85 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -7,24 +7,31 @@ "loginWithGoogle": "Login with google", "emailIsInvalid": "Email is invalid", "phoneNumberIsInvalid": "Phone number is invalid", - "thisFieldIsRequired": "This field is requried", - "verify": { - "verify": "Verify", - "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.", - "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.", - "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.", - "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.", - "theVerificationCodeIsIncorrect": "The verification code is incorrect.", - "youHaveSuccessfullyLoggedIn": "You have successfully logged in", - "youHaveSuccessfullySignedIn": "You have successfully signed in", - "resendCodeIn": "Resend code in", - "moreMinute": "minute", - "resendCode": "Resend code" - }, - "completeSignUp": { - "completeSignUp": "Complete Sign Up", - "emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "Email {{ email }} has been successfully verified. Please enter your contact number to continue.", - "phoneNumber": "Phone number" - } + "thisFieldIsRequired": "This field is requried" + }, + "verify": { + "verify": "Verify", + "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.", + "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.", + "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.", + "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.", + "theVerificationCodeIsIncorrect": "The verification code is incorrect.", + "youHaveSuccessfullyLoggedIn": "You have successfully logged in", + "youHaveSuccessfullySignedIn": "You have successfully signed in", + "resendCodeIn": "Resend code in", + "moreMinute": "minute", + "resendCode": "Resend code" + }, + "completeSignUp": { + "completeSignUp": "Complete Sign Up", + "emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "Email {{ email }} has been successfully verified. Please enter your contact number to continue.", + "phoneNumber": "Phone number" + }, + "enterPassword": { + "loginWithPassword": "Login with password", + "enterThePasswordYouSetForYourAccount": "Enter the password you set for your account.", + "loginPassword": "Login password", + "loginWithOneTimeCode": "Login with one-time code", + "iForgotMyPassword": "I forgot my password." } } diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index 224d34d..ff6f92c 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -28,5 +28,12 @@ "completeSignUp": "تکمیل ثبت نام", "emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "ایمیل {{ email }} با موفقیت تایید شد. برای ادامه لطفا شماره تماس خود را وارد کنید", "phoneNumber": "شماره تماس" + }, + "enterPassword": { + "loginWithPassword": "ورود با رمز", + "enterThePasswordYouSetForYourAccount": "رمز ورودی که برای اکانت خود تعیین کردید را وارد کنید", + "loginPassword": "رمز ورود", + "loginWithOneTimeCode": "ورود با کد یکبار مصرف", + "iForgotMyPassword": "رمز ورودم را فراموش کردم" } } diff --git a/src/features/authentication/components/AuthenticationCard.tsx b/src/features/authentication/components/AuthenticationCard.tsx index 9e82632..bde57cd 100644 --- a/src/features/authentication/components/AuthenticationCard.tsx +++ b/src/features/authentication/components/AuthenticationCard.tsx @@ -7,7 +7,7 @@ export const AuthenticationCard = ({ children }: PropsWithChildren) => { { const handleLoginRegister = (value: string) => { setLoginRegisterValue(value); setAuthType(isNumeric(value) ? 'phone' : 'email'); - setCurrentStep('verify'); + + // TODO: after api: send to password if it has account and has password + if (true) { + setCurrentStep('enterPassword'); + } else { + setCurrentStep('verify'); + } }; const handleOTPVerfied = (otpCode: string) => { @@ -45,9 +51,11 @@ export const AuthenticationContainer = (): JSX.Element => { }; const handleCompleteSignUpEditValue = () => { - setCurrentStep('addPhoneNumber'); + setCurrentStep('emailOrPassword'); }; + const handleLoggedInWithPassowrd = () => {}; + return ( <> {currentStep === 'emailOrPassword' && ( @@ -72,7 +80,9 @@ export const AuthenticationContainer = (): JSX.Element => { {currentStep === 'enterPassword' && ( {}} + onLoggedIn={handleLoggedInWithPassowrd} + onEditValue={handleEditValue} + onLoginWithOTP={() => setCurrentStep('verify')} emailOrPhone={loginRegisterValue} /> )} diff --git a/src/features/authentication/components/EnterPasswordForm.tsx b/src/features/authentication/components/EnterPasswordForm.tsx index 72ce370..5414a66 100644 --- a/src/features/authentication/components/EnterPasswordForm.tsx +++ b/src/features/authentication/components/EnterPasswordForm.tsx @@ -1,14 +1,146 @@ -import React from 'react'; +import React, { useRef, useState } from 'react'; import { AuthenticationCard } from './AuthenticationCard'; +import { ArrowLeft, Edit2, Eye, EyeSlash, MaskLeft } from 'iconsax-reactjs'; +import { + Box, + Button, + IconButton, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { Toast } from '@/components/Toast'; export interface EnterPasswordFormProps { - onLoginWithPassword: () => void; + onEditValue: () => void; + onLoginWithOTP: () => void; + onLoggedIn: () => void; emailOrPhone: string; } export const EnterPasswordForm = ({ - onLoginWithPassword, + onEditValue, + onLoginWithOTP, + onLoggedIn, emailOrPhone, }: EnterPasswordFormProps) => { - return ; + const { t } = useTranslation('authentication'); + const [passValue, setPassValue] = useState(''); + const [inputTouched, setInputTouched] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const inputRef = useRef(null); + const [loginLoading, setLoginLoading] = useState(false); + const [loginStatus, setLoginStatus] = useState<'success' | 'failed'>(); + const [loginAlertOpen, setLoginAlertOpen] = useState(false); + const [loginFailedMessage, setLoginFailedMessage] = useState(''); + + const handleBlur = () => { + setInputTouched(true); + }; + + const handleSubmit = () => { + if (!passValue) { + inputRef.current?.focus(); + } else { + setLoginLoading(true); + + // Change setTimeout to api call + setTimeout(() => { + setLoginAlertOpen(true); + // setLoginStatus('success'); + setLoginStatus('failed'); + setLoginFailedMessage('رمز عبور اشتباه میباشد'); + onLoggedIn(); + setLoginLoading(false); + }, 1000); + } + }; + + return ( + + setLoginAlertOpen(false)} + color={loginStatus === 'failed' ? 'error' : 'success'} + > + {loginStatus === 'failed' + ? loginFailedMessage + : t('verify.youHaveSuccessfullyLoggedIn')} + + + + + {t('enterPassword.loginWithPassword')} + + + + + + + {t('enterPassword.enterThePasswordYouSetForYourAccount')} + + + setPassValue(e.target.value)} + onBlur={handleBlur} + error={!passValue && inputTouched} + helperText={ + !passValue && inputTouched ? t('loginForm.thisFieldIsRequired') : '' + } + autoFocus + slotProps={{ + htmlInput: { sx: { lineHeight: 1.5 } }, + input: { + endAdornment: ( + setShowPassword(!showPassword)} + > + {showPassword ? : } + + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + + + + ); }; From 9191ea31fa0fda9a7f7a158f6ac4908f07e1ad62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Tue, 29 Jul 2025 16:51:20 +0330 Subject: [PATCH 16/32] chore: authentication container changed to auth steps --- .../AuthenticationSteps.tsx} | 4 ++-- .../components/{ => AuthenticationSteps}/CompleteSignUp.tsx | 2 +- .../{ => AuthenticationSteps}/CountryCodeSelector.tsx | 2 +- .../{ => AuthenticationSteps}/EnterPasswordForm.tsx | 2 +- .../components/{ => AuthenticationSteps}/LoginRegiserForm.tsx | 4 ++-- .../components/{ => AuthenticationSteps}/OtpVerifyForm.tsx | 4 ++-- src/features/authentication/routes/AuthenticationPage.tsx | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) rename src/features/authentication/components/{AuthenticationContainer.tsx => AuthenticationSteps/AuthenticationSteps.tsx} (96%) rename src/features/authentication/components/{ => AuthenticationSteps}/CompleteSignUp.tsx (98%) rename src/features/authentication/components/{ => AuthenticationSteps}/CountryCodeSelector.tsx (99%) rename src/features/authentication/components/{ => AuthenticationSteps}/EnterPasswordForm.tsx (98%) rename src/features/authentication/components/{ => AuthenticationSteps}/LoginRegiserForm.tsx (97%) rename src/features/authentication/components/{ => AuthenticationSteps}/OtpVerifyForm.tsx (97%) diff --git a/src/features/authentication/components/AuthenticationContainer.tsx b/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx similarity index 96% rename from src/features/authentication/components/AuthenticationContainer.tsx rename to src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx index 67a98e4..b88a506 100644 --- a/src/features/authentication/components/AuthenticationContainer.tsx +++ b/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -1,12 +1,12 @@ import React, { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; -import type { AuthMode, AuthType } from '../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/auth-types'; import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; -export const AuthenticationContainer = (): JSX.Element => { +export const AuthenticationSteps = (): JSX.Element => { const [authMode, setAuthMode] = useState('register'); const [authType, setAuthType] = useState('phone'); const [currentStep, setCurrentStep] = useState< diff --git a/src/features/authentication/components/CompleteSignUp.tsx b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx similarity index 98% rename from src/features/authentication/components/CompleteSignUp.tsx rename to src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx index 098396b..bdd2f7d 100644 --- a/src/features/authentication/components/CompleteSignUp.tsx +++ b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx @@ -3,7 +3,7 @@ import parsePhoneNumberFromString from 'libphonenumber-js'; import React, { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { CountryCodeSelector } from './CountryCodeSelector'; -import { AuthenticationCard } from './AuthenticationCard'; +import { AuthenticationCard } from '../AuthenticationCard'; export interface CompleteSignUpProps { email: string; diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx similarity index 99% rename from src/features/authentication/components/CountryCodeSelector.tsx rename to src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx index 6028d6b..dde39a4 100644 --- a/src/features/authentication/components/CountryCodeSelector.tsx +++ b/src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx @@ -10,7 +10,7 @@ import { Typography, } from '@mui/material'; import { useMemo, useRef, useState, type RefObject } from 'react'; -import { countries, type Country } from '../data/countries'; +import { countries, type Country } from '../../data/countries'; import { ArrowDown2 } from 'iconsax-reactjs'; import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; diff --git a/src/features/authentication/components/EnterPasswordForm.tsx b/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx similarity index 98% rename from src/features/authentication/components/EnterPasswordForm.tsx rename to src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx index 5414a66..579a551 100644 --- a/src/features/authentication/components/EnterPasswordForm.tsx +++ b/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState } from 'react'; -import { AuthenticationCard } from './AuthenticationCard'; +import { AuthenticationCard } from '../AuthenticationCard'; import { ArrowLeft, Edit2, Eye, EyeSlash, MaskLeft } from 'iconsax-reactjs'; import { Box, diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx similarity index 97% rename from src/features/authentication/components/LoginRegiserForm.tsx rename to src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx index a851d14..1c28e08 100644 --- a/src/features/authentication/components/LoginRegiserForm.tsx +++ b/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -11,10 +11,10 @@ import { useTranslation } from 'react-i18next'; import { CountryCodeSelector } from './CountryCodeSelector'; import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; -import type { AuthMode, AuthType } from '../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/auth-types'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; -import { AuthenticationCard } from './AuthenticationCard'; +import { AuthenticationCard } from '../AuthenticationCard'; export interface LoginRegisterFormProps { loginRegisterValue: string; diff --git a/src/features/authentication/components/OtpVerifyForm.tsx b/src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx similarity index 97% rename from src/features/authentication/components/OtpVerifyForm.tsx rename to src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx index f958bfc..1997972 100644 --- a/src/features/authentication/components/OtpVerifyForm.tsx +++ b/src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next'; import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; -import type { AuthMode, AuthType } from '../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/auth-types'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; -import { AuthenticationCard } from './AuthenticationCard'; +import { AuthenticationCard } from '../AuthenticationCard'; interface OtpVerifyFormProps { value: string; diff --git a/src/features/authentication/routes/AuthenticationPage.tsx b/src/features/authentication/routes/AuthenticationPage.tsx index b23f6c5..7cad998 100644 --- a/src/features/authentication/routes/AuthenticationPage.tsx +++ b/src/features/authentication/routes/AuthenticationPage.tsx @@ -2,7 +2,7 @@ import { FlexBox } from '@/components/components/common/FlexBox'; import Logo from '@/components/Logo'; import { Paper } from '@mui/material'; import { useState } from 'react'; -import { AuthenticationContainer } from '../components/AuthenticationContainer'; +import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps'; export function AuthenticationPage() { return ( @@ -16,7 +16,7 @@ export function AuthenticationPage() { }} > - + ); } From a2afdddf04bdf6d12a78ecaaea5db51363726bb8 Mon Sep 17 00:00:00 2001 From: mehrzadghdev Date: Fri, 8 Aug 2025 00:46:05 +0330 Subject: [PATCH 17/32] feat: forget password steps and pages added --- public/locales/en/authentication.json | 16 ++ public/locales/fa/authentication.json | 16 ++ .../AuthenticationSteps/CompleteSignUp.tsx | 2 +- .../AuthenticationSteps/LoginRegiserForm.tsx | 2 +- .../CountryCodeSelector.tsx | 2 +- .../ForgetPassword/ChangePassword.tsx | 239 ++++++++++++++++++ .../ForgetPasswordContainer.tsx | 62 +++++ .../ForgetPassword/ForgetPasswordOtp.tsx | 168 ++++++++++++ .../ForgetPassword/ForgettedPasswordInfo.tsx | 151 +++++++++++ .../routes/AuthenticationPage.tsx | 3 +- src/utils/regexes/containsNumber.tsx | 1 + src/utils/regexes/containsSymbol.tsx | 1 + src/utils/regexes/hasUpperAndLowerLetter.tsx | 5 + src/utils/regexes/least8Chars.tsx | 1 + 14 files changed, 665 insertions(+), 4 deletions(-) rename src/features/authentication/components/{AuthenticationSteps => }/CountryCodeSelector.tsx (99%) create mode 100644 src/features/authentication/components/ForgetPassword/ChangePassword.tsx create mode 100644 src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx create mode 100644 src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx create mode 100644 src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx create mode 100644 src/utils/regexes/containsNumber.tsx create mode 100644 src/utils/regexes/containsSymbol.tsx create mode 100644 src/utils/regexes/hasUpperAndLowerLetter.tsx create mode 100644 src/utils/regexes/least8Chars.tsx diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json index 2414a85..6633ea7 100644 --- a/public/locales/en/authentication.json +++ b/public/locales/en/authentication.json @@ -33,5 +33,21 @@ "loginPassword": "Login password", "loginWithOneTimeCode": "Login with one-time code", "iForgotMyPassword": "I forgot my password." + }, + "forgetPassword": { + "forgetPassword": "Forget password", + "pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "Please enter your mobile number/email to recover your password.", + "anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "An email containing a recovery code has been sent to this email address.", + "anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "An recovery code has been sent to this phone number.", + "confirm": "Confirm", + "changePassword": "Change password", + "createANewPassword": "Create a new password", + "newPassword": "New password", + "includingANumber": "Including a number", + "atLeast8Characters": "At least 8 characters", + "containsAnUppercaseAndLowercaseLetter": "Contains an uppercase and lowercase letter", + "ContainsASymbol": "Contains the symbol (!@#$%&*^)", + "confirmPassword": "Confirm password", + "passwordChangedSuccessfully": "Password changed successfully" } } diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json index ff6f92c..bfb8610 100644 --- a/public/locales/fa/authentication.json +++ b/public/locales/fa/authentication.json @@ -35,5 +35,21 @@ "loginPassword": "رمز ورود", "loginWithOneTimeCode": "ورود با کد یکبار مصرف", "iForgotMyPassword": "رمز ورودم را فراموش کردم" + }, + "forgetPassword": { + "forgetPassword": "فراموشی رمز", + "pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "لطفا برای بازیابی رمز عبور شماره موبایل/ایمیل خود را وارد کنید.", + "anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "یک ایمیل حاوی کد بازیابی به این ایمیل ارسال شد", + "anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "یک کد بازیابی به این شماره ارسال شد", + "confirm": "تایید", + "changePassword": "تغییر رمز عبور", + "createANewPassword": "یک رمز عبور جدید ایجاد کنید", + "newPassword": "رمز عبور جدید", + "includingANumber": "شامل عدد", + "atLeast8Characters": "حداقل ۸ حرف", + "containsAnUppercaseAndLowercaseLetter": "شامل یک حرف بزرگ و کوچک", + "ContainsASymbol": "شامل علامت (!@#$%&*^)", + "confirmPassword": "تکرار رمز عبور", + "passwordChangedSuccessfully": "رمز عبور با موفقیت تغییر یافت" } } diff --git a/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx index bdd2f7d..74a5cb3 100644 --- a/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx +++ b/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx @@ -2,8 +2,8 @@ import { Box, Button, Paper, TextField, Typography } from '@mui/material'; import parsePhoneNumberFromString from 'libphonenumber-js'; import React, { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; -import { CountryCodeSelector } from './CountryCodeSelector'; import { AuthenticationCard } from '../AuthenticationCard'; +import { CountryCodeSelector } from '../CountryCodeSelector'; export interface CompleteSignUpProps { email: string; diff --git a/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx index 1c28e08..df79514 100644 --- a/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx +++ b/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -8,13 +8,13 @@ import { } from '@mui/material'; import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; -import { CountryCodeSelector } from './CountryCodeSelector'; import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; import type { AuthMode, AuthType } from '../../types/auth-types'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; +import { CountryCodeSelector } from '../CountryCodeSelector'; export interface LoginRegisterFormProps { loginRegisterValue: string; diff --git a/src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx similarity index 99% rename from src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx rename to src/features/authentication/components/CountryCodeSelector.tsx index dde39a4..427bc8a 100644 --- a/src/features/authentication/components/AuthenticationSteps/CountryCodeSelector.tsx +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -10,11 +10,11 @@ import { Typography, } from '@mui/material'; import { useMemo, useRef, useState, type RefObject } from 'react'; -import { countries, type Country } from '../../data/countries'; import { ArrowDown2 } from 'iconsax-reactjs'; import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; +import { countries, type Country } from '../data/countries'; interface CountryCodeSelectorProps { show: boolean; value: string; diff --git a/src/features/authentication/components/ForgetPassword/ChangePassword.tsx b/src/features/authentication/components/ForgetPassword/ChangePassword.tsx new file mode 100644 index 0000000..a83732b --- /dev/null +++ b/src/features/authentication/components/ForgetPassword/ChangePassword.tsx @@ -0,0 +1,239 @@ +import React, { useRef, useState } from 'react'; +import { AuthenticationCard } from '../AuthenticationCard'; +import { + ArrowLeft, + Edit2, + Eye, + EyeSlash, + MaskLeft, + TickCircle, +} from 'iconsax-reactjs'; +import { + Box, + Button, + IconButton, + Stack, + TextField, + Typography, + useTheme, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { Toast } from '@/components/Toast'; +import { containsNumber } from '@/utils/regexes/containsNumber'; +import { containsSymbol } from '@/utils/regexes/containsSymbol'; +import { least8Chars } from '@/utils/regexes/least8Chars'; +import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter'; + +export interface ChangePasswordProps { + onEditInfo: () => void; + onPasswordChanged: () => void; + forgettedPasswordInfo: string; +} + +export const ChangePassword = ({ + onEditInfo, + onPasswordChanged, + forgettedPasswordInfo, +}: ChangePasswordProps) => { + const theme = useTheme(); + const { t } = useTranslation('authentication'); + const [passValue, setPassValue] = useState(''); + const [confirmPassValue, setConfirmPassValue] = useState(''); + const [inputTouched, setInputTouched] = useState(false); + const [confirmInputTouched, setConfirmInputTouched] = + useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = + useState(false); + const inputRef = useRef(null); + const confirmInputRef = useRef(null); + const [changePasswordLoading, setChangePasswordLoading] = + useState(false); + const [changePasswordStatus, setChangePasswordStatus] = useState< + 'success' | 'failed' + >(); + const [changePassAlertOpen, setChangePassAlertOpen] = + useState(false); + const [changePassFailedMessage, setChangePassFailedMessage] = + useState(''); + + const passwordValidationRules = [ + { title: t('forgetPassword.includingANumber'), validator: containsNumber }, + { title: t('forgetPassword.atLeast8Characters'), validator: least8Chars }, + { + title: t('forgetPassword.containsAnUppercaseAndLowercaseLetter'), + validator: hasUpperAndLowerLetter, + }, + { title: t('forgetPassword.ContainsASymbol'), validator: containsSymbol }, + ]; + + const handleBlur = () => { + setInputTouched(true); + }; + + const handleConfirmPassBlur = () => { + setConfirmInputTouched(true); + }; + + const handleSubmit = () => { + if (!passValue || !isValidPassword(passValue)) { + setInputTouched(true); + inputRef.current?.focus(); + } else if (passValue !== confirmPassValue) { + setConfirmInputTouched(true); + confirmInputRef.current?.focus(); + } else { + setChangePasswordLoading(true); + + // Change setTimeout to api call + setTimeout(() => { + setChangePassAlertOpen(true); + // setLoginStatus('success'); + // setLoginStatus('failed'); + // setLoginFailedMessage('رمز عبور اشتباه میباشد'); + onPasswordChanged(); + setChangePasswordLoading(false); + }, 1000); + } + }; + + const isValidPassword = (value: string) => { + return ( + containsNumber(value) && + containsSymbol(value) && + least8Chars(value) && + hasUpperAndLowerLetter(value) + ); + }; + + return ( + + setChangePassAlertOpen(false)} + color={changePasswordStatus === 'failed' ? 'error' : 'success'} + > + {changePasswordStatus === 'failed' + ? changePassFailedMessage + : t('forgetPassword.passwordChangedSuccessfully')} + + + + + {t('forgetPassword.changePassword')} + + + + + + + {t('forgetPassword.createANewPassword')} + + + setPassValue(e.target.value)} + onBlur={handleBlur} + error={inputTouched && !isValidPassword(passValue)} + autoFocus + slotProps={{ + htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } }, + input: { + startAdornment: confirmPassValue && + isValidPassword(passValue) && + passValue === confirmPassValue && ( + + ), + endAdornment: passValue ? ( + setShowPassword(!showPassword)} + > + {showPassword ? : } + + ) : ( + '' + ), + }, + }} + sx={{ mt: 4 }} + /> + + {!isValidPassword(passValue) && ( + + {passwordValidationRules.map((rule) => ( + + + + {rule.title} + + ))} + + )} + + setConfirmPassValue(e.target.value)} + onBlur={handleConfirmPassBlur} + error={confirmInputTouched && confirmPassValue !== passValue} + slotProps={{ + htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } }, + input: { + startAdornment: confirmPassValue && + isValidPassword(passValue) && + passValue === confirmPassValue && ( + + ), + endAdornment: confirmPassValue ? ( + setShowConfirmPassword(!showConfirmPassword)} + > + {showPassword ? : } + + ) : ( + '' + ), + }, + }} + sx={{ my: 4 }} + /> + + + + + + ); +}; diff --git a/src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx new file mode 100644 index 0000000..d95d294 --- /dev/null +++ b/src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import type { AuthType } from '../../types/auth-types'; +import { ForgettedPasswordInfo } from './ForgettedPasswordInfo'; +import { ForgetPasswordOtp } from './ForgetPasswordOtp'; +import { ChangePassword } from './ChangePassword'; + +export const ForgetPasswordContainer = () => { + const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState< + 'enterInfo' | 'verifyOtp' | 'setPassword' + >('enterInfo'); + const [forgettedPasswordInfo, setForgettedPasswordInfo] = + useState(''); + const [infoType, setInfoType] = useState('email'); + + const handleSendForgetPassOtp = (value: string) => { + console.log(value); + setForgetPassCurrentStep('verifyOtp'); + }; + + const handleEditInfo = () => { + setForgetPassCurrentStep('enterInfo'); + }; + + const handleOtpVerified = () => { + setForgetPassCurrentStep('setPassword'); + }; + + const handlePasswordChanged = () => { + console.log('changingPasswordTo'); + }; + + return ( + <> + {forgetPassCurrentStep === 'enterInfo' && ( + + )} + + {forgetPassCurrentStep === 'verifyOtp' && ( + + )} + + {forgetPassCurrentStep === 'setPassword' && ( + + )} + + ); +}; diff --git a/src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx new file mode 100644 index 0000000..bb67a5f --- /dev/null +++ b/src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx @@ -0,0 +1,168 @@ +import { useTranslation } from 'react-i18next'; +import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; +import { Edit2 } from 'iconsax-reactjs'; +import DigitInput from '@/components/components/DigitsInput'; +import type { AuthMode, AuthType } from '../../types/auth-types'; +import { useEffect, useState } from 'react'; +import { Toast } from '@/components/Toast'; +import { AuthenticationCard } from '../AuthenticationCard'; + +interface ForgetPasswordOtpProps { + forgettedPasswordInfo: string; + infoType: AuthType; + onEditInfo: () => void; + onOTPVerified: (otpCode: string) => void; +} + +export function ForgetPasswordOtp({ + forgettedPasswordInfo, + infoType, + onEditInfo, + onOTPVerified, +}: ForgetPasswordOtpProps) { + const [otpCode, setOtpCode] = useState(''); + const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); + const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>(); + const [verifyStatusLoading, setVerifyStatusLoading] = + useState(false); + const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); + const { t } = useTranslation('authentication'); + const [resendTimer, setResendTimer] = useState(120); + const [canResend, setCanResend] = useState(false); + const [resendLoading, setResendLoading] = useState(false); + + useEffect(() => { + let interval: NodeJS.Timeout; + if (resendTimer > 0) { + interval = setInterval(() => { + setResendTimer((prev) => prev - 1); + }, 1000); + } else { + setCanResend(true); + } + + return () => clearInterval(interval); + }, [resendTimer]); + + const handleResendOTPCode = () => { + setResendLoading(true); + + // TODO: Call API here instead of settimeout + + setTimeout(() => { + console.log('resended'); + + setResendTimer(120); + setCanResend(false); + setResendLoading(false); + }, 1000); + }; + + const formatTime = (seconds: number) => { + const min = Math.floor(seconds / 60); + const sec = seconds % 60; + return `${min}:${sec.toString().padStart(2, '0')}`; + }; + + const handleDigitInputChange = (value: string[]) => { + const formattedValue = value.filter((char) => char !== '').join(''); + + setOtpCode(formattedValue); + }; + + const handleVerifyOTP = () => { + if (!otpCode || otpCode.length < 4) { + setOtpDigitInvalid(true); + } else { + setOtpDigitInvalid(false); + setVerifyStatusLoading(true); + + // Change setTimeout to api call + setTimeout(() => { + setVerifyAlertOpen(false); + onOTPVerified(otpCode); + setVerifyStatusLoading(false); + }, 1000); + } + }; + + return ( + + + setVerifyAlertOpen(false)} + color={'error'} + > + {t('verify.theVerificationCodeIsIncorrect')} + + + + + {t('forgetPassword.forgetPassword')} + + + + + + + {infoType === 'email' + ? t( + 'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress', + ) + : t( + 'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber', + )} + + + handleDigitInputChange(value as string[])} + /> + + + + + + {t('verify.resendCodeIn')} + + + + + ); +} diff --git a/src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx b/src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx new file mode 100644 index 0000000..64f05e5 --- /dev/null +++ b/src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx @@ -0,0 +1,151 @@ +import { + Box, + Button, + Paper, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useRef, useState, type Dispatch } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Google } from 'iconsax-reactjs'; +import { isNumeric } from '@/utils/regexes/isNumeric'; +import type { AuthMode, AuthType } from '../../types/auth-types'; +import { isEmail } from '@/utils/regexes/isEmail'; +import parsePhoneNumberFromString from 'libphonenumber-js'; +import { AuthenticationCard } from '../AuthenticationCard'; +import { CountryCodeSelector } from '../CountryCodeSelector'; + +export interface ForgettedPasswordInfoProps { + forgettedPasswordInfo: string; + setForgettedPasswordInfo: Dispatch; + infoType: AuthType; + setInfoType: Dispatch; + onSendOtp: (value: string) => void; +} + +export function ForgettedPasswordInfo({ + forgettedPasswordInfo, + setForgettedPasswordInfo, + infoType, + setInfoType, + onSendOtp, +}: ForgettedPasswordInfoProps) { + const { t, i18n } = useTranslation('authentication'); + const [countryCode, setCountryCode] = useState('+98'); + const textFieldRef = useRef(null); + const inputRef = useRef(null); + const dir = i18n.dir(); + const [error, setError] = useState(); + const [touched, setTouched] = useState(false); + const inputError: boolean = touched && !!error; + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setForgettedPasswordInfo(newValue); + + // If the new value contains only digits (or is empty), it's a phone number + if (isNumeric(newValue)) { + setInfoType('phone'); + } else { + setInfoType('email'); + } + }; + + const handleBlur = () => { + setTouched(true); + validateInput(forgettedPasswordInfo, infoType); + }; + + const validateInput = (value: string, authType: AuthType) => { + if (!value) { + setError(t('loginForm.thisFieldIsRequired')); + } else if (authType === 'email' && !isEmail(value)) { + setError(t('loginForm.emailIsInvalid')); + } else if (authType === 'phone' && !isPhoneValid(countryCode, value)) { + setError(t('loginForm.phoneNumberIsInvalid')); + } else { + setError(undefined); + } + }; + + const isPhoneValid = (code: string, phone: string) => { + const phoneNumber = parsePhoneNumberFromString(code + phone); + + return phoneNumber && phoneNumber.isValid(); + }; + + const isInputValid = (value: string, authType: AuthType): boolean => { + if (!value) { + return false; + } + + if (authType === 'email' && !isEmail(value)) { + return false; + } + + if (authType === 'phone' && !isPhoneValid(countryCode, value)) { + return false; + } + + return true; + }; + + const handleSubmit = () => { + if (isInputValid(forgettedPasswordInfo, infoType)) { + onSendOtp(forgettedPasswordInfo); + } else { + inputRef.current?.focus(); + validateInput(forgettedPasswordInfo, infoType); + } + }; + + const showAdornment = + infoType === 'phone' && forgettedPasswordInfo.length > 0; + + return ( + + + + {t('forgetPassword.forgetPassword')} + + + {t( + 'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword', + )} + + + + + ), + }, + }} + sx={{ my: 4, mb: 8 }} + /> + + + + + + ); +} diff --git a/src/features/authentication/routes/AuthenticationPage.tsx b/src/features/authentication/routes/AuthenticationPage.tsx index 7cad998..871d641 100644 --- a/src/features/authentication/routes/AuthenticationPage.tsx +++ b/src/features/authentication/routes/AuthenticationPage.tsx @@ -3,6 +3,7 @@ import Logo from '@/components/Logo'; import { Paper } from '@mui/material'; import { useState } from 'react'; import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps'; +import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer'; export function AuthenticationPage() { return ( @@ -16,7 +17,7 @@ export function AuthenticationPage() { }} > - + ); } diff --git a/src/utils/regexes/containsNumber.tsx b/src/utils/regexes/containsNumber.tsx new file mode 100644 index 0000000..5ccefc8 --- /dev/null +++ b/src/utils/regexes/containsNumber.tsx @@ -0,0 +1 @@ +export const containsNumber = (value: string) => /\d/.test(value); diff --git a/src/utils/regexes/containsSymbol.tsx b/src/utils/regexes/containsSymbol.tsx new file mode 100644 index 0000000..c3ac510 --- /dev/null +++ b/src/utils/regexes/containsSymbol.tsx @@ -0,0 +1 @@ +export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value); diff --git a/src/utils/regexes/hasUpperAndLowerLetter.tsx b/src/utils/regexes/hasUpperAndLowerLetter.tsx new file mode 100644 index 0000000..a29c262 --- /dev/null +++ b/src/utils/regexes/hasUpperAndLowerLetter.tsx @@ -0,0 +1,5 @@ +export const hasUpperAndLowerLetter = (value: string) => { + const hasUpper = /[A-Z]/.test(value); + const hasLower = /[a-z]/.test(value); + return hasUpper && hasLower; +}; diff --git a/src/utils/regexes/least8Chars.tsx b/src/utils/regexes/least8Chars.tsx new file mode 100644 index 0000000..c4c4c8a --- /dev/null +++ b/src/utils/regexes/least8Chars.tsx @@ -0,0 +1 @@ +export const least8Chars = (value: string) => value.length >= 8; From 284e60fab336fe4b3653ec5654d3c3e905978275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 9 Aug 2025 12:58:28 +0330 Subject: [PATCH 18/32] chore: authorization module name changed and backend type and request functions added --- src/App.tsx | 2 +- .../authorization/api/authorizationAPI.ts | 96 ++++++++++++++++ .../components/AuthenticationCard.tsx | 0 .../AuthenticationSteps.tsx | 2 +- .../AuthenticationSteps/CompleteSignUp.tsx | 0 .../AuthenticationSteps/EnterPasswordForm.tsx | 0 .../AuthenticationSteps/LoginRegiserForm.tsx | 2 +- .../AuthenticationSteps/OtpVerifyForm.tsx | 2 +- .../components/CountryCodeSelector.tsx | 0 .../ForgetPassword/ChangePassword.tsx | 0 .../ForgetPasswordContainer.tsx | 2 +- .../ForgetPassword/ForgetPasswordOtp.tsx | 2 +- .../ForgetPassword/ForgettedPasswordInfo.tsx | 2 +- .../data/countries.ts | 0 .../index.ts | 0 .../routes/AuthenticationPage.tsx | 2 +- .../types/authTypes.ts} | 0 src/features/authorization/types/userTypes.ts | 106 ++++++++++++++++++ src/types/apiResponse.ts | 13 +++ src/types/commonTypes.ts | 1 + src/types/fetchPromise.ts | 5 + 21 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 src/features/authorization/api/authorizationAPI.ts rename src/features/{authentication => authorization}/components/AuthenticationCard.tsx (100%) rename src/features/{authentication => authorization}/components/AuthenticationSteps/AuthenticationSteps.tsx (98%) rename src/features/{authentication => authorization}/components/AuthenticationSteps/CompleteSignUp.tsx (100%) rename src/features/{authentication => authorization}/components/AuthenticationSteps/EnterPasswordForm.tsx (100%) rename src/features/{authentication => authorization}/components/AuthenticationSteps/LoginRegiserForm.tsx (98%) rename src/features/{authentication => authorization}/components/AuthenticationSteps/OtpVerifyForm.tsx (98%) rename src/features/{authentication => authorization}/components/CountryCodeSelector.tsx (100%) rename src/features/{authentication => authorization}/components/ForgetPassword/ChangePassword.tsx (100%) rename src/features/{authentication => authorization}/components/ForgetPassword/ForgetPasswordContainer.tsx (96%) rename src/features/{authentication => authorization}/components/ForgetPassword/ForgetPasswordOtp.tsx (98%) rename src/features/{authentication => authorization}/components/ForgetPassword/ForgettedPasswordInfo.tsx (98%) rename src/features/{authentication => authorization}/data/countries.ts (100%) rename src/features/{authentication => authorization}/index.ts (100%) rename src/features/{authentication => authorization}/routes/AuthenticationPage.tsx (94%) rename src/features/{authentication/types/auth-types.ts => authorization/types/authTypes.ts} (100%) create mode 100644 src/features/authorization/types/userTypes.ts create mode 100644 src/types/apiResponse.ts create mode 100644 src/types/commonTypes.ts create mode 100644 src/types/fetchPromise.ts diff --git a/src/App.tsx b/src/App.tsx index 0d50227..700db76 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { CssBaseline } from '@mui/material'; import './App.css'; import { LanguageManager } from './components/LanguageManager'; -import { AuthenticationPage } from './features/authentication/routes/AuthenticationPage'; +import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage'; function App() { return ( diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts new file mode 100644 index 0000000..990924b --- /dev/null +++ b/src/features/authorization/api/authorizationAPI.ts @@ -0,0 +1,96 @@ +import type { ApiResponse } from '@/types/apiResponse'; +import type { FetchPromise } from '@/types/fetchPromise'; +import type { + ConfirmEmailOtpRequest, + ConfirmForgetPassCodeRequest, + ConfirmOtpResponse, + ConfirmSmsOtpRequest, + GetUserStatusByPhoneNumberOrEmailRequest, + GetUserStatusByPhoneNumberOrEmailResponse, + LoginOrSignUpWithGoogleRequest, + LoginOrSignUpWithGoogleResponse, + LoginRequest, + LoginResponse, + ResetPasswordRequest, + ResetPasswordResponse, + SendEmailOtpRequest, + SendForgetPassCodeRequest, + SendSmsOtpRequest, +} from '../types/userTypes'; + +const API_URL = 'https://account.business-harmony.com/api/'; + +export const fetchRequest = ( + url: string, + body: Object | null, +): FetchPromise => { + return fetch(`${API_URL}/${url}`, { + body: JSON.stringify(body), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +// GetUserStatusByPhoneNumberOrEmail + +export const getUserStatusByPhoneNumberOrEmail = async ( + body: GetUserStatusByPhoneNumberOrEmailRequest, +) => { + return fetchRequest( + 'User/GetUserStatusByPhoneNumberOrEmail', + body, + ); +}; + +export const loginOrSignUpWithOtp = async (body: LoginRequest) => { + return fetchRequest('User/LoginOrSignUpWithOtp', body); +}; + +export const loginWithPassword = async (body: LoginRequest) => { + return fetchRequest('User/LoginWithPassword', body); +}; + +export const sendSmsOtp = async (body: SendSmsOtpRequest) => { + return fetchRequest('User/SendSmsOtp', body); +}; + +export const sendEmailOtp = async (body: SendEmailOtpRequest) => { + return fetchRequest('User/SendEmailOtp', body); +}; + +export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => { + return fetchRequest('User/ConfirmSmsOtp', body); +}; + +export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => { + return fetchRequest('User/ConfirmEmailOtp', body); +}; + +export const resetPassword = async (body: ResetPasswordRequest) => { + return fetchRequest('User/ResetPassword', body); +}; + +export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => { + return fetchRequest('User/SendForgetPassCode', body); +}; + +export const ConfirmForgetPassCode = async ( + body: ConfirmForgetPassCodeRequest, +) => { + return fetchRequest('User/ConfirmForgetPassCode', body); +}; + +export const loginOrSignUpWithGoogle = async ( + body: LoginOrSignUpWithGoogleRequest, +) => { + return fetchRequest( + 'User/LoginOrSignUpWithGoogle', + body, + ); +}; + +export const logOut = async () => { + return fetchRequest('User/LogOut', {}); +}; diff --git a/src/features/authentication/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx similarity index 100% rename from src/features/authentication/components/AuthenticationCard.tsx rename to src/features/authorization/components/AuthenticationCard.tsx diff --git a/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx similarity index 98% rename from src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx rename to src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index b88a506..c9c951a 100644 --- a/src/features/authentication/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -1,6 +1,6 @@ import React, { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; -import type { AuthMode, AuthType } from '../../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/authTypes'; import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; diff --git a/src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx similarity index 100% rename from src/features/authentication/components/AuthenticationSteps/CompleteSignUp.tsx rename to src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx diff --git a/src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx similarity index 100% rename from src/features/authentication/components/AuthenticationSteps/EnterPasswordForm.tsx rename to src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx diff --git a/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx similarity index 98% rename from src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx rename to src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx index df79514..66e7cbd 100644 --- a/src/features/authentication/components/AuthenticationSteps/LoginRegiserForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -10,7 +10,7 @@ import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; -import type { AuthMode, AuthType } from '../../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/authTypes'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; diff --git a/src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx similarity index 98% rename from src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx rename to src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index 1997972..54de8ba 100644 --- a/src/features/authentication/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; -import type { AuthMode, AuthType } from '../../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authorization/components/CountryCodeSelector.tsx similarity index 100% rename from src/features/authentication/components/CountryCodeSelector.tsx rename to src/features/authorization/components/CountryCodeSelector.tsx diff --git a/src/features/authentication/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx similarity index 100% rename from src/features/authentication/components/ForgetPassword/ChangePassword.tsx rename to src/features/authorization/components/ForgetPassword/ChangePassword.tsx diff --git a/src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx similarity index 96% rename from src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx rename to src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx index d95d294..a8112fb 100644 --- a/src/features/authentication/components/ForgetPassword/ForgetPasswordContainer.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import type { AuthType } from '../../types/auth-types'; +import type { AuthType } from '../../types/authTypes'; import { ForgettedPasswordInfo } from './ForgettedPasswordInfo'; import { ForgetPasswordOtp } from './ForgetPasswordOtp'; import { ChangePassword } from './ChangePassword'; diff --git a/src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx similarity index 98% rename from src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx rename to src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx index bb67a5f..62c81cb 100644 --- a/src/features/authentication/components/ForgetPassword/ForgetPasswordOtp.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; -import type { AuthMode, AuthType } from '../../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; diff --git a/src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx similarity index 98% rename from src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx rename to src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx index 64f05e5..dc43ebf 100644 --- a/src/features/authentication/components/ForgetPassword/ForgettedPasswordInfo.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx @@ -10,7 +10,7 @@ import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; -import type { AuthMode, AuthType } from '../../types/auth-types'; +import type { AuthMode, AuthType } from '../../types/authTypes'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; diff --git a/src/features/authentication/data/countries.ts b/src/features/authorization/data/countries.ts similarity index 100% rename from src/features/authentication/data/countries.ts rename to src/features/authorization/data/countries.ts diff --git a/src/features/authentication/index.ts b/src/features/authorization/index.ts similarity index 100% rename from src/features/authentication/index.ts rename to src/features/authorization/index.ts diff --git a/src/features/authentication/routes/AuthenticationPage.tsx b/src/features/authorization/routes/AuthenticationPage.tsx similarity index 94% rename from src/features/authentication/routes/AuthenticationPage.tsx rename to src/features/authorization/routes/AuthenticationPage.tsx index 871d641..d34cc01 100644 --- a/src/features/authentication/routes/AuthenticationPage.tsx +++ b/src/features/authorization/routes/AuthenticationPage.tsx @@ -17,7 +17,7 @@ export function AuthenticationPage() { }} > - + ); } diff --git a/src/features/authentication/types/auth-types.ts b/src/features/authorization/types/authTypes.ts similarity index 100% rename from src/features/authentication/types/auth-types.ts rename to src/features/authorization/types/authTypes.ts diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts new file mode 100644 index 0000000..54070a0 --- /dev/null +++ b/src/features/authorization/types/userTypes.ts @@ -0,0 +1,106 @@ +// GetUserStatusByPhoneNumberOrEmail + +import type { ApiResponse } from '@/types/apiResponse'; +import type { GUID } from '@/types/commonTypes'; + +export interface GetUserStatusByPhoneNumberOrEmailRequest { + phoneNumber?: string; + email?: string; +} + +export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse { + userStatus: UserStatus; +} + +export enum UserStatus { + None = 0, + Value1 = 1, + Value2 = 2, + Value3 = 3, +} + +// LoginOrSignUpWithOtp + +export interface LoginRequest { + otpCode: string; + phoneNumber?: string; + email?: string; + returnUrl: string; +} + +export interface LoginResponse extends ApiResponse { + returnUrl: string; + userId: GUID; + registeredWithOutPhoneNumber: boolean; + completedUserInformation: boolean; +} + +// SendSmsOtp + +export interface SendSmsOtpRequest { + phoneNumber: string; +} + +// SendEmailOtp + +export interface SendEmailOtpRequest { + email: string; +} + +// ConfirmOtp + +export interface ConfirmEmailOtpRequest { + email: string; + otpCode: string; +} + +export interface ConfirmSmsOtpRequest { + phoneNumber: string; + otpCode: string; +} + +export interface ConfirmOtpResponse extends ApiResponse { + confirm: boolean; +} + +// ResetPassword + +export interface ResetPasswordRequest { + email?: string; + phoneNumber?: string; + newPassword: string; + confirmNewPassword: string; +} + +export interface ResetPasswordResponse extends ApiResponse { + passwordChanged: boolean; +} + +// SendForgetPassCode + +export interface SendForgetPassCodeRequest { + email?: string; + phoneNumber?: string; +} + +// ConfirmForgetPassCode + +export interface ConfirmForgetPassCodeRequest { + email: string; + phoneNumber: string; + code: string; +} + +// LoginOrSignUpWithGoogle + +export interface LoginOrSignUpWithGoogleRequest { + idToken: string; + returnUrl: string; +} + +export interface LoginOrSignUpWithGoogleResponse extends ApiResponse { + userId: GUID; + registeredWithOutPhoneNumber: boolean; + completedUserInformation: boolean; + returnUrl: string; +} diff --git a/src/types/apiResponse.ts b/src/types/apiResponse.ts new file mode 100644 index 0000000..abc0599 --- /dev/null +++ b/src/types/apiResponse.ts @@ -0,0 +1,13 @@ +export interface ApiResponse { + success: boolean; + errorCode: number; + message: string; + validations: ApiResponseValidation[]; +} + +export interface ApiResponseValidation { + message: string; + code: number; + property: string; + severity: number; +} diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts new file mode 100644 index 0000000..a380ad0 --- /dev/null +++ b/src/types/commonTypes.ts @@ -0,0 +1 @@ +export type GUID = `${string}-${string}-${string}-${string}-${string}`; diff --git a/src/types/fetchPromise.ts b/src/types/fetchPromise.ts new file mode 100644 index 0000000..84483d5 --- /dev/null +++ b/src/types/fetchPromise.ts @@ -0,0 +1,5 @@ +export type FetchPromise = Promise>; + +export interface FetchResponse extends Response { + json(): Promise; +} From cd86254ce148de241f97fe2f50d137d750ee872a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 9 Aug 2025 15:46:51 +0330 Subject: [PATCH 19/32] feat: login and register, otp verify api calls added --- package-lock.json | 38 ++++ package.json | 1 + src/App.tsx | 9 +- .../authorization/api/authorizationAPI.ts | 9 +- .../AuthenticationSteps.tsx | 62 ++++-- .../AuthenticationSteps/LoginRegiserForm.tsx | 48 ++++- .../AuthenticationSteps/OtpVerifyForm.tsx | 62 ++++-- .../AuthenticationSteps/VerifyPhoneNumber.tsx | 191 ++++++++++++++++++ .../components/CountryCodeSelector.tsx | 5 +- src/features/authorization/data/countries.ts | 8 +- src/features/authorization/types/userTypes.ts | 27 ++- src/types/commonTypes.ts | 2 + 12 files changed, 403 insertions(+), 59 deletions(-) create mode 100644 src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx diff --git a/package-lock.json b/package-lock.json index 9e9436f..d528328 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-router": "^7.8.0", "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" @@ -2404,6 +2405,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3794,6 +3804,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", + "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3940,6 +3972,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 91e1810..a876532 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-router": "^7.8.0", "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" diff --git a/src/App.tsx b/src/App.tsx index 700db76..f1f5aa2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,13 +2,20 @@ import { CssBaseline } from '@mui/material'; import './App.css'; import { LanguageManager } from './components/LanguageManager'; import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router'; function App() { return ( <> - + + + } /> + } /> + + + , ); } diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts index 990924b..290a736 100644 --- a/src/features/authorization/api/authorizationAPI.ts +++ b/src/features/authorization/api/authorizationAPI.ts @@ -1,6 +1,7 @@ import type { ApiResponse } from '@/types/apiResponse'; import type { FetchPromise } from '@/types/fetchPromise'; import type { + CompleteUserInformationRequest, ConfirmEmailOtpRequest, ConfirmForgetPassCodeRequest, ConfirmOtpResponse, @@ -18,7 +19,7 @@ import type { SendSmsOtpRequest, } from '../types/userTypes'; -const API_URL = 'https://account.business-harmony.com/api/'; +const API_URL = 'https://account.business-harmony.com/api'; export const fetchRequest = ( url: string, @@ -91,6 +92,12 @@ export const loginOrSignUpWithGoogle = async ( ); }; +export const completeUserInformation = async ( + body: CompleteUserInformationRequest, +) => { + return fetchRequest('User/CompleteUserInformation', body); +}; + export const logOut = async () => { return fetchRequest('User/LogOut', {}); }; diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index c9c951a..ccfc6c1 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -5,61 +5,79 @@ import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; +import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; +import { UserStatus } from '../../types/userTypes'; +import type { CountryCode } from '@/types/commonTypes'; +import { VerifyPhoneNumber } from './VerifyPhoneNumber'; export const AuthenticationSteps = (): JSX.Element => { const [authMode, setAuthMode] = useState('register'); const [authType, setAuthType] = useState('phone'); const [currentStep, setCurrentStep] = useState< - | 'emailOrPassword' + | 'emailOrPhone' | 'verify' | 'enterPassword' | 'addPhoneNumber' | 'addedPhoneNumberVerify' - >('emailOrPassword'); + >('emailOrPhone'); const [loginRegisterValue, setLoginRegisterValue] = useState(''); + const [countryCode, setCountryCode] = useState('+98'); const [addedPhoneNumberValue, setAddedPhoneNumberValue] = useState(''); - const handleLoginRegister = (value: string) => { - setLoginRegisterValue(value); + const handleLoginRegister = (value: string, userStatus: UserStatus) => { setAuthType(isNumeric(value) ? 'phone' : 'email'); - // TODO: after api: send to password if it has account and has password - if (true) { - setCurrentStep('enterPassword'); - } else { - setCurrentStep('verify'); + switch (userStatus) { + case UserStatus.NotRegistered: + setAuthMode('register'); + setCurrentStep('verify'); + break; + + case UserStatus.RegisteredWithoutPassword: + setAuthMode('login'); + setCurrentStep('verify'); + + break; + + case UserStatus.RegisteredWithPassword: + setAuthMode('login'); + setCurrentStep('enterPassword'); + + break; } }; - const handleOTPVerfied = (otpCode: string) => { - if (authMode === 'register' && authType === 'email') { - setCurrentStep('addPhoneNumber'); - } + const handleOTPVerfied = (registeredWithoutPhoneNumber: boolean = false) => { + // if (registeredWithoutPhoneNumber) { + // setCurrentStep('addPhoneNumber'); + // } }; const handleEditValue = () => { - setCurrentStep('emailOrPassword'); + setCurrentStep('emailOrPhone'); }; const handleCompleteSignUp = (countryCode: string, value: string) => { setCurrentStep('addedPhoneNumberVerify'); }; - const handleCompleteSignUpOTPVerified = (otpCode: string) => { - console.log(otpCode); + const handleCompleteSignUpOTPVerified = () => { + console.log('phoneNumberVerified'); }; const handleCompleteSignUpEditValue = () => { - setCurrentStep('emailOrPassword'); + setCurrentStep('emailOrPhone'); }; const handleLoggedInWithPassowrd = () => {}; return ( <> - {currentStep === 'emailOrPassword' && ( + {currentStep === 'emailOrPhone' && ( { {currentStep === 'verify' && ( { )} {currentStep === 'addedPhoneNumberVerify' && ( - )} diff --git a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx index 66e7cbd..3d64449 100644 --- a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -15,29 +15,38 @@ import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; import { CountryCodeSelector } from '../CountryCodeSelector'; +import type { UserStatus } from '../../types/userTypes'; +import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; +import { Toast } from '@/components/Toast'; +import type { CountryCode } from '@/types/commonTypes'; export interface LoginRegisterFormProps { loginRegisterValue: string; setLoginRegisterValue: Dispatch; + countryCode: CountryCode; + setCountryCode: Dispatch; authType: AuthType; setAuthType: Dispatch; - onLoginRegisterSubmit: (value: string) => void; + onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void; } export function LoginRegisterForm({ loginRegisterValue, setLoginRegisterValue, + countryCode, + setCountryCode, authType, setAuthType, onLoginRegisterSubmit, }: LoginRegisterFormProps) { + const [checkStatusLoading, setCheckStatusLoading] = useState(false); const { t, i18n } = useTranslation('authentication'); - const [countryCode, setCountryCode] = useState('+98'); const textFieldRef = useRef(null); const inputRef = useRef(null); const dir = i18n.dir(); const [error, setError] = useState(); const [touched, setTouched] = useState(false); + const [errorMessage, setErrorMessage] = useState(); const inputError: boolean = touched && !!error; const handleInputChange = (event: React.ChangeEvent) => { @@ -91,9 +100,22 @@ export function LoginRegisterForm({ return true; }; - const handleSubmit = () => { + const handleSubmit = async () => { if (isInputValid(loginRegisterValue, authType)) { - onLoginRegisterSubmit(loginRegisterValue); + setCheckStatusLoading(true); + const result = await getUserStatusByPhoneNumberOrEmail({ + phoneNumber: + authType === 'phone' ? countryCode + loginRegisterValue : undefined, + email: authType === 'email' ? loginRegisterValue : undefined, + }); + const jsonResult = await result.json(); + + if (jsonResult.success) { + onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus); + } else { + setErrorMessage(jsonResult.message); + } + setCheckStatusLoading(false); } else { inputRef.current?.focus(); validateInput(loginRegisterValue, authType); @@ -104,6 +126,14 @@ export function LoginRegisterForm({ return ( + setErrorMessage(undefined)} + open={!!errorMessage} + > + {errorMessage} + + {t('loginForm.title')} @@ -139,8 +169,14 @@ export function LoginRegisterForm({ /> - - + diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index 54de8ba..748f7e9 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -6,25 +6,37 @@ import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; +import type { LoginRequest } from '../../types/userTypes'; +import { useSearchParams } from 'react-router'; +import { + loginOrSignUpWithOtp, + sendEmailOtp, + sendSmsOtp, +} from '../../api/authorizationAPI'; +import type { CountryCode } from '@/types/commonTypes'; interface OtpVerifyFormProps { value: string; + countryCode: CountryCode; authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: (otpCode: string) => void; + onOTPVerified: (registeredWithoutPhoneNumber: boolean) => void; } export function OtpVerifyForm({ value, + countryCode, authType, authMode, onEditValue, onOTPVerified, }: OtpVerifyFormProps) { + const [searchParams] = useSearchParams(); const [otpCode, setOtpCode] = useState(''); const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>(); + const [errorMessage, setErrorMessage] = useState(); const [verifyStatusLoading, setVerifyStatusLoading] = useState(false); const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); @@ -46,18 +58,18 @@ export function OtpVerifyForm({ return () => clearInterval(interval); }, [resendTimer]); - const handleResendOTPCode = () => { + const handleResendOTPCode = async () => { setResendLoading(true); - // TODO: Call API here instead of settimeout + if (authType === 'phone') { + await sendSmsOtp({ phoneNumber: countryCode + value }); + } else { + await sendEmailOtp({ email: value }); + } - setTimeout(() => { - console.log('resended'); - - setResendTimer(120); - setCanResend(false); - setResendLoading(false); - }, 1000); + setResendTimer(120); + setCanResend(false); + setResendLoading(false); }; const formatTime = (seconds: number) => { @@ -72,7 +84,7 @@ export function OtpVerifyForm({ setOtpCode(formattedValue); }; - const handleVerifyOTP = () => { + const handleVerifyOTP = async () => { if (!otpCode || otpCode.length < 4) { setOtpDigitInvalid(true); } else { @@ -80,12 +92,26 @@ export function OtpVerifyForm({ setVerifyStatusLoading(true); // Change setTimeout to api call - setTimeout(() => { - setVerifyAlertOpen(true); + + const loginRequest: LoginRequest = { + otpCode: otpCode, + phoneNumber: authType === 'phone' ? countryCode + value : undefined, + email: authType === 'email' ? value : undefined, + returnUrl: searchParams.get('returnUrl') ?? '/', + }; + const result = await loginOrSignUpWithOtp(loginRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { setVerifyStatus('success'); - onOTPVerified(otpCode); - setVerifyStatusLoading(false); - }, 1000); + onOTPVerified(jsonRes.registeredWithOutPhoneNumber); + } else { + setVerifyStatus('failed'); + setErrorMessage(jsonRes.message); + } + + setVerifyAlertOpen(true); + setVerifyStatusLoading(false); } }; @@ -113,7 +139,7 @@ export function OtpVerifyForm({ const verifyAlertMessage = (): string => { if (verifyStatus === 'failed') { - return t('verify.theVerificationCodeIsIncorrect'); + return errorMessage ?? t('verify.theVerificationCodeIsIncorrect'); } else if (verifyStatus === 'success' && authMode === 'register') { return t('verify.youHaveSuccessfullySignedIn'); } else if (verifyStatus === 'success' && authMode === 'login') { @@ -153,7 +179,7 @@ export function OtpVerifyForm({ endIcon={} onClick={onEditValue} > - {value} + {authType === 'phone' ? countryCode + value : value} diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx new file mode 100644 index 0000000..b047bdc --- /dev/null +++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx @@ -0,0 +1,191 @@ +import { useTranslation } from 'react-i18next'; +import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; +import { Edit2 } from 'iconsax-reactjs'; +import DigitInput from '@/components/components/DigitsInput'; +import type { AuthMode, AuthType } from '../../types/authTypes'; +import { useEffect, useState } from 'react'; +import { Toast } from '@/components/Toast'; +import { AuthenticationCard } from '../AuthenticationCard'; +import type { LoginRequest } from '../../types/userTypes'; +import { useSearchParams } from 'react-router'; +import { + loginOrSignUpWithOtp, + sendEmailOtp, + sendSmsOtp, +} from '../../api/authorizationAPI'; +import type { CountryCode } from '@/types/commonTypes'; + +interface VerifyPhoneNumberProps { + value: string; + countryCode: CountryCode; + onEditValue: () => void; + onPhoneNumberVerified: () => void; +} + +export function VerifyPhoneNumber({ + value, + countryCode, + onEditValue, + onPhoneNumberVerified, +}: VerifyPhoneNumberProps) { + const [searchParams] = useSearchParams(); + const [otpCode, setOtpCode] = useState(''); + const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); + const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>(); + const [errorMessage, setErrorMessage] = useState(); + const [verifyStatusLoading, setVerifyStatusLoading] = + useState(false); + const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); + const { t } = useTranslation('authentication'); + const [resendTimer, setResendTimer] = useState(120); + const [canResend, setCanResend] = useState(false); + const [resendLoading, setResendLoading] = useState(false); + + useEffect(() => { + let interval: NodeJS.Timeout; + if (resendTimer > 0) { + interval = setInterval(() => { + setResendTimer((prev) => prev - 1); + }, 1000); + } else { + setCanResend(true); + } + + return () => clearInterval(interval); + }, [resendTimer]); + + const handleResendOTPCode = async () => { + setResendLoading(true); + + await sendSmsOtp({ phoneNumber: countryCode + value }); + + setResendTimer(120); + setCanResend(false); + setResendLoading(false); + }; + + const formatTime = (seconds: number) => { + const min = Math.floor(seconds / 60); + const sec = seconds % 60; + return `${min}:${sec.toString().padStart(2, '0')}`; + }; + + const handleDigitInputChange = (value: string[]) => { + const formattedValue = value.filter((char) => char !== '').join(''); + + setOtpCode(formattedValue); + }; + + const handleVerifyOTP = async () => { + if (!otpCode || otpCode.length < 4) { + setOtpDigitInvalid(true); + } else { + setOtpDigitInvalid(false); + setVerifyStatusLoading(true); + + // Change setTimeout to api call + + // const loginRequest: LoginRequest = { + // otpCode: otpCode, + // phoneNumber: authType === 'phone' ? countryCode + value : undefined, + // email: authType === 'email' ? value : undefined, + // returnUrl: searchParams.get('returnUrl') ?? '/', + // }; + // const result = await loginOrSignUpWithOtp(loginRequest); + // const jsonRes = await result.json(); + + // if (jsonRes.success) { + // setVerifyStatus('success'); + // onOTPVerified(jsonRes.registeredWithOutPhoneNumber); + // } else { + // setVerifyStatus('failed'); + // setErrorMessage(jsonRes.message); + // } + + setVerifyAlertOpen(true); + setVerifyStatusLoading(false); + } + }; + + const verifyAlertMessage = (): string => { + if (verifyStatus === 'failed') { + return errorMessage ?? t('verify.theVerificationCodeIsIncorrect'); + } else { + return t('verify.youHaveSuccessfullyLoggedIn'); + } + }; + + return ( + + + setVerifyAlertOpen(false)} + color={verifyStatus === 'failed' ? 'error' : 'success'} + > + {verifyAlertMessage()} + + + + {t('verify.verify')} + + + + + + {t( + 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt', + )} + + + handleDigitInputChange(value as string[])} + /> + + + + + + {t('verify.resendCodeIn')} + + + + + ); +} diff --git a/src/features/authorization/components/CountryCodeSelector.tsx b/src/features/authorization/components/CountryCodeSelector.tsx index 427bc8a..04ad265 100644 --- a/src/features/authorization/components/CountryCodeSelector.tsx +++ b/src/features/authorization/components/CountryCodeSelector.tsx @@ -15,10 +15,11 @@ import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; import { countries, type Country } from '../data/countries'; +import type { CountryCode } from '@/types/commonTypes'; interface CountryCodeSelectorProps { show: boolean; - value: string; - onChange: (newValue: string) => void; + value: CountryCode; + onChange: (newValue: CountryCode) => void; menuAnchor: HTMLElement | null; onCloseFocusRef: RefObject; } diff --git a/src/features/authorization/data/countries.ts b/src/features/authorization/data/countries.ts index 2b05f9e..a2a0166 100644 --- a/src/features/authorization/data/countries.ts +++ b/src/features/authorization/data/countries.ts @@ -1,13 +1,9 @@ -export interface Country { - code: string; - label: string; - phone: string; -} +import type { CountryCode } from '@/types/commonTypes'; export interface Country { code: string; label: string; - phone: string; + phone: CountryCode; } export const countries: readonly Country[] = [ diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts index 54070a0..74b5e1d 100644 --- a/src/features/authorization/types/userTypes.ts +++ b/src/features/authorization/types/userTypes.ts @@ -14,9 +14,9 @@ export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse { export enum UserStatus { None = 0, - Value1 = 1, - Value2 = 2, - Value3 = 3, + RegisteredWithPassword = 1, + RegisteredWithoutPassword = 2, + NotRegistered = 3, } // LoginOrSignUpWithOtp @@ -104,3 +104,24 @@ export interface LoginOrSignUpWithGoogleResponse extends ApiResponse { completedUserInformation: boolean; returnUrl: string; } + +// CompleteUserInformation + +export interface CompleteUserInformationRequest { + firstName?: string; + lastName?: string; + gender?: Gender; + nationalCode?: string; + savePassword?: boolean; + password?: string; + saveEmail?: boolean; + email?: string; + birthDate?: string; + countryCode?: string; + userId?: GUID; +} + +export enum Gender { + Male = 1, + Female = 2, +} diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts index a380ad0..b6dee6b 100644 --- a/src/types/commonTypes.ts +++ b/src/types/commonTypes.ts @@ -1 +1,3 @@ export type GUID = `${string}-${string}-${string}-${string}-${string}`; + +export type CountryCode = `+${number}`; From 08bfe5c9798583aa27172c7b39fdd7fd8b7f19c8 Mon Sep 17 00:00:00 2001 From: mehrzadghdev Date: Sun, 10 Aug 2025 00:27:37 +0330 Subject: [PATCH 20/32] forget pass apis added --- src/App.tsx | 2 + .../authorization/api/authorizationAPI.ts | 2 +- .../AuthenticationSteps.tsx | 8 ++- .../AuthenticationSteps/OtpVerifyForm.tsx | 6 +-- .../ForgetPassword/ChangePassword.tsx | 40 ++++++++++---- .../ForgetPasswordContainer.tsx | 12 +++-- .../ForgetPassword/ForgetPasswordOtp.tsx | 37 +++++++++---- .../ForgetPassword/ForgettedPasswordInfo.tsx | 54 +++++++++++++++---- .../routes/ForgetPasswordPage.tsx | 23 ++++++++ src/features/authorization/types/userTypes.ts | 4 +- 10 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 src/features/authorization/routes/ForgetPasswordPage.tsx diff --git a/src/App.tsx b/src/App.tsx index f1f5aa2..585b205 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import './App.css'; import { LanguageManager } from './components/LanguageManager'; import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router'; +import { ForgetPasswordPage } from './features/authorization/routes/ForgetPasswordPage'; function App() { return ( @@ -13,6 +14,7 @@ function App() { } /> } /> + } /> , diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts index 290a736..47a8786 100644 --- a/src/features/authorization/api/authorizationAPI.ts +++ b/src/features/authorization/api/authorizationAPI.ts @@ -77,7 +77,7 @@ export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => { return fetchRequest('User/SendForgetPassCode', body); }; -export const ConfirmForgetPassCode = async ( +export const confirmForgetPassCode = async ( body: ConfirmForgetPassCodeRequest, ) => { return fetchRequest('User/ConfirmForgetPassCode', body); diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index ccfc6c1..8d8d496 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -7,7 +7,7 @@ import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; import { UserStatus } from '../../types/userTypes'; -import type { CountryCode } from '@/types/commonTypes'; +import type { CountryCode, GUID } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; export const AuthenticationSteps = (): JSX.Element => { @@ -48,7 +48,11 @@ export const AuthenticationSteps = (): JSX.Element => { } }; - const handleOTPVerfied = (registeredWithoutPhoneNumber: boolean = false) => { + const handleOTPVerfied = ( + registeredWithoutPhoneNumber: boolean = false, + userId: GUID, + ) => { + localStorage.setItem('userID', userId); // if (registeredWithoutPhoneNumber) { // setCurrentStep('addPhoneNumber'); // } diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index 748f7e9..776f96b 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -13,7 +13,7 @@ import { sendEmailOtp, sendSmsOtp, } from '../../api/authorizationAPI'; -import type { CountryCode } from '@/types/commonTypes'; +import type { CountryCode, GUID } from '@/types/commonTypes'; interface OtpVerifyFormProps { value: string; @@ -21,7 +21,7 @@ interface OtpVerifyFormProps { authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: (registeredWithoutPhoneNumber: boolean) => void; + onOTPVerified: (registeredWithoutPhoneNumber: boolean, userID: GUID) => void; } export function OtpVerifyForm({ @@ -104,7 +104,7 @@ export function OtpVerifyForm({ if (jsonRes.success) { setVerifyStatus('success'); - onOTPVerified(jsonRes.registeredWithOutPhoneNumber); + onOTPVerified(jsonRes.registeredWithOutPhoneNumber, jsonRes.userId); } else { setVerifyStatus('failed'); setErrorMessage(jsonRes.message); diff --git a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx index a83732b..27877a8 100644 --- a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx +++ b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx @@ -23,17 +23,25 @@ import { containsNumber } from '@/utils/regexes/containsNumber'; import { containsSymbol } from '@/utils/regexes/containsSymbol'; import { least8Chars } from '@/utils/regexes/least8Chars'; import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter'; +import type { ResetPasswordRequest } from '../../types/userTypes'; +import type { AuthType } from '../../types/authTypes'; +import type { CountryCode } from '@/types/commonTypes'; +import { resetPassword } from '../../api/authorizationAPI'; export interface ChangePasswordProps { onEditInfo: () => void; onPasswordChanged: () => void; forgettedPasswordInfo: string; + infoType: AuthType; + countryCode: CountryCode; } export const ChangePassword = ({ onEditInfo, onPasswordChanged, forgettedPasswordInfo, + infoType, + countryCode, }: ChangePasswordProps) => { const theme = useTheme(); const { t } = useTranslation('authentication'); @@ -75,7 +83,7 @@ export const ChangePassword = ({ setConfirmInputTouched(true); }; - const handleSubmit = () => { + const handleSubmit = async () => { if (!passValue || !isValidPassword(passValue)) { setInputTouched(true); inputRef.current?.focus(); @@ -85,15 +93,29 @@ export const ChangePassword = ({ } else { setChangePasswordLoading(true); - // Change setTimeout to api call - setTimeout(() => { - setChangePassAlertOpen(true); - // setLoginStatus('success'); - // setLoginStatus('failed'); - // setLoginFailedMessage('رمز عبور اشتباه میباشد'); + const apiRequest: ResetPasswordRequest = { + email: infoType === 'email' ? forgettedPasswordInfo : undefined, + phoneNumber: + infoType === 'phone' + ? countryCode + forgettedPasswordInfo + : undefined, + newPassword: passValue, + confirmNewPassword: confirmPassValue, + }; + + const result = await resetPassword(apiRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { + setChangePasswordStatus('success'); onPasswordChanged(); - setChangePasswordLoading(false); - }, 1000); + } else { + setChangePasswordStatus('failed'); + setChangePassFailedMessage(jsonRes.message); + } + setChangePassAlertOpen(true); + + setChangePasswordLoading(false); } }; diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx index a8112fb..a980bed 100644 --- a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx @@ -3,6 +3,7 @@ import type { AuthType } from '../../types/authTypes'; import { ForgettedPasswordInfo } from './ForgettedPasswordInfo'; import { ForgetPasswordOtp } from './ForgetPasswordOtp'; import { ChangePassword } from './ChangePassword'; +import type { CountryCode } from '@/types/commonTypes'; export const ForgetPasswordContainer = () => { const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState< @@ -10,10 +11,10 @@ export const ForgetPasswordContainer = () => { >('enterInfo'); const [forgettedPasswordInfo, setForgettedPasswordInfo] = useState(''); + const [infoCountryCode, setInfoCountryCode] = useState('+98'); const [infoType, setInfoType] = useState('email'); - const handleSendForgetPassOtp = (value: string) => { - console.log(value); + const handleVerifyOtp = (value: string) => { setForgetPassCurrentStep('verifyOtp'); }; @@ -37,12 +38,15 @@ export const ForgetPasswordContainer = () => { setInfoType={setInfoType} forgettedPasswordInfo={forgettedPasswordInfo} setForgettedPasswordInfo={setForgettedPasswordInfo} - onSendOtp={handleSendForgetPassOtp} + onVerifyOtp={handleVerifyOtp} + countryCode={infoCountryCode} + setCountryCode={setInfoCountryCode} /> )} {forgetPassCurrentStep === 'verifyOtp' && ( { onEditInfo={handleEditInfo} forgettedPasswordInfo={forgettedPasswordInfo} onPasswordChanged={handlePasswordChanged} + infoType={infoType} + countryCode={infoCountryCode} /> )} diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx index 62c81cb..275a79a 100644 --- a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx @@ -6,10 +6,14 @@ import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; +import type { ConfirmForgetPassCodeRequest } from '../../types/userTypes'; +import type { CountryCode } from '@/types/commonTypes'; +import { confirmForgetPassCode } from '../../api/authorizationAPI'; interface ForgetPasswordOtpProps { forgettedPasswordInfo: string; infoType: AuthType; + countryCode: CountryCode; onEditInfo: () => void; onOTPVerified: (otpCode: string) => void; } @@ -17,6 +21,7 @@ interface ForgetPasswordOtpProps { export function ForgetPasswordOtp({ forgettedPasswordInfo, infoType, + countryCode, onEditInfo, onOTPVerified, }: ForgetPasswordOtpProps) { @@ -25,7 +30,7 @@ export function ForgetPasswordOtp({ const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>(); const [verifyStatusLoading, setVerifyStatusLoading] = useState(false); - const [verifyAlertOpen, setVerifyAlertOpen] = useState(false); + const [verifyAlertMessage, setVerifyAlertMessage] = useState(); const { t } = useTranslation('authentication'); const [resendTimer, setResendTimer] = useState(120); const [canResend, setCanResend] = useState(false); @@ -70,7 +75,7 @@ export function ForgetPasswordOtp({ setOtpCode(formattedValue); }; - const handleVerifyOTP = () => { + const handleVerifyOTP = async () => { if (!otpCode || otpCode.length < 4) { setOtpDigitInvalid(true); } else { @@ -78,11 +83,25 @@ export function ForgetPasswordOtp({ setVerifyStatusLoading(true); // Change setTimeout to api call - setTimeout(() => { - setVerifyAlertOpen(false); + const apiRequest: ConfirmForgetPassCodeRequest = { + email: infoType === 'email' ? forgettedPasswordInfo : undefined, + phoneNumber: + infoType === 'phone' + ? countryCode + forgettedPasswordInfo + : undefined, + code: otpCode, + }; + + const result = await confirmForgetPassCode(apiRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { onOTPVerified(otpCode); - setVerifyStatusLoading(false); - }, 1000); + } else { + setVerifyAlertMessage(jsonRes.message); + } + + setVerifyStatusLoading(false); } }; @@ -90,11 +109,11 @@ export function ForgetPasswordOtp({ setVerifyAlertOpen(false)} + open={!!verifyAlertMessage} + onClose={() => setVerifyAlertMessage(undefined)} color={'error'} > - {t('verify.theVerificationCodeIsIncorrect')} + {verifyAlertMessage} ; infoType: AuthType; setInfoType: Dispatch; - onSendOtp: (value: string) => void; + onVerifyOtp: (value: string) => void; + countryCode: CountryCode; + setCountryCode: Dispatch; } export function ForgettedPasswordInfo({ @@ -29,15 +35,18 @@ export function ForgettedPasswordInfo({ setForgettedPasswordInfo, infoType, setInfoType, - onSendOtp, + onVerifyOtp, + countryCode, + setCountryCode, }: ForgettedPasswordInfoProps) { const { t, i18n } = useTranslation('authentication'); - const [countryCode, setCountryCode] = useState('+98'); const textFieldRef = useRef(null); const inputRef = useRef(null); const dir = i18n.dir(); const [error, setError] = useState(); const [touched, setTouched] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + const [sendCodeLoading, setSendCodeLoading] = useState(false); const inputError: boolean = touched && !!error; const handleInputChange = (event: React.ChangeEvent) => { @@ -75,25 +84,42 @@ export function ForgettedPasswordInfo({ return phoneNumber && phoneNumber.isValid(); }; - const isInputValid = (value: string, authType: AuthType): boolean => { + const isInputValid = (value: string, infoType: AuthType): boolean => { if (!value) { return false; } - if (authType === 'email' && !isEmail(value)) { + if (infoType === 'email' && !isEmail(value)) { return false; } - if (authType === 'phone' && !isPhoneValid(countryCode, value)) { + if (infoType === 'phone' && !isPhoneValid(countryCode, value)) { return false; } return true; }; - const handleSubmit = () => { + const handleSubmit = async () => { if (isInputValid(forgettedPasswordInfo, infoType)) { - onSendOtp(forgettedPasswordInfo); + setSendCodeLoading(true); + + const sendCodeRequest: SendForgetPassCodeRequest = { + email: infoType === 'email' ? forgettedPasswordInfo : undefined, + phoneNumber: + infoType === 'phone' + ? countryCode + forgettedPasswordInfo + : undefined, + }; + const result = await sendForgetPassCode(sendCodeRequest); + const jsonRes = await result.json(); + + if (!jsonRes.success) { + setErrorMessage(jsonRes.message); + } + + setSendCodeLoading(false); + onVerifyOtp(forgettedPasswordInfo); } else { inputRef.current?.focus(); validateInput(forgettedPasswordInfo, infoType); @@ -105,6 +131,14 @@ export function ForgettedPasswordInfo({ return ( + setErrorMessage(undefined)} + open={!!errorMessage} + > + {errorMessage} + + {t('forgetPassword.forgetPassword')} @@ -144,7 +178,9 @@ export function ForgettedPasswordInfo({ /> - + ); diff --git a/src/features/authorization/routes/ForgetPasswordPage.tsx b/src/features/authorization/routes/ForgetPasswordPage.tsx new file mode 100644 index 0000000..d30290e --- /dev/null +++ b/src/features/authorization/routes/ForgetPasswordPage.tsx @@ -0,0 +1,23 @@ +import { FlexBox } from '@/components/components/common/FlexBox'; +import Logo from '@/components/Logo'; +import { Paper } from '@mui/material'; +import { useState } from 'react'; +import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps'; +import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer'; + +export function ForgetPasswordPage() { + return ( + + + + + ); +} diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts index 74b5e1d..4f0a8eb 100644 --- a/src/features/authorization/types/userTypes.ts +++ b/src/features/authorization/types/userTypes.ts @@ -86,8 +86,8 @@ export interface SendForgetPassCodeRequest { // ConfirmForgetPassCode export interface ConfirmForgetPassCodeRequest { - email: string; - phoneNumber: string; + email?: string; + phoneNumber?: string; code: string; } From 945aa379ea4ad1e67b1b0210d62c202c4c23b8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sun, 10 Aug 2025 12:21:41 +0330 Subject: [PATCH 21/32] feat: login with password apis added --- src/App.tsx | 4 +- .../authorization/api/authorizationAPI.ts | 3 +- .../AuthenticationSteps.tsx | 28 +++++++- .../AuthenticationSteps/EnterPasswordForm.tsx | 68 +++++++++++++++---- .../AuthenticationSteps/OtpVerifyForm.tsx | 15 +++- .../ForgetPassword/ChangePassword.tsx | 2 +- .../ForgetPassword/ForgetPasswordOtp.tsx | 4 +- src/features/authorization/types/userTypes.ts | 7 ++ 8 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 585b205..a463dc1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,8 +12,8 @@ function App() { - } /> - } /> + } /> + } /> } /> diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts index 47a8786..25175e7 100644 --- a/src/features/authorization/api/authorizationAPI.ts +++ b/src/features/authorization/api/authorizationAPI.ts @@ -12,6 +12,7 @@ import type { LoginOrSignUpWithGoogleResponse, LoginRequest, LoginResponse, + PasswordLoginRequest, ResetPasswordRequest, ResetPasswordResponse, SendEmailOtpRequest, @@ -49,7 +50,7 @@ export const loginOrSignUpWithOtp = async (body: LoginRequest) => { return fetchRequest('User/LoginOrSignUpWithOtp', body); }; -export const loginWithPassword = async (body: LoginRequest) => { +export const loginWithPassword = async (body: PasswordLoginRequest) => { return fetchRequest('User/LoginWithPassword', body); }; diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index 8d8d496..c61f104 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -5,7 +5,11 @@ import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; -import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; +import { + getUserStatusByPhoneNumberOrEmail, + sendEmailOtp, + sendSmsOtp, +} from '../../api/authorizationAPI'; import { UserStatus } from '../../types/userTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; @@ -51,11 +55,16 @@ export const AuthenticationSteps = (): JSX.Element => { const handleOTPVerfied = ( registeredWithoutPhoneNumber: boolean = false, userId: GUID, + returnUrl?: string, ) => { localStorage.setItem('userID', userId); // if (registeredWithoutPhoneNumber) { // setCurrentStep('addPhoneNumber'); // } + + if (returnUrl) { + location.href = returnUrl; + } }; const handleEditValue = () => { @@ -74,7 +83,17 @@ export const AuthenticationSteps = (): JSX.Element => { setCurrentStep('emailOrPhone'); }; - const handleLoggedInWithPassowrd = () => {}; + const handleLoggedInWithPassowrd = (userId: GUID, returnUrl?: string) => { + localStorage.setItem('userID', userId); + + if (returnUrl) { + location.href = returnUrl; + } + }; + + const handleLoginWithOtpInsteadOfPassword = async () => { + setCurrentStep('verify'); + }; return ( <> @@ -103,9 +122,12 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'enterPassword' && ( setCurrentStep('verify')} + onLoginWithOTP={handleLoginWithOtpInsteadOfPassword} emailOrPhone={loginRegisterValue} /> )} diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index 579a551..d7418ac 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -11,12 +11,24 @@ import { } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { Toast } from '@/components/Toast'; +import { Link, Navigate, useSearchParams } from 'react-router'; +import type { AuthType } from '../../types/authTypes'; +import type { CountryCode, GUID } from '@/types/commonTypes'; +import { + loginWithPassword, + sendEmailOtp, + sendSmsOtp, +} from '../../api/authorizationAPI'; +import type { LoginRequest, PasswordLoginRequest } from '../../types/userTypes'; export interface EnterPasswordFormProps { onEditValue: () => void; onLoginWithOTP: () => void; - onLoggedIn: () => void; + onLoggedIn: (userId: GUID, returnUrl?: string) => void; emailOrPhone: string; + authType: AuthType; + loginRegisterValue: string; + countryCode: CountryCode; } export const EnterPasswordForm = ({ @@ -24,7 +36,12 @@ export const EnterPasswordForm = ({ onLoginWithOTP, onLoggedIn, emailOrPhone, + authType, + loginRegisterValue, + countryCode, }: EnterPasswordFormProps) => { + const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; + const [searchParams] = useSearchParams(); const { t } = useTranslation('authentication'); const [passValue, setPassValue] = useState(''); const [inputTouched, setInputTouched] = useState(false); @@ -34,29 +51,53 @@ export const EnterPasswordForm = ({ const [loginStatus, setLoginStatus] = useState<'success' | 'failed'>(); const [loginAlertOpen, setLoginAlertOpen] = useState(false); const [loginFailedMessage, setLoginFailedMessage] = useState(''); + const [sendOtpLoading, setSendOtpLoading] = useState(false); const handleBlur = () => { setInputTouched(true); }; - const handleSubmit = () => { + const handleSubmit = async () => { if (!passValue) { inputRef.current?.focus(); } else { setLoginLoading(true); - // Change setTimeout to api call - setTimeout(() => { - setLoginAlertOpen(true); - // setLoginStatus('success'); + const apiRequest: PasswordLoginRequest = { + phoneNumber: + authType === 'phone' ? countryCode + loginRegisterValue : undefined, + email: authType === 'email' ? loginRegisterValue : undefined, + password: passValue, + returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, + }; + const result = await loginWithPassword(apiRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { + setLoginStatus('success'); + onLoggedIn(jsonRes.userId, jsonRes.returnUrl); + } else { setLoginStatus('failed'); - setLoginFailedMessage('رمز عبور اشتباه میباشد'); - onLoggedIn(); - setLoginLoading(false); - }, 1000); + setLoginFailedMessage(jsonRes.message); + } + setLoginAlertOpen(true); + setLoginLoading(false); } }; + const handleLoginWithOtp = async () => { + setSendOtpLoading(true); + + if (authType === 'phone') { + await sendSmsOtp({ phoneNumber: countryCode + loginRegisterValue }); + } else { + await sendEmailOtp({ email: loginRegisterValue }); + } + + setSendOtpLoading(false); + onLoginWithOTP(); + }; + return ( - + + + ); diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index 776f96b..3c37714 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -21,7 +21,11 @@ interface OtpVerifyFormProps { authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: (registeredWithoutPhoneNumber: boolean, userID: GUID) => void; + onOTPVerified: ( + registeredWithoutPhoneNumber: boolean, + userID: GUID, + returnUrl?: string, + ) => void; } export function OtpVerifyForm({ @@ -32,6 +36,7 @@ export function OtpVerifyForm({ onEditValue, onOTPVerified, }: OtpVerifyFormProps) { + const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; const [searchParams] = useSearchParams(); const [otpCode, setOtpCode] = useState(''); const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); @@ -97,14 +102,18 @@ export function OtpVerifyForm({ otpCode: otpCode, phoneNumber: authType === 'phone' ? countryCode + value : undefined, email: authType === 'email' ? value : undefined, - returnUrl: searchParams.get('returnUrl') ?? '/', + returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, }; const result = await loginOrSignUpWithOtp(loginRequest); const jsonRes = await result.json(); if (jsonRes.success) { setVerifyStatus('success'); - onOTPVerified(jsonRes.registeredWithOutPhoneNumber, jsonRes.userId); + onOTPVerified( + jsonRes.registeredWithOutPhoneNumber, + jsonRes.userId, + jsonRes.returnUrl, + ); } else { setVerifyStatus('failed'); setErrorMessage(jsonRes.message); diff --git a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx index 27877a8..b4b35dd 100644 --- a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx +++ b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx @@ -241,7 +241,7 @@ export const ChangePassword = ({ color="primary" onClick={() => setShowConfirmPassword(!showConfirmPassword)} > - {showPassword ? : } + {showConfirmPassword ? : } ) : ( '' diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx index 275a79a..5f50a7f 100644 --- a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx @@ -137,7 +137,9 @@ export function ForgetPasswordOtp({ endIcon={} onClick={onEditInfo} > - {forgettedPasswordInfo} + {infoType === 'phone' + ? countryCode + forgettedPasswordInfo + : forgettedPasswordInfo} diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts index 4f0a8eb..22d974d 100644 --- a/src/features/authorization/types/userTypes.ts +++ b/src/features/authorization/types/userTypes.ts @@ -28,6 +28,13 @@ export interface LoginRequest { returnUrl: string; } +export interface PasswordLoginRequest { + phoneNumber?: string; + email?: string; + password: string; + returnUrl: string; +} + export interface LoginResponse extends ApiResponse { returnUrl: string; userId: GUID; From a4ee95832fb01ce314a10693a8efe2368b7b3b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sun, 10 Aug 2025 15:37:09 +0330 Subject: [PATCH 22/32] feat: login with google added --- index.html | 57 +++++++------ .../AuthenticationSteps.tsx | 61 +++++--------- .../AuthenticationSteps/EnterPasswordForm.tsx | 10 +-- .../GoogleAuthentication.tsx | 84 +++++++++++++++++++ .../AuthenticationSteps/LoginRegiserForm.tsx | 18 ++-- .../AuthenticationSteps/OtpVerifyForm.tsx | 18 ++-- .../AuthenticationSteps/VerifyPhoneNumber.tsx | 4 +- src/features/authorization/types/userTypes.ts | 4 + 8 files changed, 163 insertions(+), 93 deletions(-) create mode 100644 src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx diff --git a/index.html b/index.html index 6e5d786..c4f15da 100644 --- a/index.html +++ b/index.html @@ -1,29 +1,32 @@ - - - - - Harmony club - - - - -
- - - + + + + + + Harmony club + + + + + +
+ + + + \ No newline at end of file diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index c61f104..ce4f113 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -13,8 +13,13 @@ import { import { UserStatus } from '../../types/userTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; +import { useSearchParams } from 'react-router'; export const AuthenticationSteps = (): JSX.Element => { + const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; + const [searchParams] = useSearchParams(); + const authReturnUrl: string = + searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL; const [authMode, setAuthMode] = useState('register'); const [authType, setAuthType] = useState('phone'); const [currentStep, setCurrentStep] = useState< @@ -55,50 +60,26 @@ export const AuthenticationSteps = (): JSX.Element => { const handleOTPVerfied = ( registeredWithoutPhoneNumber: boolean = false, userId: GUID, - returnUrl?: string, ) => { - localStorage.setItem('userID', userId); - // if (registeredWithoutPhoneNumber) { - // setCurrentStep('addPhoneNumber'); - // } - - if (returnUrl) { - location.href = returnUrl; + if (registeredWithoutPhoneNumber) { + setCurrentStep('addPhoneNumber'); } + + handleUserLoggedIn(userId); }; - const handleEditValue = () => { - setCurrentStep('emailOrPhone'); - }; - - const handleCompleteSignUp = (countryCode: string, value: string) => { - setCurrentStep('addedPhoneNumberVerify'); - }; - - const handleCompleteSignUpOTPVerified = () => { - console.log('phoneNumberVerified'); - }; - - const handleCompleteSignUpEditValue = () => { - setCurrentStep('emailOrPhone'); - }; - - const handleLoggedInWithPassowrd = (userId: GUID, returnUrl?: string) => { + const handleUserLoggedIn = (userId: GUID) => { localStorage.setItem('userID', userId); - if (returnUrl) { - location.href = returnUrl; - } - }; - - const handleLoginWithOtpInsteadOfPassword = async () => { - setCurrentStep('verify'); + location.href = authReturnUrl; }; return ( <> {currentStep === 'emailOrPhone' && ( { {currentStep === 'verify' && ( setCurrentStep('emailOrPhone')} authMode={authMode} authType={authType} value={loginRegisterValue} @@ -122,12 +104,13 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'enterPassword' && ( setCurrentStep('emailOrPhone')} + onLoginWithOTP={() => setCurrentStep('verify')} emailOrPhone={loginRegisterValue} /> )} @@ -137,16 +120,16 @@ export const AuthenticationSteps = (): JSX.Element => { value={addedPhoneNumberValue} setValue={setAddedPhoneNumberValue} email={loginRegisterValue} - onCompleteSignUp={handleCompleteSignUp} + onCompleteSignUp={() => setCurrentStep('addedPhoneNumberVerify')} /> )} {currentStep === 'addedPhoneNumberVerify' && ( setCurrentStep('emailOrPhone')} value={addedPhoneNumberValue} - onPhoneNumberVerified={handleCompleteSignUpOTPVerified} + onPhoneNumberVerified={handleUserLoggedIn} /> )} diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index d7418ac..56dadd4 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -24,11 +24,12 @@ import type { LoginRequest, PasswordLoginRequest } from '../../types/userTypes'; export interface EnterPasswordFormProps { onEditValue: () => void; onLoginWithOTP: () => void; - onLoggedIn: (userId: GUID, returnUrl?: string) => void; + onLoggedIn: (userId: GUID) => void; emailOrPhone: string; authType: AuthType; loginRegisterValue: string; countryCode: CountryCode; + authReturnUrl: string; } export const EnterPasswordForm = ({ @@ -39,9 +40,8 @@ export const EnterPasswordForm = ({ authType, loginRegisterValue, countryCode, + authReturnUrl, }: EnterPasswordFormProps) => { - const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; - const [searchParams] = useSearchParams(); const { t } = useTranslation('authentication'); const [passValue, setPassValue] = useState(''); const [inputTouched, setInputTouched] = useState(false); @@ -68,14 +68,14 @@ export const EnterPasswordForm = ({ authType === 'phone' ? countryCode + loginRegisterValue : undefined, email: authType === 'email' ? loginRegisterValue : undefined, password: passValue, - returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, + returnUrl: authReturnUrl, }; const result = await loginWithPassword(apiRequest); const jsonRes = await result.json(); if (jsonRes.success) { setLoginStatus('success'); - onLoggedIn(jsonRes.userId, jsonRes.returnUrl); + onLoggedIn(jsonRes.userId); } else { setLoginStatus('failed'); setLoginFailedMessage(jsonRes.message); diff --git a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx new file mode 100644 index 0000000..c5c49cf --- /dev/null +++ b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx @@ -0,0 +1,84 @@ +import { Button } from '@mui/material'; +import { Google } from 'iconsax-reactjs'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { GoogleCodeClientResponse } from '../../types/userTypes'; +import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI'; +import type { GUID } from '@/types/commonTypes'; + +declare global { + interface Window { + google: typeof google; + } + const google: any; +} + +export interface GoogleAuthenticationProps { + disabled: boolean; + authReturnUrl: string; + onGoogleAuthenticated: (userId: GUID) => void; +} + +export const GoogleAuthentication = ({ + disabled, + authReturnUrl, + onGoogleAuthenticated, +}: GoogleAuthenticationProps) => { + const { t } = useTranslation('authentication'); + const [loginWithGoogleLoading, setLoginWithGoogleLoading] = + useState(false); + + const clientRef = useRef(null); + + useEffect(() => { + const script = document.createElement('script'); + script.src = 'https://accounts.google.com/gsi/client'; + script.async = true; + script.defer = true; + document.body.appendChild(script); + + script.onload = () => { + clientRef.current = google.accounts.oauth2.initCodeClient({ + client_id: 'CLIEND_ID', + scope: 'openid email profile', + ux_mode: 'popup', + response_type: 'id_token', + callback: async (resp: GoogleCodeClientResponse) => { + setLoginWithGoogleLoading(true); + + const result = await loginOrSignUpWithGoogle({ + idToken: resp.id_token, + returnUrl: authReturnUrl, + }); + const jsonRes = await result.json(); + + if (jsonRes.success) { + onGoogleAuthenticated(jsonRes.userId); + } else { + handleGoogleLogin(); + } + + setLoginWithGoogleLoading(false); + }, + }); + }; + }, []); + + const handleGoogleLogin = () => { + if (clientRef.current) { + clientRef.current.requestCode(); + } + }; + + return ( + + ); +}; diff --git a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx index 3d64449..1f7aa5a 100644 --- a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -18,7 +18,8 @@ import { CountryCodeSelector } from '../CountryCodeSelector'; import type { UserStatus } from '../../types/userTypes'; import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI'; import { Toast } from '@/components/Toast'; -import type { CountryCode } from '@/types/commonTypes'; +import type { CountryCode, GUID } from '@/types/commonTypes'; +import { GoogleAuthentication } from './GoogleAuthentication'; export interface LoginRegisterFormProps { loginRegisterValue: string; @@ -28,6 +29,8 @@ export interface LoginRegisterFormProps { authType: AuthType; setAuthType: Dispatch; onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void; + authReturnUrl: string; + onGoogleAuthenticated: (userId: GUID) => void; } export function LoginRegisterForm({ @@ -38,6 +41,8 @@ export function LoginRegisterForm({ authType, setAuthType, onLoginRegisterSubmit, + authReturnUrl, + onGoogleAuthenticated, }: LoginRegisterFormProps) { const [checkStatusLoading, setCheckStatusLoading] = useState(false); const { t, i18n } = useTranslation('authentication'); @@ -172,13 +177,12 @@ export function LoginRegisterForm({ - + /> ); diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index 3c37714..baa6bc4 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -21,11 +21,8 @@ interface OtpVerifyFormProps { authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: ( - registeredWithoutPhoneNumber: boolean, - userID: GUID, - returnUrl?: string, - ) => void; + onOTPVerified: (registeredWithoutPhoneNumber: boolean, userID: GUID) => void; + authReturnUrl: string; } export function OtpVerifyForm({ @@ -35,9 +32,8 @@ export function OtpVerifyForm({ authMode, onEditValue, onOTPVerified, + authReturnUrl, }: OtpVerifyFormProps) { - const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; - const [searchParams] = useSearchParams(); const [otpCode, setOtpCode] = useState(''); const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>(); @@ -102,18 +98,14 @@ export function OtpVerifyForm({ otpCode: otpCode, phoneNumber: authType === 'phone' ? countryCode + value : undefined, email: authType === 'email' ? value : undefined, - returnUrl: searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL, + returnUrl: authReturnUrl, }; const result = await loginOrSignUpWithOtp(loginRequest); const jsonRes = await result.json(); if (jsonRes.success) { setVerifyStatus('success'); - onOTPVerified( - jsonRes.registeredWithOutPhoneNumber, - jsonRes.userId, - jsonRes.returnUrl, - ); + onOTPVerified(jsonRes.registeredWithOutPhoneNumber, jsonRes.userId); } else { setVerifyStatus('failed'); setErrorMessage(jsonRes.message); diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx index b047bdc..4236d59 100644 --- a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx +++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx @@ -13,13 +13,13 @@ import { sendEmailOtp, sendSmsOtp, } from '../../api/authorizationAPI'; -import type { CountryCode } from '@/types/commonTypes'; +import type { CountryCode, GUID } from '@/types/commonTypes'; interface VerifyPhoneNumberProps { value: string; countryCode: CountryCode; onEditValue: () => void; - onPhoneNumberVerified: () => void; + onPhoneNumberVerified: (userId: GUID) => void; } export function VerifyPhoneNumber({ diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts index 22d974d..99eb998 100644 --- a/src/features/authorization/types/userTypes.ts +++ b/src/features/authorization/types/userTypes.ts @@ -100,6 +100,10 @@ export interface ConfirmForgetPassCodeRequest { // LoginOrSignUpWithGoogle +export interface GoogleCodeClientResponse { + id_token: string; +} + export interface LoginOrSignUpWithGoogleRequest { idToken: string; returnUrl: string; From 1584b7d7a88b412cae403885873f10b93aef8710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sun, 10 Aug 2025 16:02:07 +0330 Subject: [PATCH 23/32] chore: card responsive --- .../authorization/components/AuthenticationCard.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/features/authorization/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx index bde57cd..a4f5e0d 100644 --- a/src/features/authorization/components/AuthenticationCard.tsx +++ b/src/features/authorization/components/AuthenticationCard.tsx @@ -1,4 +1,4 @@ -import { Paper } from '@mui/material'; +import { Box, Paper } from '@mui/material'; import React, { type PropsWithChildren } from 'react'; // Beacuse in the otp verify there is a element outside of the authentication card @@ -8,8 +8,13 @@ export const AuthenticationCard = ({ children }: PropsWithChildren) => { elevation={0} sx={{ borderRadius: 4, - p: 6, - width: '34.5rem', + p: { + xs: 4, + md: 6, + }, + marginInline: 2, + width: (t) => `calc(100% - ${t.spacing(2)})`, + maxWidth: '552px', }} > {children} From 848ca4dd625d37d7b8d49d9a68d992f8ce549a72 Mon Sep 17 00:00:00 2001 From: SajadMRjl Date: Sun, 10 Aug 2025 20:01:18 +0330 Subject: [PATCH 24/32] feat: add router config, router, and sidenav config --- azure-pipelines.yml | 18 ++++----- package-lock.json | 9 ++--- package.json | 2 + public/locales/fa/common.json | 6 ++- src/App.tsx | 61 +++--------------------------- src/components/Layout/Layout.tsx | 46 ++++++++++++++++++++++ src/components/Layout/navItems.tsx | 27 +++++++++++++ src/routes/config.tsx | 48 +++++++++++++++++++++++ src/routes/index.tsx | 34 +++++++++++++++++ 9 files changed, 179 insertions(+), 72 deletions(-) create mode 100644 src/components/Layout/Layout.tsx create mode 100644 src/components/Layout/navItems.tsx create mode 100644 src/routes/config.tsx create mode 100644 src/routes/index.tsx diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7c90784..cc58a0d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,18 +4,18 @@ # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript trigger: -- develop + - develop pool: vmImage: ubuntu-latest steps: -- task: NodeTool@0 - inputs: - versionSpec: '20.x' - displayName: 'Install Node.js' + - task: NodeTool@0 + inputs: + versionSpec: '20.x' + displayName: 'Install Node.js' -- script: | - npm install - npm run build - displayName: 'npm install and build' + - script: | + npm install + npm run build + displayName: 'npm install and build' diff --git a/package-lock.json b/package-lock.json index 44936b8..0c87afa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", + "iconsax-react": "^0.0.8", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-router-dom": "^7.8.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, @@ -3033,7 +3035,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3995,7 +3996,6 @@ "resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz", "integrity": "sha512-l3dVk4zGtkkJHgvNYqAf0wDKqnKxXykee5/DoESGo2JvSYwaxajJUHSX2YrPRXSov8Hd8ClGFwJxCEaEjrFD1Q==", "license": "MIT", - "peer": true, "dependencies": { "prop-types": "^15.7.2" }, @@ -5033,7 +5033,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz", "integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -5056,7 +5055,6 @@ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", "license": "MIT", - "peer": true, "dependencies": { "react-router": "7.8.0" }, @@ -5321,8 +5319,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/setimmediate": { "version": "1.0.5", diff --git a/package.json b/package.json index 00ed48f..da6fe2c 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,11 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", + "iconsax-react": "^0.0.8", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-router-dom": "^7.8.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index 3f4cd0d..71d6ae5 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -1,3 +1,7 @@ { - "helloWorld": "سلام دنیا" + "side": { + "account": "حساب کاربری", + "personalInfo": "اطلاعات شخصی", + "contactInfo": "شماره تماس" + } } diff --git a/src/App.tsx b/src/App.tsx index f1bd30e..ec7f7b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,68 +1,17 @@ -import { - Alert, - Box, - CssBaseline, - TextField, - Typography, - useColorScheme, -} from '@mui/material'; +import { CssBaseline } from '@mui/material'; import './App.css'; -import { useTranslation } from 'react-i18next'; -import { LanguageManager } from './components/LanguageManager'; +import { LanguageManager } from '@/components/LanguageManager'; +import { RouterProvider } from 'react-router-dom'; +import { router } from '@/routes'; function App() { - const { t } = useTranslation(); - const showToast = useToast(); - return ( <> -
- {t('helloWorld')} - - - - - - success - - - warning - - - info - - - error - - -
- + ); } export default App; - -import { Button } from '@mui/material'; -import { useToast } from '@rkheftan/harmony-ui'; - -export const ThemeToggleButton = () => { - const { mode, setMode } = useColorScheme(); - - return ( - - ); -}; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..504dd3e --- /dev/null +++ b/src/components/Layout/Layout.tsx @@ -0,0 +1,46 @@ +import { SideNav } from '@rkheftan/harmony-ui'; +import { buildNavItems } from './navItems'; +import { appRoutes } from '@/routes/config'; +import { Outlet, useLocation } from 'react-router-dom'; +import { Box } from '@mui/material'; +import { grey } from '@mui/material/colors'; + +export const Layout = () => { + const navItemConfigs = buildNavItems(appRoutes); + const location = useLocation(); + + return ( + + + + + + + + + + ); +}; diff --git a/src/components/Layout/navItems.tsx b/src/components/Layout/navItems.tsx new file mode 100644 index 0000000..7160ea9 --- /dev/null +++ b/src/components/Layout/navItems.tsx @@ -0,0 +1,27 @@ +// src/components/SideNav.tsx (Conceptual Example) + +import { useTranslation } from 'react-i18next'; +import { type RouteConfig } from '@/routes/config'; +import { Icon, type NavItemConfig } from '@rkheftan/harmony-ui'; +import type { Icon as Iconsax } from 'iconsax-react'; + +const getIcon = (icon?: Iconsax) => (isSelected: boolean) => + icon ? ( + + ) : undefined; + +export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] { + const { t } = useTranslation(); + + return routes + .filter((route) => route.navConfig) + .map((route) => { + const { title, icon } = route.navConfig!; + return { + text: t(title), + getIcon: getIcon(icon), + path: route.path, + children: route.children ? buildNavItems(route.children) : undefined, + }; + }); +} diff --git a/src/routes/config.tsx b/src/routes/config.tsx new file mode 100644 index 0000000..b5ed42c --- /dev/null +++ b/src/routes/config.tsx @@ -0,0 +1,48 @@ +import { Layout } from '@/components/Layout/Layout'; +import { Mobile, Personalcard, ProfileCircle, type Icon } from 'iconsax-react'; +import { type ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; + +export interface RouteConfig { + path: string; + element?: ReactNode; + navConfig?: { + title: string; // Translation key + icon?: Icon; + }; + children?: RouteConfig[]; +} + +export const appRoutes: RouteConfig[] = [ + { + path: '/', + element: , + }, + { + path: '/profile', + // can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));) + element: , + navConfig: { + title: 'side.account', + icon: ProfileCircle, + }, + children: [ + { + path: '/profile/info', + element:
Personal Info Section
, + navConfig: { + title: 'side.personalInfo', + icon: Personalcard, + }, + }, + { + path: '/profile/contact-info', + element:
Personal Info Section
, + navConfig: { + title: 'side.contactInfo', + icon: Mobile, + }, + }, + ], + }, +]; diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100644 index 0000000..541a2fb --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,34 @@ +import { Suspense, type ReactNode } from 'react'; +import { createBrowserRouter, type RouteObject } from 'react-router-dom'; +import { appRoutes, type RouteConfig } from './config'; + +/** + * A recursive function to map our custom route config to the format + * that react-router-dom expects, applying layouts and guards. + */ +function mapRoutes(routes: RouteConfig[]): RouteObject[] { + return routes.map((route) => { + // Start with the base element, wrapped in Suspense for lazy loading + let element: ReactNode = ( + Loading...}>{route.element} + ); + + // Conditionally wrap the element in the specified layout + // if (route.layout) { + // element = {element}; + // } + + // Conditionally wrap the element in the authentication guard + // if (route.authRequired) { + // element = {element}; + // } + + return { + path: route.path, + element: element, + ...(route.children && { children: mapRoutes(route.children) }), + }; + }); +} + +export const router = createBrowserRouter(mapRoutes(appRoutes)); From b8f72217804158bd6cb9ceb7bbfd7d6e00270004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Mon, 11 Aug 2025 14:19:32 +0330 Subject: [PATCH 25/32] feat: complete sign up apis added --- .../AuthenticationSteps.tsx | 18 ++-- .../AuthenticationSteps/CompleteSignUp.tsx | 17 +++- .../AuthenticationSteps/OtpVerifyForm.tsx | 85 +++++++++++++------ .../AuthenticationSteps/VerifyPhoneNumber.tsx | 37 ++++---- 4 files changed, 102 insertions(+), 55 deletions(-) diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index ce4f113..8f08908 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -31,6 +31,8 @@ export const AuthenticationSteps = (): JSX.Element => { >('emailOrPhone'); const [loginRegisterValue, setLoginRegisterValue] = useState(''); const [countryCode, setCountryCode] = useState('+98'); + const [addPhoneCountryCode, setAddPhoneCountryCode] = + useState('+98'); const [addedPhoneNumberValue, setAddedPhoneNumberValue] = useState(''); @@ -57,14 +59,7 @@ export const AuthenticationSteps = (): JSX.Element => { } }; - const handleOTPVerfied = ( - registeredWithoutPhoneNumber: boolean = false, - userId: GUID, - ) => { - if (registeredWithoutPhoneNumber) { - setCurrentStep('addPhoneNumber'); - } - + const handleOTPVerfied = (userId: GUID) => { handleUserLoggedIn(userId); }; @@ -92,6 +87,7 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'verify' && ( setCurrentStep('addPhoneNumber')} authReturnUrl={authReturnUrl} countryCode={countryCode} onOTPVerified={handleOTPVerfied} @@ -117,6 +113,8 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'addPhoneNumber' && ( { {currentStep === 'addedPhoneNumberVerify' && ( setCurrentStep('emailOrPhone')} + onEditValue={() => setCurrentStep('addPhoneNumber')} value={addedPhoneNumberValue} + email={loginRegisterValue} onPhoneNumberVerified={handleUserLoggedIn} /> )} diff --git a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx index 74a5cb3..063892c 100644 --- a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx +++ b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx @@ -4,11 +4,15 @@ import React, { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { AuthenticationCard } from '../AuthenticationCard'; import { CountryCodeSelector } from '../CountryCodeSelector'; +import { sendSmsOtp } from '../../api/authorizationAPI'; +import type { CountryCode } from '@/types/commonTypes'; export interface CompleteSignUpProps { email: string; value: string; setValue: Dispatch; + countryCode: CountryCode; + setCountryCode: Dispatch; onCompleteSignUp: (countryCode: string, value: string) => void; } @@ -16,15 +20,17 @@ export const CompleteSignUp = ({ email, value, setValue, + countryCode, + setCountryCode, onCompleteSignUp, }: CompleteSignUpProps) => { const { t } = useTranslation('authentication'); - const [countryCode, setCountryCode] = useState('+98'); const [error, setError] = useState(); const textFieldRef = useRef(null); const inputRef = useRef(null); const [touched, setTouched] = useState(false); const inputError: boolean = touched && !!error; + const [sendOtpLoading, setSendOtpLoading] = useState(false); const isPhoneValid = (code: string, phone: string) => { const phoneNumber = parsePhoneNumberFromString(code + phone); @@ -45,7 +51,7 @@ export const CompleteSignUp = ({ } }; - const handleCompleteSignUp = () => { + const handleCompleteSignUp = async () => { if (!value) { setError(t('loginForm.thisFieldIsRequired')); inputRef.current?.focus(); @@ -55,7 +61,12 @@ export const CompleteSignUp = ({ inputRef.current?.focus(); } else { setError(undefined); + setSendOtpLoading(true); + + await sendSmsOtp({ phoneNumber: countryCode + value }); onCompleteSignUp(countryCode, value); + + setSendOtpLoading(false); } }; @@ -99,7 +110,7 @@ export const CompleteSignUp = ({ sx={{ my: 4 }} /> - diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index baa6bc4..c1f992a 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -6,9 +6,13 @@ import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; -import type { LoginRequest } from '../../types/userTypes'; +import type { + ConfirmEmailOtpRequest, + LoginRequest, +} from '../../types/userTypes'; import { useSearchParams } from 'react-router'; import { + confirmEmailOtp, loginOrSignUpWithOtp, sendEmailOtp, sendSmsOtp, @@ -21,7 +25,8 @@ interface OtpVerifyFormProps { authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: (registeredWithoutPhoneNumber: boolean, userID: GUID) => void; + onOTPVerified: (userID: GUID) => void; + onVerifyPhoneNumber: () => void; authReturnUrl: string; } @@ -32,6 +37,7 @@ export function OtpVerifyForm({ authMode, onEditValue, onOTPVerified, + onVerifyPhoneNumber, authReturnUrl, }: OtpVerifyFormProps) { const [otpCode, setOtpCode] = useState(''); @@ -85,37 +91,66 @@ export function OtpVerifyForm({ setOtpCode(formattedValue); }; - const handleVerifyOTP = async () => { + const handleVerifyOTP = () => { if (!otpCode || otpCode.length < 4) { setOtpDigitInvalid(true); } else { - setOtpDigitInvalid(false); - setVerifyStatusLoading(true); - - // Change setTimeout to api call - - const loginRequest: LoginRequest = { - otpCode: otpCode, - phoneNumber: authType === 'phone' ? countryCode + value : undefined, - email: authType === 'email' ? value : undefined, - returnUrl: authReturnUrl, - }; - const result = await loginOrSignUpWithOtp(loginRequest); - const jsonRes = await result.json(); - - if (jsonRes.success) { - setVerifyStatus('success'); - onOTPVerified(jsonRes.registeredWithOutPhoneNumber, jsonRes.userId); + if (authMode === 'register' && authType === 'email') { + handleConfirmEmailAndAddPhone(); } else { - setVerifyStatus('failed'); - setErrorMessage(jsonRes.message); + handleLoginRequestWithOtp(); } - - setVerifyAlertOpen(true); - setVerifyStatusLoading(false); } }; + const handleConfirmEmailAndAddPhone = async () => { + setOtpDigitInvalid(false); + setVerifyStatusLoading(true); + + const confirmOtpRequest: ConfirmEmailOtpRequest = { + otpCode: otpCode, + email: value, + }; + const result = await confirmEmailOtp(confirmOtpRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { + setVerifyStatus('success'); + onVerifyPhoneNumber(); + } else { + setVerifyStatus('failed'); + setErrorMessage(jsonRes.message); + } + + setVerifyAlertOpen(true); + setVerifyStatusLoading(false); + }; + + const handleLoginRequestWithOtp = async () => { + setOtpDigitInvalid(false); + setVerifyStatusLoading(true); + + const loginRequest: LoginRequest = { + otpCode: otpCode, + phoneNumber: authType === 'phone' ? countryCode + value : undefined, + email: authType === 'email' ? value : undefined, + returnUrl: authReturnUrl, + }; + const result = await loginOrSignUpWithOtp(loginRequest); + const jsonRes = await result.json(); + + if (jsonRes.success) { + setVerifyStatus('success'); + onOTPVerified(jsonRes.userId); + } else { + setVerifyStatus('failed'); + setErrorMessage(jsonRes.message); + } + + setVerifyAlertOpen(true); + setVerifyStatusLoading(false); + }; + const otpMessage = (): string => { if (authType === 'phone' && authMode === 'login') { return t( diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx index 4236d59..a7638a6 100644 --- a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx +++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx @@ -16,19 +16,22 @@ import { import type { CountryCode, GUID } from '@/types/commonTypes'; interface VerifyPhoneNumberProps { + authReturnUrl: string; value: string; + email: string; countryCode: CountryCode; onEditValue: () => void; onPhoneNumberVerified: (userId: GUID) => void; } export function VerifyPhoneNumber({ + authReturnUrl, + email, value, countryCode, onEditValue, onPhoneNumberVerified, }: VerifyPhoneNumberProps) { - const [searchParams] = useSearchParams(); const [otpCode, setOtpCode] = useState(''); const [otpDigitInvalid, setOtpDigitInvalid] = useState(false); const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>(); @@ -83,24 +86,22 @@ export function VerifyPhoneNumber({ setOtpDigitInvalid(false); setVerifyStatusLoading(true); - // Change setTimeout to api call + const loginRequest: LoginRequest = { + otpCode: otpCode, + phoneNumber: countryCode + value, + email: email, + returnUrl: authReturnUrl, + }; + const result = await loginOrSignUpWithOtp(loginRequest); + const jsonRes = await result.json(); - // const loginRequest: LoginRequest = { - // otpCode: otpCode, - // phoneNumber: authType === 'phone' ? countryCode + value : undefined, - // email: authType === 'email' ? value : undefined, - // returnUrl: searchParams.get('returnUrl') ?? '/', - // }; - // const result = await loginOrSignUpWithOtp(loginRequest); - // const jsonRes = await result.json(); - - // if (jsonRes.success) { - // setVerifyStatus('success'); - // onOTPVerified(jsonRes.registeredWithOutPhoneNumber); - // } else { - // setVerifyStatus('failed'); - // setErrorMessage(jsonRes.message); - // } + if (jsonRes.success) { + setVerifyStatus('success'); + onPhoneNumberVerified(jsonRes.userId); + } else { + setVerifyStatus('failed'); + setErrorMessage(jsonRes.message); + } setVerifyAlertOpen(true); setVerifyStatusLoading(false); From 85c16acb741c6221445f1d3c04691560498be528 Mon Sep 17 00:00:00 2001 From: SajadMRjl Date: Mon, 11 Aug 2025 17:29:41 +0330 Subject: [PATCH 26/32] feat: add router config, layout, header, and toolbar --- package-lock.json | 1399 ++++++++++++----------- package.json | 3 +- public/locales/fa/common.json | 9 +- src/components/Layout/Header.tsx | 35 + src/components/Layout/Layout.tsx | 37 +- src/components/Layout/Toolbar.tsx | 75 ++ src/components/Layout/buildNavItems.tsx | 34 + src/components/Layout/navItems.tsx | 27 - src/components/Layout/type.ts | 8 + src/providers/RtlProvider.tsx | 5 +- src/routes/config.tsx | 103 +- 11 files changed, 1036 insertions(+), 699 deletions(-) create mode 100644 src/components/Layout/Header.tsx create mode 100644 src/components/Layout/Toolbar.tsx create mode 100644 src/components/Layout/buildNavItems.tsx delete mode 100644 src/components/Layout/navItems.tsx create mode 100644 src/components/Layout/type.ts diff --git a/package-lock.json b/package-lock.json index 0c87afa..f8b51e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@emotion/styled": "^11.14.1", "@mui/material": "^7.2.0", "@mui/stylis-plugin-rtl": "^7.2.0", - "@rkheftan/harmony-ui": "^0.1.5", + "@rkheftan/harmony-ui": "^0.1.6", + "@types/stylis": "^4.2.7", "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -73,9 +74,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.7.tgz", - "integrity": "sha512-xgu/ySj2mTiUFmdE9yCMfBxLp4DHd5DwmbbD05YAuICfodYT3VvRxbrh81LGQ/8UpSdtMdfKMn3KouYDX59DGQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -83,22 +84,22 @@ } }, "node_modules/@babel/core": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", - "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.27.7", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -113,16 +114,33 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -146,6 +164,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -216,26 +253,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", - "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.7" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -300,36 +337,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", - "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", - "@babel/parser": "^7.27.7", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", - "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -358,12 +386,6 @@ "stylis": "4.2.0" } }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, "node_modules/@emotion/babel-plugin/node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -504,9 +526,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -521,9 +543,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -538,9 +560,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -555,9 +577,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -572,9 +594,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -589,9 +611,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -606,9 +628,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -623,9 +645,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -640,9 +662,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -657,9 +679,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -674,9 +696,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -691,9 +713,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -708,9 +730,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -725,9 +747,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -742,9 +764,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -759,9 +781,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -776,9 +798,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -793,9 +815,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -810,9 +832,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -827,9 +849,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -844,9 +866,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -860,10 +882,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -878,9 +917,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -895,9 +934,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -912,9 +951,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -947,19 +986,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -985,10 +1011,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -996,9 +1046,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1032,6 +1082,34 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1045,10 +1123,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", - "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -1069,32 +1177,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@fast-csv/format": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", @@ -1217,9 +1312,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.11.tgz", - "integrity": "sha512-C512c1ytBTio4MrpWKlJpyFHT6+qfFL8SZ58zBzJ1OOzUEjHeF1BtjY2fH7n4x/g2OV/KiiMLAivOp1DXmiMMw==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1236,15 +1331,15 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.3.tgz", - "integrity": "sha512-AiR5uKpFxP3PjO4R19kQGIMwxyRyPuXmKEEy301V1C0+1rVjS94EZQXf1QKZYN8Q0YM+estSPhmx5JwNftv6nw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.28", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.28.tgz", - "integrity": "sha512-KNNHHwW3EIp4EDYOvYFGyIFfx36R2dNJYH4knnZlF8T5jdbD5Wx8xmSaQ2gP9URkJ04LGEtlcCtwArKcmFcwKw==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1252,9 +1347,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", - "integrity": "sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.1.tgz", + "integrity": "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -1262,22 +1357,22 @@ } }, "node_modules/@mui/material": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", - "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz", + "integrity": "sha512-Xf6Shbo03YmcBedZMwSpEFOwpYDtU7tC+rhAHTrA9FHk0FpsDqiQ9jUa1j/9s3HLs7KWb5mDcGnlwdh9Q9KAag==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/core-downloads-tracker": "^7.2.0", - "@mui/system": "^7.2.0", - "@mui/types": "^7.4.4", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.28.2", + "@mui/core-downloads-tracker": "^7.3.1", + "@mui/system": "^7.3.1", + "@mui/types": "^7.4.5", + "@mui/utils": "^7.3.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^19.1.0", + "react-is": "^19.1.1", "react-transition-group": "^4.4.5" }, "engines": { @@ -1290,7 +1385,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.2.0", + "@mui/material-pigment-css": "^7.3.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1311,13 +1406,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz", - "integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.1.tgz", + "integrity": "sha512-WU3YLkKXii/x8ZEKnrLKsPwplCVE11yZxUvlaaZSIzCcI3x2OdFC8eMlNy74hVeUsYQvzzX1Es/k4ARPlFvpPQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.1", "prop-types": "^15.8.1" }, "engines": { @@ -1338,12 +1433,12 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz", - "integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.1.tgz", + "integrity": "sha512-Nqo6OHjvJpXJ1+9TekTE//+8RybgPQUKwns2Lh0sq+8rJOUSUKS3KALv4InSOdHhIM9Mdi8/L7LTF1/Ky6D6TQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", + "@babel/runtime": "^7.28.2", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", @@ -1372,12 +1467,12 @@ } }, "node_modules/@mui/stylis-plugin-rtl": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/stylis-plugin-rtl/-/stylis-plugin-rtl-7.2.0.tgz", - "integrity": "sha512-x+9UMuAuB82dbzxxFeCoZEZaLzdBVOSlkSEhFu8iJgsEmwJXJ9l09NsiONw8sJlp7J5etpGEiafDz0XSM/C/Fw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/stylis-plugin-rtl/-/stylis-plugin-rtl-7.3.1.tgz", + "integrity": "sha512-CN3d+TOMhDGSTe5YTrHSkuSfaj2DO1Jo4C1gqBDWARuI1iZMer6HXWHP86m17OdIIUSjHb0Mma9OZncb86gMwQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", + "@babel/runtime": "^7.28.2", "cssjanus": "^2.3.0" }, "engines": { @@ -1392,16 +1487,16 @@ } }, "node_modules/@mui/system": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz", - "integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.1.tgz", + "integrity": "sha512-mIidecvcNVpNJMdPDmCeoSL5zshKBbYPcphjuh6ZMjhybhqhZ4mX6k9zmIWh6XOXcqRQMg5KrcjnO0QstrNj3w==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/private-theming": "^7.2.0", - "@mui/styled-engine": "^7.2.0", - "@mui/types": "^7.4.4", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.28.2", + "@mui/private-theming": "^7.3.1", + "@mui/styled-engine": "^7.3.1", + "@mui/types": "^7.4.5", + "@mui/utils": "^7.3.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1432,12 +1527,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz", - "integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz", + "integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.28.2" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1449,17 +1544,17 @@ } }, "node_modules/@mui/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.1.tgz", + "integrity": "sha512-/31y4wZqVWa0jzMnzo6JPjxwP6xXy4P3+iLbosFg/mJQowL1KIou0LC+lquWW60FKVbKz5ZUWBg2H3jausa0pw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/types": "^7.4.4", + "@babel/runtime": "^7.28.2", + "@mui/types": "^7.4.5", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.1.1" }, "engines": { "node": ">=14.0.0" @@ -1714,9 +1809,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", - "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", "engines": { @@ -1737,9 +1832,9 @@ } }, "node_modules/@rkheftan/harmony-ui": { - "version": "0.1.5", - "resolved": "https://npm.pkg.github.com/download/@rkheftan/harmony-ui/0.1.5/a26ede3b74431bf792e044b34d2116b5ce62c19d", - "integrity": "sha512-fyOCnHT1UEV0FNnDKN0uiKoOYJZoGwgKl2XsZcvvzEkYC1oRAgyFMRiY2qNVulSQH97FX5F2NS8zGjQR8eO8qw==", + "version": "0.1.6", + "resolved": "https://npm.pkg.github.com/download/@rkheftan/harmony-ui/0.1.6/6a5193594a84e443cec9d657a45df92bf0ba4a19", + "integrity": "sha512-1Dp2YGSZ4J02quJ5R10DBzmuL8rDQBwbr0++0AJXBs00gcBF537edy9eG5du6p9fMxWZra057dXYLauHgMFRZA==", "peerDependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -1752,16 +1847,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.19", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", - "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", - "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", "cpu": [ "arm" ], @@ -1773,9 +1868,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", - "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", "cpu": [ "arm64" ], @@ -1787,9 +1882,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", - "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", "cpu": [ "arm64" ], @@ -1801,9 +1896,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", - "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", "cpu": [ "x64" ], @@ -1815,9 +1910,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", - "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", "cpu": [ "arm64" ], @@ -1829,9 +1924,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", - "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", "cpu": [ "x64" ], @@ -1843,9 +1938,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", - "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", "cpu": [ "arm" ], @@ -1857,9 +1952,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", - "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", "cpu": [ "arm" ], @@ -1871,9 +1966,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", - "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", "cpu": [ "arm64" ], @@ -1885,9 +1980,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", - "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", "cpu": [ "arm64" ], @@ -1899,9 +1994,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", - "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", "cpu": [ "loong64" ], @@ -1912,10 +2007,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", - "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", "cpu": [ "ppc64" ], @@ -1927,9 +2022,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", - "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", "cpu": [ "riscv64" ], @@ -1941,9 +2036,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", - "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", "cpu": [ "riscv64" ], @@ -1955,9 +2050,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", - "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", "cpu": [ "s390x" ], @@ -1969,9 +2064,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", - "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", "cpu": [ "x64" ], @@ -1983,9 +2078,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", - "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", "cpu": [ "x64" ], @@ -1997,9 +2092,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", - "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", "cpu": [ "arm64" ], @@ -2011,9 +2106,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", - "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", "cpu": [ "ia32" ], @@ -2025,9 +2120,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", - "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", "cpu": [ "x64" ], @@ -2074,13 +2169,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/estree": { @@ -2105,13 +2200,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/parse-json": { @@ -2127,18 +2222,18 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", + "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2154,18 +2249,24 @@ "@types/react": "*" } }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", - "integrity": "sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/type-utils": "8.35.1", - "@typescript-eslint/utils": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2179,32 +2280,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.1", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.1.tgz", - "integrity": "sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2216,18 +2307,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.1.tgz", - "integrity": "sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.1", - "@typescript-eslint/types": "^8.35.1", + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "engines": { @@ -2238,18 +2329,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.1.tgz", - "integrity": "sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2260,9 +2351,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.1.tgz", - "integrity": "sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -2273,18 +2364,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.1.tgz", - "integrity": "sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.1", - "@typescript-eslint/utils": "8.35.1", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -2297,13 +2389,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.1.tgz", - "integrity": "sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", "engines": { @@ -2315,16 +2407,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.1.tgz", - "integrity": "sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.1", - "@typescript-eslint/tsconfig-utils": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/visitor-keys": "8.35.1", + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2340,59 +2432,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.1.tgz", - "integrity": "sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.1", - "@typescript-eslint/types": "8.35.1", - "@typescript-eslint/typescript-estree": "8.35.1" + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2403,17 +2456,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.1.tgz", - "integrity": "sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.1", + "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -2424,17 +2477,30 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", - "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", + "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.19", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2442,7 +2508,7 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { @@ -2469,16 +2535,16 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -2503,30 +2569,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2727,13 +2769,12 @@ "peer": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -2750,9 +2791,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2770,8 +2811,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2846,9 +2887,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001726", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", - "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "dev": true, "funding": [ { @@ -2986,48 +3027,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/conf/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/conf/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT", - "peer": true - }, - "node_modules/conf/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, "node_modules/cookie": { @@ -3254,9 +3257,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.178", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz", - "integrity": "sha512-wObbz/ar3Bc6e4X5vf0iO8xTN8YAjN/tgiAOJLr7yjYFtP9wAjq8Mb5h0yn6kResir+VYx2DXBj9NNobs0ETSA==", + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", "dev": true, "license": "ISC" }, @@ -3293,9 +3296,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3306,31 +3309,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -3356,20 +3360,20 @@ } }, "node_modules/eslint": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", - "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.14.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.30.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3417,9 +3421,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.5", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", - "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -3433,9 +3437,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", - "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3504,6 +3508,47 @@ } }, "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", @@ -3516,6 +3561,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3534,6 +3609,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -3869,10 +3957,34 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -3943,9 +4055,9 @@ } }, "node_modules/i18next": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.0.tgz", - "integrity": "sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==", + "version": "25.3.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz", + "integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==", "funding": [ { "type": "individual", @@ -4025,9 +4137,9 @@ "peer": true }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -4215,11 +4327,11 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true }, "node_modules/json-schema-typed": { "version": "8.0.1", @@ -4562,15 +4674,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -4966,30 +5082,30 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-i18next": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.0.tgz", - "integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz", + "integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.27.6", @@ -5013,9 +5129,9 @@ } }, "node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", "license": "MIT" }, "node_modules/react-refresh": { @@ -5107,16 +5223,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/readdir-glob/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -5202,9 +5308,9 @@ } }, "node_modules/rollup": { - "version": "4.44.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", - "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", "dependencies": { @@ -5218,26 +5324,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.1", - "@rollup/rollup-android-arm64": "4.44.1", - "@rollup/rollup-darwin-arm64": "4.44.1", - "@rollup/rollup-darwin-x64": "4.44.1", - "@rollup/rollup-freebsd-arm64": "4.44.1", - "@rollup/rollup-freebsd-x64": "4.44.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", - "@rollup/rollup-linux-arm-musleabihf": "4.44.1", - "@rollup/rollup-linux-arm64-gnu": "4.44.1", - "@rollup/rollup-linux-arm64-musl": "4.44.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-gnu": "4.44.1", - "@rollup/rollup-linux-riscv64-musl": "4.44.1", - "@rollup/rollup-linux-s390x-gnu": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1", - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-win32-arm64-msvc": "4.44.1", - "@rollup/rollup-win32-ia32-msvc": "4.44.1", - "@rollup/rollup-win32-x64-msvc": "4.44.1", + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" } }, @@ -5306,13 +5412,15 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/set-cookie-parser": { @@ -5443,13 +5551,13 @@ } }, "node_modules/synckit": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", - "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.4" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -5508,9 +5616,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5620,15 +5728,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.35.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.1.tgz", - "integrity": "sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.35.1", - "@typescript-eslint/parser": "8.35.1", - "@typescript-eslint/utils": "8.35.1" + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5639,13 +5748,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -5770,17 +5879,17 @@ } }, "node_modules/vite": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", - "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz", + "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", + "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "bin": { @@ -5860,9 +5969,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5952,9 +6061,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", "optional": true, diff --git a/package.json b/package.json index da6fe2c..14e1fe4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@emotion/styled": "^11.14.1", "@mui/material": "^7.2.0", "@mui/stylis-plugin-rtl": "^7.2.0", - "@rkheftan/harmony-ui": "^0.1.5", + "@rkheftan/harmony-ui": "^0.1.6", + "@types/stylis": "^4.2.7", "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index 71d6ae5..c92ae3a 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -2,6 +2,13 @@ "side": { "account": "حساب کاربری", "personalInfo": "اطلاعات شخصی", - "contactInfo": "شماره تماس" + "contactInfo": "شماره تماس", + "email": "ایمیل", + "security": "امنیت", + "password": "رمز عبور", + "confirmedIps": "آدرس های تایید شده", + "recentSessions": "ورود های اخیر", + "activeSessions": "نشست های فعال", + "setting": "تنظیمات" } } diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx new file mode 100644 index 0000000..39f4eb9 --- /dev/null +++ b/src/components/Layout/Header.tsx @@ -0,0 +1,35 @@ +import { Box, IconButton, Typography } from '@mui/material'; +import { Icon } from '@rkheftan/harmony-ui'; +import { More } from 'iconsax-react'; +import type { User } from './type'; + +interface HeaderProps { + user: User; +} + +export const Header: React.FC = ({ user }) => { + return ( + t.spacing(10.5), + }} + > + + + {user.firstName + ' ' + user.lastName} + + + {user.phoneNumber} + + + + + + + + ); +}; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 504dd3e..2652dfe 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,13 +1,24 @@ import { SideNav } from '@rkheftan/harmony-ui'; -import { buildNavItems } from './navItems'; +import { buildNavItems } from './buildNavItems'; import { appRoutes } from '@/routes/config'; import { Outlet, useLocation } from 'react-router-dom'; -import { Box } from '@mui/material'; -import { grey } from '@mui/material/colors'; +import { Box, useMediaQuery, useTheme } from '@mui/material'; +import { Header } from './Header'; +import { useState } from 'react'; +import { Toolbar } from './Toolbar'; +import type { User } from './type'; export const Layout = () => { const navItemConfigs = buildNavItems(appRoutes); const location = useLocation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const [sideNavOpen, setSideNavOpen] = useState(false); + const [user] = useState({ + firstName: 'محمد حسین', + lastName: 'برزه گر', + phoneNumber: '09123456789', + }); return ( { > + setSideNavOpen(false)} + header={isMobile ? undefined :
} + footer={isMobile ?
: undefined} navConfig={navItemConfigs} activePath={location.pathname + location.hash} selectedVariant="textOnly" positioning="absolute" + sideNavVariant={isMobile ? 'temporary' : 'full'} + top={8.125} /> diff --git a/src/components/Layout/Toolbar.tsx b/src/components/Layout/Toolbar.tsx new file mode 100644 index 0000000..46b482e --- /dev/null +++ b/src/components/Layout/Toolbar.tsx @@ -0,0 +1,75 @@ +import { + Avatar, + Box, + IconButton, + Toolbar as MuiToolbar, + Typography, +} from '@mui/material'; +import { Icon } from '@rkheftan/harmony-ui'; +import { HambergerMenu, Menu } from 'iconsax-react'; +import type { Dispatch, SetStateAction } from 'react'; +import type { User } from './type'; + +interface ToolbarProps { + sideNavOpen: boolean; + setSideNavOpen: Dispatch>; + isMobile: boolean; + user: User; +} + +export const Toolbar: React.FC = ({ + sideNavOpen, + setSideNavOpen, + isMobile, + user, +}) => { + return ( + t.spacing(isMobile ? 8 : 10.5), + px: isMobile ? 3 : 2, + borderBottom: (t) => `1px solid ${t.palette.divider}`, + boxSizing: 'content-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }} + > + + {isMobile && ( + setSideNavOpen(!sideNavOpen)} + > + + + )} + {/* */} + LOGO placeholder + + + {isMobile && ( + + {user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)} + + )} + + + + + + ); +}; diff --git a/src/components/Layout/buildNavItems.tsx b/src/components/Layout/buildNavItems.tsx new file mode 100644 index 0000000..4cfaba0 --- /dev/null +++ b/src/components/Layout/buildNavItems.tsx @@ -0,0 +1,34 @@ +// src/components/SideNav.tsx (Conceptual Example) + +import { useTranslation } from 'react-i18next'; +import { type RouteConfig } from '@/routes/config'; +import { Icon, type NavItemConfig } from '@rkheftan/harmony-ui'; +import type { Icon as Iconsax } from 'iconsax-react'; + +const getIcon = (icon?: Iconsax) => (isSelected: boolean) => + icon ? ( + + ) : undefined; + +export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] { + const { t } = useTranslation(); + + return routes.flatMap((route) => { + // Check if route itself does not have a navItem but its child has + if (!route.navConfig && route.children) { + return buildNavItems(route.children); + } + + // Check if route.navConfig is defined before destructuring + if (!route.navConfig) { + return []; // Return an empty array to be flattened + } + const { title, icon } = route.navConfig; + return { + text: t(title), + getIcon: getIcon(icon), + path: route.path, + children: route.children ? buildNavItems(route.children) : undefined, + }; + }); +} diff --git a/src/components/Layout/navItems.tsx b/src/components/Layout/navItems.tsx deleted file mode 100644 index 7160ea9..0000000 --- a/src/components/Layout/navItems.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// src/components/SideNav.tsx (Conceptual Example) - -import { useTranslation } from 'react-i18next'; -import { type RouteConfig } from '@/routes/config'; -import { Icon, type NavItemConfig } from '@rkheftan/harmony-ui'; -import type { Icon as Iconsax } from 'iconsax-react'; - -const getIcon = (icon?: Iconsax) => (isSelected: boolean) => - icon ? ( - - ) : undefined; - -export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] { - const { t } = useTranslation(); - - return routes - .filter((route) => route.navConfig) - .map((route) => { - const { title, icon } = route.navConfig!; - return { - text: t(title), - getIcon: getIcon(icon), - path: route.path, - children: route.children ? buildNavItems(route.children) : undefined, - }; - }); -} diff --git a/src/components/Layout/type.ts b/src/components/Layout/type.ts new file mode 100644 index 0000000..ab31407 --- /dev/null +++ b/src/components/Layout/type.ts @@ -0,0 +1,8 @@ +// TODO: this type file is temporary and should replace it with the actual use type and value when api is ready + +export interface User { + firstName: string; + lastName: string; + phoneNumber: string; + profileUrl?: string; +} diff --git a/src/providers/RtlProvider.tsx b/src/providers/RtlProvider.tsx index 7bbf52a..ce4201c 100644 --- a/src/providers/RtlProvider.tsx +++ b/src/providers/RtlProvider.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { CacheProvider } from '@emotion/react'; import createCache from '@emotion/cache'; import rtlPlugin from 'stylis-plugin-rtl'; +import { prefixer } from 'stylis'; // This provider configures Emotion's cache to support RTL. export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({ @@ -15,8 +16,8 @@ export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({ const newDir = i18n.dir(i18n.language); const newCache = createCache({ - key: 'css', - stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [], + key: 'mui', + stylisPlugins: newDir === 'rtl' ? [prefixer, rtlPlugin] : [], }); setCache(newCache); }, [i18n, i18n.language]); diff --git a/src/routes/config.tsx b/src/routes/config.tsx index b5ed42c..61f2f8d 100644 --- a/src/routes/config.tsx +++ b/src/routes/config.tsx @@ -1,5 +1,17 @@ import { Layout } from '@/components/Layout/Layout'; -import { Mobile, Personalcard, ProfileCircle, type Icon } from 'iconsax-react'; +import { + Calendar, + Devices, + LocationTick, + Mobile, + PasswordCheck, + Personalcard, + ProfileCircle, + Setting, + Shield, + Sms, + type Icon, +} from 'iconsax-react'; import { type ReactNode } from 'react'; import { Navigate } from 'react-router-dom'; @@ -13,34 +25,93 @@ export interface RouteConfig { children?: RouteConfig[]; } +// can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));) export const appRoutes: RouteConfig[] = [ { path: '/', - element: , + element: , }, { - path: '/profile', - // can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));) + path: '/setting', element: , - navConfig: { - title: 'side.account', - icon: ProfileCircle, - }, children: [ + // TODO: add route component to each route { - path: '/profile/info', - element:
Personal Info Section
, + path: '/setting/profile', navConfig: { - title: 'side.personalInfo', - icon: Personalcard, + // Profile component + title: 'side.account', + icon: ProfileCircle, + }, + children: [ + { + path: '/setting/profile#info', + navConfig: { + title: 'side.personalInfo', + icon: Personalcard, + }, + }, + { + path: '/setting/profile#contact-info', + navConfig: { + title: 'side.contactInfo', + icon: Mobile, + }, + }, + { + path: '/setting/profile#email', + navConfig: { + title: 'side.email', + icon: Sms, + }, + }, + ], + }, + { + path: '/setting/security', + // security component + navConfig: { + title: 'side.security', + icon: Shield, + }, + children: [ + { + path: '/setting/security#password', + navConfig: { + title: 'side.password', + icon: PasswordCheck, + }, + }, + { + path: '/setting/security#confirmed-ips', + navConfig: { + title: 'side.confirmedIps', + icon: LocationTick, + }, + }, + { + path: '/setting/security#recent-sessions', + navConfig: { + title: 'side.recentSessions', + icon: Devices, + }, + }, + ], + }, + { + path: '/setting/active-sessions', + // active session component + navConfig: { + title: 'side.activeSessions', + icon: Calendar, }, }, { - path: '/profile/contact-info', - element:
Personal Info Section
, + path: '/setting/preferences', + // setting component navConfig: { - title: 'side.contactInfo', - icon: Mobile, + title: 'side.setting', + icon: Setting, }, }, ], From e4755ded7396a230dbe23732038d71d384094223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Tue, 12 Aug 2025 11:52:09 +0330 Subject: [PATCH 27/32] chore: add phone number logic changed --- .../AuthenticationSteps.tsx | 14 ++++++- .../AuthenticationSteps/OtpVerifyForm.tsx | 42 +++++-------------- .../AuthenticationSteps/VerifyPhoneNumber.tsx | 13 +++--- 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index 8f08908..481f5a8 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -69,6 +69,16 @@ export const AuthenticationSteps = (): JSX.Element => { location.href = authReturnUrl; }; + const handleConfrimPhoneNumber = (userId: GUID) => { + handleUserLoggedIn(userId); + + setCurrentStep('addPhoneNumber'); + }; + + const handlePhoneNumberVerified = () => { + location.href = authReturnUrl; + }; + return ( <> {currentStep === 'emailOrPhone' && ( @@ -87,7 +97,7 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'verify' && ( setCurrentStep('addPhoneNumber')} + onVerifyPhoneNumber={handleConfrimPhoneNumber} authReturnUrl={authReturnUrl} countryCode={countryCode} onOTPVerified={handleOTPVerfied} @@ -129,7 +139,7 @@ export const AuthenticationSteps = (): JSX.Element => { onEditValue={() => setCurrentStep('addPhoneNumber')} value={addedPhoneNumberValue} email={loginRegisterValue} - onPhoneNumberVerified={handleUserLoggedIn} + onPhoneNumberVerified={handlePhoneNumberVerified} /> )} diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index c1f992a..bc5a4c0 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -25,8 +25,8 @@ interface OtpVerifyFormProps { authType: AuthType; authMode: AuthMode; onEditValue: () => void; - onOTPVerified: (userID: GUID) => void; - onVerifyPhoneNumber: () => void; + onOTPVerified: (userId: GUID) => void; + onVerifyPhoneNumber: (userId: GUID) => void; authReturnUrl: string; } @@ -95,38 +95,11 @@ export function OtpVerifyForm({ if (!otpCode || otpCode.length < 4) { setOtpDigitInvalid(true); } else { - if (authMode === 'register' && authType === 'email') { - handleConfirmEmailAndAddPhone(); - } else { - handleLoginRequestWithOtp(); - } + handleLoginOrSignUp(); } }; - const handleConfirmEmailAndAddPhone = async () => { - setOtpDigitInvalid(false); - setVerifyStatusLoading(true); - - const confirmOtpRequest: ConfirmEmailOtpRequest = { - otpCode: otpCode, - email: value, - }; - const result = await confirmEmailOtp(confirmOtpRequest); - const jsonRes = await result.json(); - - if (jsonRes.success) { - setVerifyStatus('success'); - onVerifyPhoneNumber(); - } else { - setVerifyStatus('failed'); - setErrorMessage(jsonRes.message); - } - - setVerifyAlertOpen(true); - setVerifyStatusLoading(false); - }; - - const handleLoginRequestWithOtp = async () => { + const handleLoginOrSignUp = async () => { setOtpDigitInvalid(false); setVerifyStatusLoading(true); @@ -141,7 +114,12 @@ export function OtpVerifyForm({ if (jsonRes.success) { setVerifyStatus('success'); - onOTPVerified(jsonRes.userId); + + if (jsonRes.registeredWithOutPhoneNumber) { + onVerifyPhoneNumber(jsonRes.userId); + } else { + onOTPVerified(jsonRes.userId); + } } else { setVerifyStatus('failed'); setErrorMessage(jsonRes.message); diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx index a7638a6..d35baba 100644 --- a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx +++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx @@ -6,9 +6,10 @@ import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; -import type { LoginRequest } from '../../types/userTypes'; +import type { ConfirmSmsOtpRequest, LoginRequest } from '../../types/userTypes'; import { useSearchParams } from 'react-router'; import { + confirmSmsOtp, loginOrSignUpWithOtp, sendEmailOtp, sendSmsOtp, @@ -21,7 +22,7 @@ interface VerifyPhoneNumberProps { email: string; countryCode: CountryCode; onEditValue: () => void; - onPhoneNumberVerified: (userId: GUID) => void; + onPhoneNumberVerified: () => void; } export function VerifyPhoneNumber({ @@ -86,18 +87,16 @@ export function VerifyPhoneNumber({ setOtpDigitInvalid(false); setVerifyStatusLoading(true); - const loginRequest: LoginRequest = { + const confirmSmsOtpRequest: ConfirmSmsOtpRequest = { otpCode: otpCode, phoneNumber: countryCode + value, - email: email, - returnUrl: authReturnUrl, }; - const result = await loginOrSignUpWithOtp(loginRequest); + const result = await confirmSmsOtp(confirmSmsOtpRequest); const jsonRes = await result.json(); if (jsonRes.success) { setVerifyStatus('success'); - onPhoneNumberVerified(jsonRes.userId); + onPhoneNumberVerified(); } else { setVerifyStatus('failed'); setErrorMessage(jsonRes.message); From 0a65e5f25c1b56aa5b88316d0ada30d8e9e7709f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Tue, 12 Aug 2025 15:25:55 +0330 Subject: [PATCH 28/32] chore: api url changed to accounts --- src/features/authorization/api/authorizationAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts index 25175e7..51b1dfe 100644 --- a/src/features/authorization/api/authorizationAPI.ts +++ b/src/features/authorization/api/authorizationAPI.ts @@ -20,7 +20,7 @@ import type { SendSmsOtpRequest, } from '../types/userTypes'; -const API_URL = 'https://account.business-harmony.com/api'; +const API_URL = 'https://accounts.business-harmony.com/api'; export const fetchRequest = ( url: string, From 20da3f980e2f6b580475e95f334f77c4d68c6d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Wed, 13 Aug 2025 16:32:29 +0330 Subject: [PATCH 29/32] removing unused imports, creating a file for global decelaration --- package-lock.json | 8 ++++++++ package.json | 1 + src/components/Toast.tsx | 2 +- .../components/AuthenticationCard.tsx | 4 ++-- .../AuthenticationSteps.tsx | 9 +-------- .../AuthenticationSteps/CompleteSignUp.tsx | 4 ++-- .../AuthenticationSteps/EnterPasswordForm.tsx | 8 ++++---- .../GoogleAuthentication.tsx | 9 +-------- .../AuthenticationSteps/LoginRegiserForm.tsx | 15 +++------------ .../AuthenticationSteps/OtpVerifyForm.tsx | 9 ++------- .../AuthenticationSteps/VerifyPhoneNumber.tsx | 19 ++++--------------- .../components/CountryCodeSelector.tsx | 1 - .../ForgetPassword/ChangePassword.tsx | 11 ++--------- .../ForgetPasswordContainer.tsx | 4 ++-- .../ForgetPassword/ForgetPasswordOtp.tsx | 6 ++++-- .../ForgetPassword/ForgettedPasswordInfo.tsx | 15 +++------------ .../routes/AuthenticationPage.tsx | 3 --- .../routes/ForgetPasswordPage.tsx | 3 --- src/global.d.ts | 12 ++++++++++++ vite.config.d.ts | 2 ++ vite.config.js | 12 ++++++++++++ 21 files changed, 66 insertions(+), 91 deletions(-) create mode 100644 src/global.d.ts create mode 100644 vite.config.d.ts create mode 100644 vite.config.js diff --git a/package-lock.json b/package-lock.json index d528328..eb009a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/stylis": "^4.2.7", "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "@vitejs/plugin-react": "^4.5.2", @@ -1882,6 +1883,13 @@ "@types/react": "*" } }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.35.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", diff --git a/package.json b/package.json index a876532..eb041d6 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/stylis": "^4.2.7", "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "@vitejs/plugin-react": "^4.5.2", diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 78b89a1..d9376be 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,5 +1,5 @@ import { Alert, Snackbar, type AlertColor } from '@mui/material'; -import React, { type PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; export interface ToastProps extends PropsWithChildren { color: AlertColor | undefined; diff --git a/src/features/authorization/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx index a4f5e0d..6c49575 100644 --- a/src/features/authorization/components/AuthenticationCard.tsx +++ b/src/features/authorization/components/AuthenticationCard.tsx @@ -1,5 +1,5 @@ -import { Box, Paper } from '@mui/material'; -import React, { type PropsWithChildren } from 'react'; +import { Paper } from '@mui/material'; +import { type PropsWithChildren } from 'react'; // Beacuse in the otp verify there is a element outside of the authentication card export const AuthenticationCard = ({ children }: PropsWithChildren) => { diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index 481f5a8..edba9e0 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -1,15 +1,10 @@ -import React, { useState, type JSX } from 'react'; +import { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; import type { AuthMode, AuthType } from '../../types/authTypes'; import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; -import { - getUserStatusByPhoneNumberOrEmail, - sendEmailOtp, - sendSmsOtp, -} from '../../api/authorizationAPI'; import { UserStatus } from '../../types/userTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; @@ -134,11 +129,9 @@ export const AuthenticationSteps = (): JSX.Element => { {currentStep === 'addedPhoneNumberVerify' && ( setCurrentStep('addPhoneNumber')} value={addedPhoneNumberValue} - email={loginRegisterValue} onPhoneNumberVerified={handlePhoneNumberVerified} /> )} diff --git a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx index 063892c..0394f8c 100644 --- a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx +++ b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx @@ -1,6 +1,6 @@ -import { Box, Button, Paper, TextField, Typography } from '@mui/material'; +import { Button, TextField, Typography } from '@mui/material'; import parsePhoneNumberFromString from 'libphonenumber-js'; -import React, { useRef, useState, type Dispatch } from 'react'; +import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; import { AuthenticationCard } from '../AuthenticationCard'; import { CountryCodeSelector } from '../CountryCodeSelector'; diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index 56dadd4..8fa1206 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -1,6 +1,6 @@ -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { AuthenticationCard } from '../AuthenticationCard'; -import { ArrowLeft, Edit2, Eye, EyeSlash, MaskLeft } from 'iconsax-reactjs'; +import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-reactjs'; import { Box, Button, @@ -11,7 +11,7 @@ import { } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { Toast } from '@/components/Toast'; -import { Link, Navigate, useSearchParams } from 'react-router'; +import { Link } from 'react-router'; import type { AuthType } from '../../types/authTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { @@ -19,7 +19,7 @@ import { sendEmailOtp, sendSmsOtp, } from '../../api/authorizationAPI'; -import type { LoginRequest, PasswordLoginRequest } from '../../types/userTypes'; +import type { PasswordLoginRequest } from '../../types/userTypes'; export interface EnterPasswordFormProps { onEditValue: () => void; diff --git a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx index c5c49cf..90ad828 100644 --- a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx +++ b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx @@ -1,18 +1,11 @@ import { Button } from '@mui/material'; import { Google } from 'iconsax-reactjs'; -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { GoogleCodeClientResponse } from '../../types/userTypes'; import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI'; import type { GUID } from '@/types/commonTypes'; -declare global { - interface Window { - google: typeof google; - } - const google: any; -} - export interface GoogleAuthenticationProps { disabled: boolean; authReturnUrl: string; diff --git a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx index 1f7aa5a..0f7f048 100644 --- a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx @@ -1,16 +1,8 @@ -import { - Box, - Button, - Paper, - Stack, - TextField, - Typography, -} from '@mui/material'; +import { Button, Stack, TextField, Typography } from '@mui/material'; import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; -import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; -import type { AuthMode, AuthType } from '../../types/authTypes'; +import type { AuthType } from '../../types/authTypes'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; @@ -45,10 +37,9 @@ export function LoginRegisterForm({ onGoogleAuthenticated, }: LoginRegisterFormProps) { const [checkStatusLoading, setCheckStatusLoading] = useState(false); - const { t, i18n } = useTranslation('authentication'); + const { t } = useTranslation('authentication'); const textFieldRef = useRef(null); const inputRef = useRef(null); - const dir = i18n.dir(); const [error, setError] = useState(); const [touched, setTouched] = useState(false); const [errorMessage, setErrorMessage] = useState(); diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx index bc5a4c0..d5eaec5 100644 --- a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx @@ -1,18 +1,13 @@ import { useTranslation } from 'react-i18next'; -import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; -import type { - ConfirmEmailOtpRequest, - LoginRequest, -} from '../../types/userTypes'; -import { useSearchParams } from 'react-router'; +import type { LoginRequest } from '../../types/userTypes'; import { - confirmEmailOtp, loginOrSignUpWithOtp, sendEmailOtp, sendSmsOtp, diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx index d35baba..10c6abd 100644 --- a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx +++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx @@ -1,33 +1,22 @@ import { useTranslation } from 'react-i18next'; -import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; -import type { AuthMode, AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; -import type { ConfirmSmsOtpRequest, LoginRequest } from '../../types/userTypes'; -import { useSearchParams } from 'react-router'; -import { - confirmSmsOtp, - loginOrSignUpWithOtp, - sendEmailOtp, - sendSmsOtp, -} from '../../api/authorizationAPI'; -import type { CountryCode, GUID } from '@/types/commonTypes'; +import type { ConfirmSmsOtpRequest } from '../../types/userTypes'; +import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI'; +import type { CountryCode } from '@/types/commonTypes'; interface VerifyPhoneNumberProps { - authReturnUrl: string; value: string; - email: string; countryCode: CountryCode; onEditValue: () => void; onPhoneNumberVerified: () => void; } export function VerifyPhoneNumber({ - authReturnUrl, - email, value, countryCode, onEditValue, diff --git a/src/features/authorization/components/CountryCodeSelector.tsx b/src/features/authorization/components/CountryCodeSelector.tsx index 04ad265..849e986 100644 --- a/src/features/authorization/components/CountryCodeSelector.tsx +++ b/src/features/authorization/components/CountryCodeSelector.tsx @@ -13,7 +13,6 @@ import { useMemo, useRef, useState, type RefObject } from 'react'; import { ArrowDown2 } from 'iconsax-reactjs'; import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; -import { Virtuoso } from 'react-virtuoso'; import { countries, type Country } from '../data/countries'; import type { CountryCode } from '@/types/commonTypes'; interface CountryCodeSelectorProps { diff --git a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx index b4b35dd..6928596 100644 --- a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx +++ b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx @@ -1,13 +1,6 @@ -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { AuthenticationCard } from '../AuthenticationCard'; -import { - ArrowLeft, - Edit2, - Eye, - EyeSlash, - MaskLeft, - TickCircle, -} from 'iconsax-reactjs'; +import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-reactjs'; import { Box, Button, diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx index a980bed..137b413 100644 --- a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import type { AuthType } from '../../types/authTypes'; import { ForgettedPasswordInfo } from './ForgettedPasswordInfo'; import { ForgetPasswordOtp } from './ForgetPasswordOtp'; @@ -14,7 +14,7 @@ export const ForgetPasswordContainer = () => { const [infoCountryCode, setInfoCountryCode] = useState('+98'); const [infoType, setInfoType] = useState('email'); - const handleVerifyOtp = (value: string) => { + const handleVerifyOtp = () => { setForgetPassCurrentStep('verifyOtp'); }; diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx index 5f50a7f..cede6af 100644 --- a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx @@ -1,8 +1,8 @@ import { useTranslation } from 'react-i18next'; -import { Alert, Box, Button, Snackbar, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Typography } from '@mui/material'; import { Edit2 } from 'iconsax-reactjs'; import DigitInput from '@/components/components/DigitsInput'; -import type { AuthMode, AuthType } from '../../types/authTypes'; +import type { AuthType } from '../../types/authTypes'; import { useEffect, useState } from 'react'; import { Toast } from '@/components/Toast'; import { AuthenticationCard } from '../AuthenticationCard'; @@ -96,8 +96,10 @@ export function ForgetPasswordOtp({ const jsonRes = await result.json(); if (jsonRes.success) { + setVerifyStatus('success'); onOTPVerified(otpCode); } else { + setVerifyStatus('failed'); setVerifyAlertMessage(jsonRes.message); } diff --git a/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx index cc9abf3..2ccf34e 100644 --- a/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx +++ b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx @@ -1,16 +1,8 @@ -import { - Box, - Button, - Paper, - Stack, - TextField, - Typography, -} from '@mui/material'; +import { Button, Stack, TextField, Typography } from '@mui/material'; import { useRef, useState, type Dispatch } from 'react'; import { useTranslation } from 'react-i18next'; -import { Google } from 'iconsax-reactjs'; import { isNumeric } from '@/utils/regexes/isNumeric'; -import type { AuthMode, AuthType } from '../../types/authTypes'; +import type { AuthType } from '../../types/authTypes'; import { isEmail } from '@/utils/regexes/isEmail'; import parsePhoneNumberFromString from 'libphonenumber-js'; import { AuthenticationCard } from '../AuthenticationCard'; @@ -39,10 +31,9 @@ export function ForgettedPasswordInfo({ countryCode, setCountryCode, }: ForgettedPasswordInfoProps) { - const { t, i18n } = useTranslation('authentication'); + const { t } = useTranslation('authentication'); const textFieldRef = useRef(null); const inputRef = useRef(null); - const dir = i18n.dir(); const [error, setError] = useState(); const [touched, setTouched] = useState(false); const [errorMessage, setErrorMessage] = useState(); diff --git a/src/features/authorization/routes/AuthenticationPage.tsx b/src/features/authorization/routes/AuthenticationPage.tsx index d34cc01..47917cf 100644 --- a/src/features/authorization/routes/AuthenticationPage.tsx +++ b/src/features/authorization/routes/AuthenticationPage.tsx @@ -1,9 +1,6 @@ import { FlexBox } from '@/components/components/common/FlexBox'; import Logo from '@/components/Logo'; -import { Paper } from '@mui/material'; -import { useState } from 'react'; import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps'; -import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer'; export function AuthenticationPage() { return ( diff --git a/src/features/authorization/routes/ForgetPasswordPage.tsx b/src/features/authorization/routes/ForgetPasswordPage.tsx index d30290e..f54d961 100644 --- a/src/features/authorization/routes/ForgetPasswordPage.tsx +++ b/src/features/authorization/routes/ForgetPasswordPage.tsx @@ -1,8 +1,5 @@ import { FlexBox } from '@/components/components/common/FlexBox'; import Logo from '@/components/Logo'; -import { Paper } from '@mui/material'; -import { useState } from 'react'; -import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps'; import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer'; export function ForgetPasswordPage() { diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..3a11320 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,12 @@ +import React from 'react'; + +declare global { + namespace JSX { + interface Element extends React.ReactElement {} + } + + interface Window { + google: typeof google; + } + const google: any; +} diff --git a/vite.config.d.ts b/vite.config.d.ts new file mode 100644 index 0000000..340562a --- /dev/null +++ b/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..70ef262 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); From c15e47b8b022c13a65ebff639f70b3067ce8c8dc Mon Sep 17 00:00:00 2001 From: mehrzadghdev Date: Thu, 14 Aug 2025 00:10:29 +0330 Subject: [PATCH 30/32] chore: all review comments --- .env | 1 + eslint.config.js | 8 +- package-lock.json | 39 +-- package.json | 4 +- public/locales/en/country.json | 247 ------------------ public/locales/fa/country.json | 182 ------------- src/App.tsx | 2 +- src/components/components/DigitsInput.tsx | 11 +- .../authorization/data => }/countries.ts | 0 .../AuthenticationSteps.tsx | 35 ++- .../AuthenticationSteps/CompleteSignUp.tsx | 14 +- .../AuthenticationSteps/EnterPasswordForm.tsx | 16 +- .../GoogleAuthentication.tsx | 10 +- .../AuthenticationSteps/LoginRegiserForm.tsx | 34 +-- .../AuthenticationSteps/OtpVerifyForm.tsx | 31 +-- .../AuthenticationSteps/VerifyPhoneNumber.tsx | 10 +- .../components/CountryCodeSelector.tsx | 4 +- .../ForgetPassword/ChangePassword.tsx | 2 +- .../ForgetPassword/ForgetPasswordOtp.tsx | 10 +- .../ForgetPassword/ForgettedPasswordInfo.tsx | 46 ++-- src/features/authorization/types/authTypes.ts | 7 + src/utils/regexes/isValidPhoneNumber.tsx | 7 + 22 files changed, 137 insertions(+), 583 deletions(-) create mode 100644 .env delete mode 100644 public/locales/en/country.json delete mode 100644 public/locales/fa/country.json rename src/{features/authorization/data => }/countries.ts (100%) create mode 100644 src/utils/regexes/isValidPhoneNumber.tsx diff --git a/.env b/.env new file mode 100644 index 0000000..2731127 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/ \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 45e6782..75e9038 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,12 +34,8 @@ export default tseslint.config( ...tseslint.configs.recommended.rules, ...reactHooks.configs.recommended.rules, ...prettierConfig.rules, - 'prettier/prettier': [ - 'error', - { - "endOfLine": "auto" - } - ], + 'prettier/prettier': 'error', + 'linebreak-style': ['error', 'unix'], 'react-refresh/only-export-components': 'warn', '@typescript-eslint/no-explicit-any': 'warn', }, diff --git a/package-lock.json b/package-lock.json index eb009a8..e6ae04e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,14 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "iconsax-reactjs": "^0.0.8", + "iconsax-react": "^0.0.8", "libphonenumber-js": "^1.12.10", "react": "^19.1.0", "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router": "^7.8.0", - "react-virtuoso": "^4.13.0", + "react-router-dom": "^7.8.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, @@ -3124,11 +3124,14 @@ "cross-fetch": "4.0.0" } }, - "node_modules/iconsax-reactjs": { + "node_modules/iconsax-react": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/iconsax-reactjs/-/iconsax-reactjs-0.0.8.tgz", - "integrity": "sha512-cb+uTMxbkSFNbu8ZclX7BWQVfOWQt8+m/PsDjnsm/H+mcYrnfTYMjHxiof1FB43k7UAgt1ds+0oFeMVKdqyslw==", + "resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz", + "integrity": "sha512-l3dVk4zGtkkJHgvNYqAf0wDKqnKxXykee5/DoESGo2JvSYwaxajJUHSX2YrPRXSov8Hd8ClGFwJxCEaEjrFD1Q==", "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, "peerDependencies": { "react": "*" } @@ -3834,6 +3837,22 @@ } } }, + "node_modules/react-router-dom": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz", + "integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3850,16 +3869,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-virtuoso": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.13.0.tgz", - "integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==", - "license": "MIT", - "peerDependencies": { - "react": ">=16 || >=17 || >= 18 || >= 19", - "react-dom": ">=16 || >=17 || >= 18 || >=19" - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index eb041d6..d35683f 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,14 @@ "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", - "iconsax-reactjs": "^0.0.8", + "iconsax-react": "^0.0.8", "libphonenumber-js": "^1.12.10", "react": "^19.1.0", "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", "react-router": "^7.8.0", - "react-virtuoso": "^4.13.0", + "react-router-dom": "^7.8.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, diff --git a/public/locales/en/country.json b/public/locales/en/country.json deleted file mode 100644 index 73a26ff..0000000 --- a/public/locales/en/country.json +++ /dev/null @@ -1,247 +0,0 @@ -{ - "country.afghanistan": "Afghanistan", - "country.aland_islands": "Aland islands", - "country.albania": "Albania", - "country.algeria": "Algeria", - "country.american_samoa": "American samoa", - "country.andorra": "Andorra", - "country.angola": "Angola", - "country.anguilla": "Anguilla", - "country.antarctica": "Antarctica", - "country.antigua_and_barbuda": "Antigua and barbuda", - "country.argentina": "Argentina", - "country.armenia": "Armenia", - "country.aruba": "Aruba", - "country.australia": "Australia", - "country.austria": "Austria", - "country.azerbaijan": "Azerbaijan", - "country.bahamas": "Bahamas", - "country.bahrain": "Bahrain", - "country.bangladesh": "Bangladesh", - "country.barbados": "Barbados", - "country.belarus": "Belarus", - "country.belgium": "Belgium", - "country.belize": "Belize", - "country.benin": "Benin", - "country.bermuda": "Bermuda", - "country.bhutan": "Bhutan", - "country.bolivia": "Bolivia", - "country.bosnia_and_herzegovina": "Bosnia and herzegovina", - "country.botswana": "Botswana", - "country.brazil": "Brazil", - "country.british_indian_ocean_territory": "British indian ocean territory", - "country.british_virgin_islands": "British virgin islands", - "country.brunei": "Brunei", - "country.bulgaria": "Bulgaria", - "country.burkina_faso": "Burkina faso", - "country.burundi": "Burundi", - "country.cambodia": "Cambodia", - "country.cameroon": "Cameroon", - "country.canada": "Canada", - "country.cape_verde": "Cape verde", - "country.cayman_islands": "Cayman islands", - "country.central_african_republic": "Central african republic", - "country.chad": "Chad", - "country.chile": "Chile", - "country.china": "China", - "country.christmas_island": "Christmas island", - "country.cocos_keeling_islands": "Cocos keeling islands", - "country.colombia": "Colombia", - "country.comoros": "Comoros", - "country.congo_brazzaville": "Congo brazzaville", - "country.congo_kinshasa": "Congo kinshasa", - "country.cook_islands": "Cook islands", - "country.costa_rica": "Costa rica", - "country.cote_divoire": "Cote divoire", - "country.croatia": "Croatia", - "country.cuba": "Cuba", - "country.curacao": "Curacao", - "country.cyprus": "Cyprus", - "country.czech_republic": "Czech republic", - "country.denmark": "Denmark", - "country.djibouti": "Djibouti", - "country.dominica": "Dominica", - "country.dominican_republic": "Dominican republic", - "country.ecuador": "Ecuador", - "country.egypt": "Egypt", - "country.el_salvador": "El salvador", - "country.equatorial_guinea": "Equatorial guinea", - "country.eritrea": "Eritrea", - "country.estonia": "Estonia", - "country.eswatini": "Eswatini", - "country.ethiopia": "Ethiopia", - "country.falkland_islands": "Falkland islands", - "country.faroe_islands": "Faroe islands", - "country.fiji": "Fiji", - "country.finland": "Finland", - "country.france": "France", - "country.french_guiana": "French guiana", - "country.french_polynesia": "French polynesia", - "country.gabon": "Gabon", - "country.gambia": "Gambia", - "country.georgia": "Georgia", - "country.germany": "Germany", - "country.ghana": "Ghana", - "country.gibraltar": "Gibraltar", - "country.greece": "Greece", - "country.greenland": "Greenland", - "country.grenada": "Grenada", - "country.guadeloupe": "Guadeloupe", - "country.guam": "Guam", - "country.guatemala": "Guatemala", - "country.guernsey": "Guernsey", - "country.guinea": "Guinea", - "country.guinea_bissau": "Guinea bissau", - "country.guyana": "Guyana", - "country.haiti": "Haiti", - "country.honduras": "Honduras", - "country.hong_kong": "Hong kong", - "country.hungary": "Hungary", - "country.iceland": "Iceland", - "country.india": "India", - "country.indonesia": "Indonesia", - "country.iran": "Iran", - "country.iraq": "Iraq", - "country.ireland": "Ireland", - "country.isle_of_man": "Isle of man", - "country.israel": "Israel", - "country.italy": "Italy", - "country.jamaica": "Jamaica", - "country.japan": "Japan", - "country.jersey": "Jersey", - "country.jordan": "Jordan", - "country.kazakhstan": "Kazakhstan", - "country.kenya": "Kenya", - "country.kiribati": "Kiribati", - "country.kosovo": "Kosovo", - "country.kuwait": "Kuwait", - "country.kyrgyzstan": "Kyrgyzstan", - "country.laos": "Laos", - "country.latvia": "Latvia", - "country.lebanon": "Lebanon", - "country.lesotho": "Lesotho", - "country.liberia": "Liberia", - "country.libya": "Libya", - "country.liechtenstein": "Liechtenstein", - "country.lithuania": "Lithuania", - "country.luxembourg": "Luxembourg", - "country.macau": "Macau", - "country.madagascar": "Madagascar", - "country.malawi": "Malawi", - "country.malaysia": "Malaysia", - "country.maldives": "Maldives", - "country.mali": "Mali", - "country.malta": "Malta", - "country.marshall_islands": "Marshall islands", - "country.martinique": "Martinique", - "country.mauritania": "Mauritania", - "country.mauritius": "Mauritius", - "country.mayotte": "Mayotte", - "country.mexico": "Mexico", - "country.micronesia": "Micronesia", - "country.moldova": "Moldova", - "country.monaco": "Monaco", - "country.mongolia": "Mongolia", - "country.montenegro": "Montenegro", - "country.montserrat": "Montserrat", - "country.morocco": "Morocco", - "country.mozambique": "Mozambique", - "country.myanmar": "Myanmar", - "country.namibia": "Namibia", - "country.nauru": "Nauru", - "country.nepal": "Nepal", - "country.netherlands": "Netherlands", - "country.new_caledonia": "New caledonia", - "country.new_zealand": "New zealand", - "country.nicaragua": "Nicaragua", - "country.niger": "Niger", - "country.nigeria": "Nigeria", - "country.niue": "Niue", - "country.norfolk_island": "Norfolk island", - "country.north_korea": "North korea", - "country.north_macedonia": "North macedonia", - "country.northern_mariana_islands": "Northern mariana islands", - "country.norway": "Norway", - "country.oman": "Oman", - "country.pakistan": "Pakistan", - "country.palau": "Palau", - "country.palestine": "Palestine", - "country.panama": "Panama", - "country.papua_new_guinea": "Papua new guinea", - "country.paraguay": "Paraguay", - "country.peru": "Peru", - "country.philippines": "Philippines", - "country.pitcairn_islands": "Pitcairn islands", - "country.poland": "Poland", - "country.portugal": "Portugal", - "country.puerto_rico": "Puerto rico", - "country.qatar": "Qatar", - "country.reunion": "Reunion", - "country.romania": "Romania", - "country.russia": "Russia", - "country.rwanda": "Rwanda", - "country.saint_barthelemy": "Saint barthelemy", - "country.saint_helena": "Saint helena", - "country.saint_kitts_and_nevis": "Saint kitts and nevis", - "country.saint_lucia": "Saint lucia", - "country.saint_martin": "Saint martin", - "country.saint_pierre_and_miquelon": "Saint pierre and miquelon", - "country.saint_vincent_and_the_grenadines": "Saint vincent and the grenadines", - "country.samoa": "Samoa", - "country.san_marino": "San marino", - "country.sao_tome_and_principe": "Sao tome and principe", - "country.saudi_arabia": "Saudi arabia", - "country.senegal": "Senegal", - "country.serbia": "Serbia", - "country.seychelles": "Seychelles", - "country.sierra_leone": "Sierra leone", - "country.singapore": "Singapore", - "country.sint_maarten": "Sint maarten", - "country.slovakia": "Slovakia", - "country.slovenia": "Slovenia", - "country.solomon_islands": "Solomon islands", - "country.somalia": "Somalia", - "country.south_africa": "South africa", - "country.south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands", - "country.south_korea": "South korea", - "country.south_sudan": "South sudan", - "country.spain": "Spain", - "country.sri_lanka": "Sri lanka", - "country.sudan": "Sudan", - "country.suriname": "Suriname", - "country.svalbard_and_jan_mayen": "Svalbard and jan mayen", - "country.sweden": "Sweden", - "country.switzerland": "Switzerland", - "country.syria": "Syria", - "country.taiwan": "Taiwan", - "country.tajikistan": "Tajikistan", - "country.tanzania": "Tanzania", - "country.thailand": "Thailand", - "country.timor_leste": "Timor leste", - "country.togo": "Togo", - "country.tokelau": "Tokelau", - "country.tonga": "Tonga", - "country.trinidad_and_tobago": "Trinidad and tobago", - "country.tunisia": "Tunisia", - "country.turkey": "Turkey", - "country.turkmenistan": "Turkmenistan", - "country.turks_and_caicos_islands": "Turks and caicos islands", - "country.tuvalu": "Tuvalu", - "country.us_virgin_islands": "Us virgin islands", - "country.uganda": "Uganda", - "country.ukraine": "Ukraine", - "country.united_arab_emirates": "United arab emirates", - "country.united_kingdom": "United kingdom", - "country.united_states": "United states", - "country.uruguay": "Uruguay", - "country.uzbekistan": "Uzbekistan", - "country.vanuatu": "Vanuatu", - "country.vatican_city": "Vatican city", - "country.venezuela": "Venezuela", - "country.vietnam": "Vietnam", - "country.wallis_and_futuna": "Wallis and futuna", - "country.western_sahara": "Western sahara", - "country.yemen": "Yemen", - "country.zambia": "Zambia", - "country.zimbabwe": "Zimbabwe" - } \ No newline at end of file diff --git a/public/locales/fa/country.json b/public/locales/fa/country.json deleted file mode 100644 index d77b3aa..0000000 --- a/public/locales/fa/country.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "country.afghanistan": "افغانستان", - "country.aland_islands": "جزایر آلند", - "country.albania": "آلبانی", - "country.algeria": "الجزایر", - "country.american_samoa": "ساموای آمریکایی", - "country.andorra": "آندورا", - "country.angola": "آنگولا", - "country.anguilla": "آنگویلا", - "country.antarctica": "جنوبگان", - "country.antigua_and_barbuda": "آنتیگوا و باربودا", - "country.argentina": "آرژانتین", - "country.armenia": "ارمنستان", - "country.aruba": "آروبا", - "country.australia": "استرالیا", - "country.austria": "اتریش", - "country.azerbaijan": "آذربایجان", - "country.bahamas": "باهاما", - "country.bahrain": "بحرین", - "country.bangladesh": "بنگلادش", - "country.barbados": "باربادوس", - "country.belarus": "بلاروس", - "country.belgium": "بلژیک", - "country.belize": "بلیز", - "country.benin": "بنین", - "country.bermuda": "برمودا", - "country.bhutan": "بوتان", - "country.bolivia": "بولیوی", - "country.bosnia_and_herzegovina": "بوسنی و هرزگوین", - "country.botswana": "بوتسوانا", - "country.brazil": "برزیل", - "country.british_virgin_islands": "جزایر ویرجین بریتانیا", - "country.brunei": "برونئی", - "country.bulgaria": "بلغارستان", - "country.burkina_faso": "بورکینافاسو", - "country.burundi": "بوروندی", - "country.cambodia": "کامبوج", - "country.cameroon": "کامرون", - "country.canada": "کانادا", - "country.cape_verde": "کیپ ورد", - "country.cayman_islands": "جزایر کیمن", - "country.central_african_republic": "جمهوری آفریقای مرکزی", - "country.chad": "چاد", - "country.chile": "شیلی", - "country.china": "چین", - "country.colombia": "کلمبیا", - "country.comoros": "کومور", - "country.costa_rica": "کاستاریکا", - "country.cote_divoire": "ساحل عاج", - "country.croatia": "کرواسی", - "country.cuba": "کوبا", - "country.cyprus": "قبرس", - "country.czech_republic": "جمهوری چک", - "country.denmark": "دانمارک", - "country.djibouti": "جیبوتی", - "country.dominica": "دومینیکا", - "country.dominican_republic": "جمهوری دومینیکن", - "country.ecuador": "اکوادور", - "country.egypt": "مصر", - "country.el_salvador": "السالوادور", - "country.equatorial_guinea": "گینه استوایی", - "country.eritrea": "اریتره", - "country.estonia": "استونی", - "country.eswatini": "سوازیلند", - "country.ethiopia": "اتیوپی", - "country.fiji": "فیجی", - "country.finland": "فنلاند", - "country.france": "فرانسه", - "country.gabon": "گابن", - "country.gambia": "گامبیا", - "country.georgia": "گرجستان", - "country.germany": "آلمان", - "country.ghana": "غنا", - "country.greece": "یونان", - "country.guatemala": "گواتمالا", - "country.guinea": "گینه", - "country.guinea_bissau": "گینه بیسائو", - "country.guyana": "گویان", - "country.haiti": "هائیتی", - "country.honduras": "هندوراس", - "country.hungary": "مجارستان", - "country.iceland": "ایسلند", - "country.india": "هندوستان", - "country.indonesia": "اندونزی", - "country.iran": "ایران", - "country.iraq": "عراق", - "country.ireland": "ایرلند", - "country.israel": "اسرائیل", - "country.italy": "ایتالیا", - "country.jamaica": "جامائیکا", - "country.japan": "ژاپن", - "country.jordan": "اردن", - "country.kazakhstan": "قزاقستان", - "country.kenya": "کنیا", - "country.kuwait": "کویت", - "country.kyrgyzstan": "قرقیزستان", - "country.laos": "لائوس", - "country.latvia": "لتونی", - "country.lebanon": "لبنان", - "country.lesotho": "لسوتو", - "country.liberia": "لیبریا", - "country.libya": "لیبی", - "country.luxembourg": "لوکزامبورگ", - "country.malaysia": "مالزی", - "country.maldives": "مالدیو", - "country.mali": "مالی", - "country.malta": "مالت", - "country.mauritania": "موریتانی", - "country.mauritius": "موریس", - "country.mexico": "مکزیک", - "country.moldova": "مولداوی", - "country.monaco": "موناکو", - "country.mongolia": "مغولستان", - "country.morocco": "مراکش", - "country.mozambique": "موزامبیک", - "country.myanmar": "میانمار", - "country.namibia": "نامیبیا", - "country.nepal": "نپال", - "country.netherlands": "هلند", - "country.new_zealand": "نیوزیلند", - "country.nicaragua": "نیکاراگوئه", - "country.niger": "نیجر", - "country.nigeria": "نیجریه", - "country.north_korea": "کره شمالی", - "country.north_macedonia": "مقدونیه", - "country.norway": "نروژ", - "country.oman": "عمان", - "country.pakistan": "پاکستان", - "country.palau": "پالائو", - "country.panama": "پاناما", - "country.papua_new_guinea": "پاپوآ گینه نو", - "country.paraguay": "پاراگوئه", - "country.peru": "پرو", - "country.philippines": "فیلیپین", - "country.poland": "لهستان", - "country.portugal": "پرتغال", - "country.qatar": "قطر", - "country.romania": "رومانی", - "country.russia": "روسیه", - "country.rwanda": "رواندا", - "country.saudi_arabia": "عربستان سعودی", - "country.senegal": "سنگال", - "country.serbia": "صربستان", - "country.seychelles": "سیشل", - "country.sierra_leone": "سیرالئون", - "country.singapore": "سنگاپور", - "country.south_africa": "آفریقای جنوبی", - "country.south_korea": "کره جنوبی", - "country.south_sudan": "سودان جنوبی", - "country.spain": "اسپانیا", - "country.sri_lanka": "سری‌لانکا", - "country.sudan": "سودان", - "country.suriname": "سورینام", - "country.sweden": "سوئد", - "country.switzerland": "سوئیس", - "country.syria": "سوریه", - "country.taiwan": "تایوان", - "country.tajikistan": "تاجیکستان", - "country.tanzania": "تانزانیا", - "country.thailand": "تایلند", - "country.timor_leste": "تیمور شرقی", - "country.togo": "توگو", - "country.tonga": "تونگا", - "country.trinidad_and_tobago": "ترینیداد و توباگو", - "country.tunisia": "تونس", - "country.turkey": "ترکیه", - "country.turkmenistan": "ترکمنستان", - "country.tuvalu": "تووالو", - "country.uganda": "اوگاندا", - "country.ukraine": "اوکراین", - "country.united_arab_emirates": "امارات متحده عربی", - "country.united_kingdom": "انگلستان", - "country.united_states": "ایالات متحده آمریکا", - "country.uruguay": "اروگوئه", - "country.uzbekistan": "ازبکستان", - "country.vanuatu": "وانواتو", - "country.venezuela": "ونزوئلا", - "country.vietnam": "ویتنام", - "country.yemen": "یمن", - "country.zambia": "زامبیا", - "country.zimbabwe": "زیمبابوه" - } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index a463dc1..c193bae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { CssBaseline } from '@mui/material'; import './App.css'; import { LanguageManager } from './components/LanguageManager'; import { AuthenticationPage } from './features/authorization/routes/AuthenticationPage'; -import { BrowserRouter, Navigate, Route, Routes } from 'react-router'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { ForgetPasswordPage } from './features/authorization/routes/ForgetPasswordPage'; function App() { diff --git a/src/components/components/DigitsInput.tsx b/src/components/components/DigitsInput.tsx index 90d8b36..88932f3 100644 --- a/src/components/components/DigitsInput.tsx +++ b/src/components/components/DigitsInput.tsx @@ -11,7 +11,7 @@ import { TextField, Stack } from '@mui/material'; interface DigitInputProps { error: boolean; success: boolean; - onChange: Dispatch>; + onChange: Dispatch>; } const DigitInput: React.FC = ({ @@ -26,13 +26,18 @@ const DigitInput: React.FC = ({ inputRefs.current[0]?.focus(); }, []); + const handleDigitInputValueChange = (value: string[]) => { + const formatted = value.filter((char) => char !== '').join(''); + onChange(formatted); + }; + const handleChange = (value: string, index: number) => { if (!/^\d$/.test(value) && value !== '') return; const newCode = [...code]; newCode[index] = value; setCode(newCode); - onChange(newCode); + handleDigitInputValueChange(newCode); if (value && index < 4 - 1) { inputRefs.current[index + 1]?.focus(); @@ -62,7 +67,7 @@ const DigitInput: React.FC = ({ }); setCode(newCode); - onChange(newCode); + handleDigitInputValueChange(newCode); // Focus the next empty input after the last pasted character const lastIndex = Math.min(pastedData.length, code.length) - 1; diff --git a/src/features/authorization/data/countries.ts b/src/countries.ts similarity index 100% rename from src/features/authorization/data/countries.ts rename to src/countries.ts diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index edba9e0..e02bd3a 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -1,6 +1,6 @@ import { useState, type JSX } from 'react'; import { LoginRegisterForm } from './LoginRegiserForm'; -import type { AuthMode, AuthType } from '../../types/authTypes'; +import type { AuthMode, AuthStep, AuthType } from '../../types/authTypes'; import { OtpVerifyForm } from './OtpVerifyForm'; import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; @@ -8,22 +8,17 @@ import { EnterPasswordForm } from './EnterPasswordForm'; import { UserStatus } from '../../types/userTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; -import { useSearchParams } from 'react-router'; +import { useNavigate, useSearchParams } from 'react-router-dom'; export const AuthenticationSteps = (): JSX.Element => { - const DEFAULT_RETURN_URL = 'https://account.business-harmony.com/'; + const navigate = useNavigate(); + const DEFAULT_RETURN_URL = '/profile'; const [searchParams] = useSearchParams(); const authReturnUrl: string = searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL; const [authMode, setAuthMode] = useState('register'); const [authType, setAuthType] = useState('phone'); - const [currentStep, setCurrentStep] = useState< - | 'emailOrPhone' - | 'verify' - | 'enterPassword' - | 'addPhoneNumber' - | 'addedPhoneNumberVerify' - >('emailOrPhone'); + const [currentStep, setCurrentStep] = useState('emailOrPhone'); const [loginRegisterValue, setLoginRegisterValue] = useState(''); const [countryCode, setCountryCode] = useState('+98'); const [addPhoneCountryCode, setAddPhoneCountryCode] = @@ -54,24 +49,28 @@ export const AuthenticationSteps = (): JSX.Element => { } }; - const handleOTPVerfied = (userId: GUID) => { - handleUserLoggedIn(userId); - }; - const handleUserLoggedIn = (userId: GUID) => { localStorage.setItem('userID', userId); - location.href = authReturnUrl; + redirectToReturnUrl(); }; const handleConfrimPhoneNumber = (userId: GUID) => { - handleUserLoggedIn(userId); + localStorage.setItem('userID', userId); setCurrentStep('addPhoneNumber'); }; const handlePhoneNumberVerified = () => { - location.href = authReturnUrl; + redirectToReturnUrl(); + }; + + const redirectToReturnUrl = () => { + if (authReturnUrl === DEFAULT_RETURN_URL) { + navigate(DEFAULT_RETURN_URL); + } else { + location.href = authReturnUrl; + } }; return ( @@ -95,7 +94,7 @@ export const AuthenticationSteps = (): JSX.Element => { onVerifyPhoneNumber={handleConfrimPhoneNumber} authReturnUrl={authReturnUrl} countryCode={countryCode} - onOTPVerified={handleOTPVerfied} + onOTPVerified={handleUserLoggedIn} onEditValue={() => setCurrentStep('emailOrPhone')} authMode={authMode} authType={authType} diff --git a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx index 0394f8c..132f136 100644 --- a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx +++ b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx @@ -41,6 +41,10 @@ export const CompleteSignUp = ({ const handleBlur = () => { setTouched(true); + handleValueError(); + }; + + const handleValueError = () => { if (!value) { setError(t('loginForm.thisFieldIsRequired')); } @@ -52,15 +56,11 @@ export const CompleteSignUp = ({ }; const handleCompleteSignUp = async () => { - if (!value) { - setError(t('loginForm.thisFieldIsRequired')); - inputRef.current?.focus(); - } - if (!isPhoneValid(countryCode, value)) { - setError(t('loginForm.phoneNumberIsInvalid')); + handleValueError(); + + if (!value || !isPhoneValid(countryCode, value)) { inputRef.current?.focus(); } else { - setError(undefined); setSendOtpLoading(true); await sendSmsOtp({ phoneNumber: countryCode + value }); diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx index 8fa1206..d5b9b89 100644 --- a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx +++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from 'react'; import { AuthenticationCard } from '../AuthenticationCard'; -import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-reactjs'; +import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-react'; import { Box, Button, @@ -11,7 +11,7 @@ import { } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { Toast } from '@/components/Toast'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import type { AuthType } from '../../types/authTypes'; import type { CountryCode, GUID } from '@/types/commonTypes'; import { @@ -48,7 +48,7 @@ export const EnterPasswordForm = ({ const [showPassword, setShowPassword] = useState(false); const inputRef = useRef(null); const [loginLoading, setLoginLoading] = useState(false); - const [loginStatus, setLoginStatus] = useState<'success' | 'failed'>(); + const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState(); const [loginAlertOpen, setLoginAlertOpen] = useState(false); const [loginFailedMessage, setLoginFailedMessage] = useState(''); const [sendOtpLoading, setSendOtpLoading] = useState(false); @@ -74,10 +74,10 @@ export const EnterPasswordForm = ({ const jsonRes = await result.json(); if (jsonRes.success) { - setLoginStatus('success'); + setIsLoginStatusSuccess(true); onLoggedIn(jsonRes.userId); } else { - setLoginStatus('failed'); + setIsLoginStatusSuccess(false); setLoginFailedMessage(jsonRes.message); } setLoginAlertOpen(true); @@ -103,9 +103,9 @@ export const EnterPasswordForm = ({ setLoginAlertOpen(false)} - color={loginStatus === 'failed' ? 'error' : 'success'} + color={!isLoginStatusSuccess ? 'error' : 'success'} > - {loginStatus === 'failed' + {!isLoginStatusSuccess ? loginFailedMessage : t('verify.youHaveSuccessfullyLoggedIn')} @@ -127,7 +127,7 @@ export const EnterPasswordForm = ({