feat:change countryocde component

This commit is contained in:
Sajad Mirjalili
2025-07-21 18:22:25 +03:30
parent b69ca9fbac
commit 2190518c26
7 changed files with 221 additions and 51 deletions

View File

@@ -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'

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
@@ -41,21 +45,29 @@ export function LoginForm() {
</Stack>
<TextField
ref={textFieldRef}
inputRef={inputRef}
label={t('loginForm.emailOrPhoneLabel')}
value={value}
onChange={handleInputChange}
slotProps={{
htmlInput: { dir: 'ltr' },
htmlInput: { dir: 'ltr', sx: { lineHeight: 1.5 } },
input: {
// in en mode it placed in wrong end
endAdornment: (
<InputAdornment
position="start"
sx={{
mr: 0,
// transition: (theme) => theme.transitions.create('margin'),
}}
>
<CountryCodeAdornment show={showAdornment} />
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={showAdornment}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
</InputAdornment>
),
},
@@ -64,8 +76,12 @@ export function LoginForm() {
/>
<Stack spacing={2}>
<Button>{t('loginForm.submitButton')}</Button>
<Button variant="outlined">{t('loginForm.loginWithGoogle')}</Button>
<Button onClick={() => inputRef.current?.focus()}>
{t('loginForm.submitButton')}
</Button>
<Button variant="outlined" startIcon={<Google variant="Bold" />}>
{t('loginForm.loginWithGoogle')}
</Button>
</Stack>
</Box>
);

View 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: '🇦🇪' },
];