This commit is contained in:
2026-08-08 18:59:56 +03:30
commit a949fc3766
175 changed files with 16575 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import AuthHeader from '../auth-header';
import { mockFailureResponse } from '../../../../mocks/server';
import { renderWithClient } from '../../../../utils/test/react-query';
import { MemoryRouter } from 'react-router-dom';
describe('AuthHeader component', () => {
test('successful query component', async () => {
const result = renderWithClient(<AuthHeader />, MemoryRouter);
expect(await result.findByText(/تست/i)).toBeInTheDocument();
expect(await result.findByText(/خروج/i)).toBeInTheDocument();
});
test('failure query component', async () => {
mockFailureResponse();
const result = renderWithClient(<AuthHeader />, MemoryRouter);
expect(await result.findByText(/ورود/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,9 @@
import { render } from '@testing-library/react';
import Footer from '../footer';
describe('Footer component', () => {
test('successful render component', async () => {
const result = render(<Footer />);
expect(await result.findByTestId('footer')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,16 @@
import { renderWithClient } from '../../../../utils/test/react-query';
import { MemoryRouter } from 'react-router-dom';
import Header from '../header';
describe('Header component', () => {
test('successful render component', async () => {
const result = renderWithClient(<Header />, MemoryRouter);
expect(await result.findByTestId('header')).toBeInTheDocument();
expect(await result.findByTestId('header-nav')).toBeInTheDocument();
});
test('successful query component', async () => {
const result = renderWithClient(<Header />, MemoryRouter);
expect(await result.findByText(/Med Moshaver/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,21 @@
import { renderWithClient } from '../../../../utils/test/react-query';
import { MemoryRouter } from 'react-router-dom';
import MainLayout from '../index';
import { expectPageTestIds, expectPageTitle } from '../../../../utils/test/expects';
describe('MainLayout component', () => {
test('successful render component', async () => {
const title = 'title';
const result = renderWithClient(
<MainLayout loginRequired={false} title={title} />,
MemoryRouter
);
await expectPageTitle(title);
await expectPageTestIds(result);
});
test('successful render loader component', async () => {
const result = renderWithClient(<MainLayout loginRequired />, MemoryRouter);
expect(await result.findByTestId('loader')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,59 @@
import { useHistory } from 'react-router-dom';
import NavLink from '../../shared/buttons/nav-link';
import Button from '../../shared/buttons/button';
import ButtonGroup from '../../shared/buttons/button-group';
import { useUser } from '../../../hooks/react-query/use-user';
import { useLogoutMutation } from '../../../hooks/react-query/use-auth';
import { getLoginRedirectURL } from '../../utils/url';
type AuthHeaderProps = { className?: string };
function AuthHeader(props: AuthHeaderProps) {
const history = useHistory();
const { status, data: user } = useUser();
const { mutate: logoutUser } = useLogoutMutation();
if (status === 'error') {
return (
<ButtonGroup className={props.className}>
<NavLink
to={getLoginRedirectURL(history.location.pathname, history.location.search)}
color='teal'
colorWeight={600}
content='ورود'
className={props.className}
/>
<NavLink
to='/register'
color='teal'
colorWeight={600}
content='ثبت نام'
className={props.className}
/>
</ButtonGroup>
);
}
return (
<ButtonGroup className={props.className}>
<NavLink
to='/home'
color='teal'
colorWeight={600}
content={user?.name || user?.mobile}
loading={status === 'loading'}
disabled={status === 'loading'}
/>
<Button
content='خروج'
color='red'
colorWeight={500}
onClick={logoutUser}
loading={status === 'loading'}
disabled={status === 'loading'}
/>
</ButtonGroup>
);
}
export default AuthHeader;

View File

@@ -0,0 +1,5 @@
function Footer() {
return <footer data-testid='footer' />;
}
export default Footer;

View File

@@ -0,0 +1,87 @@
import { useState } from 'react';
import classNames from 'classnames';
import { MenuIcon } from '@heroicons/react/outline';
import { XIcon } from '@heroicons/react/solid';
import AuthHeader from './auth-header';
import NavLink from '../../shared/buttons/nav-link';
export type HeaderPropsType = { isAuthenticated: boolean };
function Header({ isAuthenticated }: HeaderPropsType) {
const [isHidden, setIsHidden] = useState(true);
const iconSize = 34;
return (
<header data-testid='header' className='sticky top-0 z-50'>
<nav
data-testid='header-nav'
className='flex flex-wrap items-center justify-between bg-teal-800 p-2'
>
<NavLink to='/' color='transparent' wrapperClassName='hidden md:block'>
<p className='text-lg font-bold'>Med Moshaver</p>
</NavLink>
<AuthHeader className='mx-2 md:order-last' />
<div className='flex md:hidden'>
<div className='flex md:hidden'>
<button id='hamburger' onClick={() => setIsHidden(isHidden => !isHidden)}>
<MenuIcon
width={iconSize}
height={iconSize}
className={classNames(
'toggle block',
{ hidden: !isHidden },
'rounded-md border-2 border-yellow-400 stroke-current p-1 text-yellow-400'
)}
/>
<XIcon
width={iconSize}
height={iconSize}
className={classNames(
'toggle block',
{ hidden: isHidden },
'rounded-md border-2 border-yellow-400 stroke-current p-1 text-yellow-400'
)}
/>
</button>
</div>
</div>
<div
className={classNames(
'toggle',
{ hidden: isHidden },
'text-bold mt-5 w-full text-right md:mt-0 md:flex md:w-auto',
'border-t-2 border-blue-900 md:border-none'
)}
>
{links
.filter(link => !link.auth || isAuthenticated)
.map(link => (
<div
key={link.id}
className={`
w-100 block rounded-none border-t-2 border-yellow-400
px-1 py-1 md:inline-block md:border-none
`}
>
<NavLink to={link.href} textSize='base' color='transparent'>
{link.name}
</NavLink>
</div>
))}
</div>
</nav>
</header>
);
}
const links = [
{ id: 1, name: 'معرفی سامانه', href: '/', auth: false },
{ id: 2, name: 'کد معرف', href: '/home', auth: true },
{ id: 3, name: 'مالی', href: '/financial', auth: true },
{ id: 4, name: 'شرایط و قوانین', href: '/terms-and-conditions', auth: false }
];
export default Header;

View File

@@ -0,0 +1,75 @@
import { ReactNode, useEffect } from 'react';
import { Redirect, RouteComponentProps, withRouter } from 'react-router-dom';
import Header from './header';
import Container from '../../shared/container';
import Footer from './footer';
import ScrollToTop from '../../shared/buttons/scroll-to-top';
import Loader from '../../shared/loader';
import { getLoginRedirectURL } from '../../utils/url';
import { useUser } from '../../../hooks/react-query/use-user';
import { useIntroducer } from '../../../hooks/react-query/use-introducer';
import CreateIntroducerForm from '../../shared/create-introducer-form';
export type MainLayoutProps = RouteComponentProps & {
title?: string;
loginRequired?: boolean;
introducerRequired?: boolean;
children?: ReactNode;
};
function MainLayout({
title,
loginRequired = true,
introducerRequired = false,
location,
children
}: MainLayoutProps) {
const { status: userStatus } = useUser();
const { status: introducerStatus } = useIntroducer(
introducerRequired && userStatus === 'success'
);
useEffect(() => {
if (title) document.title = title;
}, [title]);
function renderContent() {
if (introducerRequired && introducerStatus === 'error')
return (
<div className='mx-auto w-full w-full sm:w-4/5 md:w-3/4 lg:w-2/3'>
<CreateIntroducerForm />
</div>
);
if (
(introducerRequired && ['indle', 'loading'].includes(introducerStatus)) ||
(loginRequired && userStatus === 'loading')
) {
return (
<div>
<Loader />
</div>
);
}
return children;
}
if (loginRequired && userStatus === 'error')
return <Redirect to={getLoginRedirectURL(location.pathname, location.search)} />;
return (
<>
<div className='flex h-screen flex-col'>
<Header isAuthenticated={userStatus === 'success'} />
<main data-testid='main' className='flex-1 py-4'>
<Container>{renderContent()}</Container>
</main>
<Footer />
</div>
<ScrollToTop />
</>
);
}
export default withRouter(MainLayout);