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