fix: base component dirs, remove duplicate component instance

This commit is contained in:
Sajad Mirjalili
2025-09-26 16:02:28 +03:30
parent 5e332861cd
commit d4ee5114ab
18 changed files with 76 additions and 290 deletions

View File

@@ -4,7 +4,7 @@ import { LanguageManager } from '@/components/LanguageManager';
import { RouterProvider } from 'react-router-dom';
import { router } from '@/routes';
import { useAuth } from './hooks/useAuth';
import { Loading } from './components/Loading';
import { Loading } from './components/routes/Loading';
function App() {
const { authFinished } = useAuth();

16
src/assets/logo_icon.svg Normal file
View File

@@ -0,0 +1,16 @@
<svg width="30" height="24" viewBox="0 0 30 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.9995 12C29.9995 8.81744 28.7352 5.76516 26.4847 3.51472C24.2342 1.26429 21.1819 4.17348e-07 17.9993 0C14.8166 -4.17347e-07 11.7643 1.26429 9.51383 3.51472C7.26333 5.76516 5.99902 8.81744 5.99902 12H9.74915C10.9918 12 11.961 10.9587 12.4161 9.80234C12.715 9.04317 13.1684 8.3455 13.7565 7.75736C14.8818 6.63215 16.4079 6.00001 17.9993 6.00001C19.5906 6.00001 21.1168 6.63215 22.242 7.75736C23.3673 8.88258 23.9994 10.4087 23.9994 12H29.9995Z" fill="url(#paint0_linear_5679_13944)"/>
<path d="M23.087 16.5922C23.6901 15.1363 24.0005 13.5758 24.0005 12H30C30 18.6274 24.6273 24 17.9997 24C16.9618 24 15.9546 23.8682 14.9941 23.6205C15.5376 23.4805 16.0717 23.3022 16.5925 23.0865C18.0485 22.4835 19.3713 21.5996 20.4857 20.4853C21.6 19.371 22.484 18.0481 23.087 16.5922Z" fill="#FBCF8E"/>
<path d="M0 12C-2.25446e-07 13.5759 0.310395 15.1363 0.913462 16.5923C1.51653 18.0482 2.40046 19.371 3.51479 20.4854C4.62911 21.5996 5.95201 22.4835 7.40795 23.0866C8.86387 23.6897 10.4243 24 12.0003 24C13.5761 24 15.1366 23.6897 16.5925 23.0866C18.0485 22.4835 19.3714 21.5996 20.4857 20.4854C21.6 19.371 22.4839 18.0482 23.087 16.5923C23.6901 15.1363 24.0005 13.5759 24.0005 12H20.2504C19.0077 12 18.0386 13.0414 17.5834 14.1976C17.5705 14.2306 17.5572 14.2634 17.5436 14.2962C17.2421 15.0241 16.8001 15.6856 16.2429 16.2427C15.6858 16.7998 15.0243 17.2418 14.2964 17.5433C13.5685 17.8449 12.7882 18.0001 12.0003 18.0001C11.2123 18.0001 10.4321 17.8449 9.70413 17.5433C8.97612 17.2418 8.31471 16.7998 7.7575 16.2427C7.20035 15.6856 6.75839 15.0241 6.45686 14.2962C6.15532 13.5682 6.00012 12.788 6.00012 12H0Z" fill="url(#paint1_linear_5679_13944)"/>
<path d="M0.000976562 12C0.000976562 5.37257 5.37365 0 12.0012 0C13.0388 0 14.0457 0.131696 15.0061 0.379295C12.9454 0.91015 11.0444 1.98446 9.51404 3.51473C7.26357 5.76515 5.99928 8.81744 5.99928 12H0.000976562Z" fill="#73E5E2"/>
<defs>
<linearGradient id="paint0_linear_5679_13944" x1="29.9995" y1="6.00001" x2="5.99902" y2="6.00001" gradientUnits="userSpaceOnUse">
<stop stop-color="#73E5E2"/>
<stop offset="1" stop-color="#394F87"/>
</linearGradient>
<linearGradient id="paint1_linear_5679_13944" x1="7.8682e-07" y1="18.0001" x2="24.0005" y2="18.0001" gradientUnits="userSpaceOnUse">
<stop stop-color="#FBDC8E"/>
<stop offset="1" stop-color="#FB958E"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -12,7 +12,7 @@ import { useMemo, useRef, useState, type RefObject } from 'react';
import { ArrowDown2 } from 'iconsax-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { countries, type Country } from '../../../data/countries';
import { countries, type Country } from '../data/countries';
import type { CountryCode } from '@/types/commonTypes';
import { Icon } from '@rkheftan/harmony-ui';
import { LTRTypography } from '@/components/common/LTRTypography';
@@ -40,7 +40,7 @@ export function CountryCodeSelector({
const open = Boolean(anchorEl);
const searchInputRef = useRef<HTMLInputElement>(null);
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
const { t } = useTranslation(['country', 'common']);
const { t, i18n } = useTranslation(['country', 'common']);
const selectedCountry =
countries.find((c) => c.phone === value) || countries[0];
@@ -106,7 +106,7 @@ export function CountryCodeSelector({
height: '100%',
display: 'flex',
alignItems: 'center',
flexDirection: 'row-reverse',
flexDirection: i18n.dir() === 'ltr' ? 'row-reverse' : 'row',
gap: 0.25,
pl: show ? 0.25 : 0,
@@ -147,11 +147,11 @@ export function CountryCodeSelector({
open={open}
onClose={handleClose}
slotProps={{
list: {
paper: {
sx: {
width: menuWidth,
height: '18.75rem',
p: 0,
maxHeight: '20rem',
overflow: 'hidden',
},
},
transition: { onEntered: handleMenuEntered },
@@ -160,6 +160,7 @@ export function CountryCodeSelector({
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
>
<Box
tabIndex={-1}
sx={{
position: 'sticky',
top: 0,
@@ -178,7 +179,7 @@ export function CountryCodeSelector({
/>
</Box>
<Box sx={{ width: '100%', height: '14.75rem' }}>
<Box component="div" sx={{ width: '100%', height: '25rem' }}>
{filteredCountries.length === 0 ? (
<MenuItem disabled>
<ListItem>

View File

@@ -1,7 +1,25 @@
import LogoSvg from '@/assets/logo.svg';
import LogoIconSvg from '@/assets/logo_icon.svg';
import { Box, type SxProps } from '@mui/material';
function Logo() {
return <img src={LogoSvg} style={{ width: '150px', height: 'auto' }} />;
interface LogoProps {
boxSx?: SxProps;
isIcon?: boolean;
}
function Logo({ boxSx, isIcon = false }: LogoProps) {
// TODO: handle transition
return (
<Box
component="img"
src={isIcon ? LogoIconSvg : LogoSvg}
sx={{
width: isIcon ? '30px' : '120px',
height: 'auto',
...boxSx,
}}
/>
);
}
export default Logo;

View File

@@ -1,27 +0,0 @@
import { Alert, Snackbar, type AlertColor } from '@mui/material';
import { 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 (
<Snackbar
sx={{ width: (t) => `calc(100% - ${t.spacing(6)})`, maxWidth: '396px' }}
open={open}
onClose={onClose}
>
<Alert
onClose={onClose}
severity={color}
variant="filled"
sx={{ width: '100%' }}
>
{children}
</Alert>
</Snackbar>
);
};

View File

@@ -85,9 +85,7 @@ export default function ProductsMenu({
</Button>
</Stack>
<Box sx={{ width: 69 }}>
<product.LogoComponent />
</Box>
<Box sx={{ width: 69 }}>{product.LogoComponent}</Box>
</Stack>
))}
</Box>

View File

@@ -1,12 +1,12 @@
import Logo from '@/components/Logo';
import { type FC } from 'react';
import type { ReactElement } from 'react';
export interface Product {
id: string;
titleKey: string;
descriptionKey: string;
LogoComponent: FC;
demoLink: string | null;
LogoComponent: ReactElement;
demoLink: string;
}
export const productsData: Product[] = [
@@ -14,8 +14,8 @@ export const productsData: Product[] = [
id: 'harmony-club',
titleKey: 'products.harmonyClub.title',
descriptionKey: 'products.harmonyClub.description',
LogoComponent: Logo, // Reference the component
demoLink: null, // FIXME: update this url
LogoComponent: <Logo isIcon boxSx={{ width: 69, height: 55 }} />, // Reference the component
demoLink: '', // FIXME: update this url
},
// add more products here
];

View File

@@ -3,7 +3,7 @@ import parsePhoneNumberFromString from 'libphonenumber-js';
import { useRef, useState, type Dispatch } from 'react';
import { useTranslation } from 'react-i18next';
import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector';
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
import { sendSmsOtp } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes';
import { useApi } from '@/hooks/useApi';

View File

@@ -5,7 +5,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric';
import type { AuthFactory, AuthType } from '../../types/authTypes';
import { isEmail } from '@/utils/regexes/isEmail';
import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector';
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
import type { LoginResult, UserStatus } from '../../types/userTypes';
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes';

View File

@@ -5,7 +5,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric';
import type { AuthType } from '../../types/authTypes';
import { isEmail } from '@/utils/regexes/isEmail';
import { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../CountryCodeSelector';
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
import type { CountryCode } from '@/types/commonTypes';
import { sendForgetPassCode } from '../../api/authorizationAPI';
import type { SendForgetPassCodeRequest } from '../../types/userTypes';

View File

@@ -1,213 +0,0 @@
import {
Box,
InputAdornment,
ListItem,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
TextField,
Typography,
} from '@mui/material';
import { useMemo, useRef, useState, type RefObject } from 'react';
import { ArrowDown2 } from 'iconsax-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { countries, type Country } from '../../../data/countries';
import type { CountryCode } from '@/types/commonTypes';
import { Icon } from '@rkheftan/harmony-ui';
import { LTRBox } from '@/components/common/LTRBox';
import { Virtuoso } from 'react-virtuoso';
interface CountryCodeSelectorProps {
show: boolean;
value: CountryCode;
onChange: (newValue: CountryCode) => void;
menuAnchor: HTMLElement | null;
onCloseFocusRef: RefObject<HTMLInputElement | null>;
}
export function CountryCodeSelector({
show,
value,
onChange,
menuAnchor,
onCloseFocusRef,
}: CountryCodeSelectorProps) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const open = Boolean(anchorEl);
const searchInputRef = useRef<HTMLInputElement>(null);
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
const { t, i18n } = useTranslation(['country', 'common']);
const selectedCountry =
countries.find((c) => c.phone === value) || countries[0];
const handleClick = () => setAnchorEl(menuAnchor);
const handleClose = () => {
setTimeout(() => setAnchorEl(null), 0);
setTimeout(() => {
onCloseFocusRef.current?.focus();
}, 100);
setSearchTerm('');
};
const handleSelect = (country: Country) => {
onChange(country.phone);
handleClose();
};
const handleMenuEntered = () => {
searchInputRef.current?.focus();
};
const filteredCountries = useMemo(
() =>
countries.filter(
(country) =>
t(country.label, { ns: 'country' })
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
country.phone.includes(searchTerm),
),
[searchTerm, t],
);
const initialIndex = useMemo(() => {
const index = filteredCountries.findIndex((c) => c.phone === value);
return Math.max(0, index);
}, [filteredCountries, value]);
return (
<InputAdornment
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
sx={{ mx: 0 }}
>
<Box
onClick={handleClick}
sx={{
width: show ? 'auto' : 0,
opacity: show ? 1 : 0,
transition: (theme) =>
theme.transitions.create(['width', 'opacity'], {
duration: theme.transitions.duration.standard,
}),
overflow: 'hidden',
whiteSpace: 'nowrap',
height: '200%',
display: 'flex',
alignItems: 'center',
gap: 0.25,
pl: show ? 0.25 : 0,
'&:hover': { cursor: 'pointer' },
}}
>
<Icon Component={ArrowDown2} size="medium" variant="Bold" />
<Typography
variant="body1"
color="text.primary"
component="div"
sx={{ direction: 'rtl' }}
>
{value}
</Typography>
<ReactCountryFlag
countryCode={selectedCountry.code}
svg
style={{
height: '1.5rem',
width: '1.5rem',
marginTop: '-2px',
marginRight: '4px',
}}
/>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
slotProps={{
paper: {
sx: {
width: menuWidth,
maxHeight: '20rem',
overflow: 'hidden',
},
},
transition: { onEntered: handleMenuEntered },
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
>
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1,
p: 1,
backgroundColor: 'background.paper',
}}
>
<TextField
inputRef={searchInputRef}
size="small"
fullWidth
label={t('labels.search', { ns: 'common' })}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</Box>
<Box sx={{ width: '100%', height: '25rem' }}>
{filteredCountries.length === 0 ? (
<MenuItem disabled>
<ListItem>
<ListItemText>
{t('messages.noResualtFound', { ns: 'common' })}
</ListItemText>
</ListItem>
</MenuItem>
) : (
<Virtuoso
style={{ height: '100%', width: '100%' }}
data={filteredCountries}
initialTopMostItemIndex={initialIndex}
itemContent={(_, country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
sx={{ justifyContent: 'space-between' }}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ListItemIcon sx={{ minWidth: 'auto' }}>
<ReactCountryFlag
countryCode={country.code}
svg
style={{ height: '1.125rem', width: '1.125rem' }}
/>
</ListItemIcon>
<ListItemText
primary={t(country.label, { ns: 'country' })}
/>
</Box>
<Typography color="text.secondary">
<LTRBox>{country.phone}</LTRBox>
</Typography>
</MenuItem>
)}
computeItemKey={(_, c) => c.code}
/>
)}
</Box>
</Menu>
</Box>
</InputAdornment>
);
}

View File

@@ -13,7 +13,7 @@ import { Icon } from '@rkheftan/harmony-ui';
import { type PhoneEditFormProps } from '@/features/profile/types/settingsType';
import { useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { CountryCodeSelector } from '@/features/authentication/components/CountryCodeSelector';
import { CountryCodeSelector } from '@/components/CountryCodeSelector';
import { LTRTypography } from '@/components/common/LTRTypography';
export default function PhoneEditForm({

View File

@@ -1,5 +1,5 @@
import { Layout } from '@/components';
import { NavigateWithToast } from '@/components/NavigateWithToast';
import { NavigateWithToast } from '@/components/routes/NavigateWithToast';
import { ProfileProvider } from '@/features/profile/providers/ProfileProvider';
import {
Calendar,

View File

@@ -1,8 +1,8 @@
import { Suspense, type ReactNode } from 'react';
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import { appRoutes, type RouteConfig } from './config';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import { Loading } from '@/components/Loading';
import { ProtectedRoute } from '@/components/routes/ProtectedRoute';
import { Loading } from '@/components/routes/Loading';
/**
* A recursive function to map our custom route config to the format