feat:change countryocde component
This commit is contained in:
@@ -4,18 +4,18 @@
|
|||||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-latest
|
vmImage: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: '20.x'
|
versionSpec: '20.x'
|
||||||
displayName: 'Install Node.js'
|
displayName: 'Install Node.js'
|
||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
displayName: 'npm install and build'
|
displayName: 'npm install and build'
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -15,6 +15,7 @@
|
|||||||
"i18next": "^25.3.0",
|
"i18next": "^25.3.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"iconsax-reactjs": "^0.0.8",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^15.6.0",
|
"react-i18next": "^15.6.0",
|
||||||
@@ -3102,6 +3103,15 @@
|
|||||||
"cross-fetch": "4.0.0"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"i18next": "^25.3.0",
|
"i18next": "^25.3.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"iconsax-reactjs": "^0.0.8",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^15.6.0",
|
"react-i18next": "^15.6.0",
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
// Animate width and opacity based on the 'show' prop
|
|
||||||
width: show ? 'auto' : 0,
|
|
||||||
opacity: show ? 1 : 0,
|
|
||||||
transition: (theme) =>
|
|
||||||
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 */}
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, gap: 0.25 }}>
|
|
||||||
<Typography variant="body1" color="text.primary">
|
|
||||||
+41
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
163
src/features/authentication/components/CountryCodeSelector.tsx
Normal file
163
src/features/authentication/components/CountryCodeSelector.tsx
Normal file
@@ -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<HTMLInputElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 | HTMLElement>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(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 (
|
||||||
|
<Box
|
||||||
|
onClick={handleClick}
|
||||||
|
sx={{
|
||||||
|
// Animate width and opacity based on the 'show' prop
|
||||||
|
width: show ? 'auto' : 0,
|
||||||
|
opacity: show ? 1 : 0,
|
||||||
|
transition: (theme) =>
|
||||||
|
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 */}
|
||||||
|
<ArrowDown2 size="24" variant="Bold" />
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="text.primary"
|
||||||
|
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
slotProps={{
|
||||||
|
list: {
|
||||||
|
sx: {
|
||||||
|
// Set the width to match the anchor element's width
|
||||||
|
width: menuWidth,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
onEntered: handleMenuEntered,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<TextField
|
||||||
|
inputRef={searchInputRef}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="جست و جو"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{filteredCountries.map((country) => (
|
||||||
|
<MenuItem
|
||||||
|
key={country.code}
|
||||||
|
selected={country.phone === value}
|
||||||
|
onClick={() => handleSelect(country)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>{country.flag}</ListItemIcon>
|
||||||
|
<ListItemText primary={country.label} />
|
||||||
|
<Typography color="text.secondary">{country.phone}</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,16 +6,20 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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);
|
const isNumeric = (value: string) => /^\d*$/.test(value);
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const { t } = useTranslation('authentication');
|
const { t } = useTranslation('authentication');
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
const [countryCode, setCountryCode] = useState('+41');
|
||||||
const [inputType, setInputType] = useState<'phone' | 'email'>('phone');
|
const [inputType, setInputType] = useState<'phone' | 'email'>('phone');
|
||||||
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = event.target.value;
|
const newValue = event.target.value;
|
||||||
@@ -41,21 +45,29 @@ export function LoginForm() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
ref={textFieldRef}
|
||||||
|
inputRef={inputRef}
|
||||||
label={t('loginForm.emailOrPhoneLabel')}
|
label={t('loginForm.emailOrPhoneLabel')}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: { dir: 'ltr' },
|
htmlInput: { dir: 'ltr', sx: { lineHeight: 1.5 } },
|
||||||
input: {
|
input: {
|
||||||
|
// in en mode it placed in wrong end
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment
|
<InputAdornment
|
||||||
position="start"
|
position="start"
|
||||||
sx={{
|
sx={{
|
||||||
mr: 0,
|
mr: 0,
|
||||||
// transition: (theme) => theme.transitions.create('margin'),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CountryCodeAdornment show={showAdornment} />
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
show={showAdornment}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -64,8 +76,12 @@ export function LoginForm() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Button>{t('loginForm.submitButton')}</Button>
|
<Button onClick={() => inputRef.current?.focus()}>
|
||||||
<Button variant="outlined">{t('loginForm.loginWithGoogle')}</Button>
|
{t('loginForm.submitButton')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" startIcon={<Google variant="Bold" />}>
|
||||||
|
{t('loginForm.loginWithGoogle')}
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
15
src/features/authentication/data/countries.ts
Normal file
15
src/features/authentication/data/countries.ts
Normal file
@@ -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: '🇦🇪' },
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user