apps/web: add mobile tabs

This commit is contained in:
vchikalkin 2024-06-19 14:15:59 +03:00
parent 07ca9a5b2a
commit b87c22b6ed
7 changed files with 194 additions and 19 deletions

View File

@ -1,3 +1,5 @@
import { NavigationContext } from '@/context/navigation';
import { useContext } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
const Container = styled.div` const Container = styled.div`
@ -22,11 +24,31 @@ const TabButton = styled.button`
`; `;
export function NavigationBar() { export function NavigationBar() {
const { setCurrentTab, tabsList } = useContext(NavigationContext);
return ( return (
<Container> <Container>
<TabButton>Tab1</TabButton> {tabsList.map(({ key, title }) => (
<TabButton>Tab2</TabButton> <TabButton key={key} onClick={() => setCurrentTab(key)}>
<TabButton>Tab3</TabButton> {title}
</TabButton>
))}
</Container> </Container>
); );
} }
function Tab({ children, visible }) {
if (!visible) return null;
return children;
}
export function Tabs({ tabs }) {
const { currentTab } = useContext(NavigationContext);
return tabs.map(({ Component, key }) => (
<Tab key={key} visible={key === currentTab}>
<Component />
</Tab>
));
}

View File

@ -2,6 +2,7 @@ import Header from './Header';
import { AppMenu } from './Menu'; import { AppMenu } from './Menu';
import { NavigationBar } from './Navigation'; import { NavigationBar } from './Navigation';
import { screens } from '@/config/ui'; import { screens } from '@/config/ui';
import { NavigationProvider } from '@/context/navigation';
import { max, min } from '@/styles/mq'; import { max, min } from '@/styles/mq';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import styled from 'styled-components'; import styled from 'styled-components';
@ -24,13 +25,13 @@ const Main = styled.main`
export default function Layout({ children, user }) { export default function Layout({ children, user }) {
return ( return (
<> <NavigationProvider>
<Header /> <Header />
{user?.admin ? <AppMenu /> : false} {user?.admin ? <AppMenu /> : false}
<Main>{children}</Main> <Main>{children}</Main>
<MediaQuery maxWidth={screens.laptop}> <MediaQuery maxWidth={screens.laptop}>
<NavigationBar /> <NavigationBar />
</MediaQuery> </MediaQuery>
</> </NavigationProvider>
); );
} }

View File

@ -0,0 +1,27 @@
import { createContext, useMemo, useState } from 'react';
type Tab = {
key: string;
title: string;
};
type NavigationContextType = {
currentTab: string;
setCurrentTab: (tab: string) => void;
setTabsList: (list: Tab[]) => void;
tabsList: Tab[];
};
export const NavigationContext = createContext<NavigationContextType>({} as NavigationContextType);
export function NavigationProvider({ children }: { readonly children: React.ReactNode }) {
const [currentTab, setCurrentTab] = useState('');
const [tabsList, setTabsList] = useState<Tab[]>([]);
const value = useMemo(
() => ({ currentTab, setCurrentTab, setTabsList, tabsList }),
[currentTab, setCurrentTab, setTabsList, tabsList]
);
return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;
}

View File

@ -33,6 +33,7 @@
"radash": "^11.0.0", "radash": "^11.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-responsive": "^10.0.0",
"styled-components": "^5.3.11", "styled-components": "^5.3.11",
"superjson": "^2.2.1", "superjson": "^2.2.1",
"tools": "workspace:*", "tools": "workspace:*",

View File

@ -1,11 +1,39 @@
import initializeApollo from '@/apollo/client'; import initializeApollo from '@/apollo/client';
import * as Calculation from '@/Components/Calculation'; import * as Calculation from '@/Components/Calculation';
import { Error } from '@/Components/Common/Error'; import { Error } from '@/Components/Common/Error';
import { Tabs } from '@/Components/Layout/Navigation';
import { screens } from '@/config/ui';
import { NavigationContext } from '@/context/navigation';
import * as hooks from '@/process/hooks'; import * as hooks from '@/process/hooks';
import { getPageTitle } from '@/utils/page'; import { getPageTitle } from '@/utils/page';
import { makeGetUserType } from '@/utils/user'; import { makeGetUserType } from '@/utils/user';
import { dehydrate, QueryClient } from '@tanstack/react-query'; import { dehydrate, QueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import { useContext, useEffect } from 'react';
const MediaQuery = dynamic(() => import('react-responsive'), {
ssr: false,
});
const tabs = [
{
Component: () => <Calculation.Settings />,
key: 'settings',
title: 'Расчет',
},
{
Component: () => <Calculation.Form prune={['unlimited']} />,
key: 'form',
title: 'Параметры',
},
{
Component: () => <Calculation.Output />,
key: 'output',
title: 'Результаты',
},
];
function Content() { function Content() {
hooks.useSentryScope(); hooks.useSentryScope();
@ -13,15 +41,32 @@ function Content() {
hooks.useInsuranceData(); hooks.useInsuranceData();
hooks.useReactions(); hooks.useReactions();
const { setCurrentTab, setTabsList } = useContext(NavigationContext);
useEffect(() => {
setTabsList(tabs);
setCurrentTab('settings');
}, [setCurrentTab, setTabsList]);
return ( return (
<Calculation.Layout> <>
<Head> <Head>
<title>{getPageTitle()}</title> <title>{getPageTitle()}</title>
</Head> </Head>
<Calculation.Form prune={['unlimited']} /> <MediaQuery maxWidth={screens.laptop}>
<Calculation.Settings /> {(match) => {
<Calculation.Output /> if (match) return <Tabs tabs={tabs} />;
</Calculation.Layout>
return (
<Calculation.Layout>
<Calculation.Form prune={['unlimited']} />
<Calculation.Settings />
<Calculation.Output />
</Calculation.Layout>
);
}}
</MediaQuery>
</>
); );
} }

View File

@ -1,11 +1,39 @@
import initializeApollo from '@/apollo/client'; import initializeApollo from '@/apollo/client';
import * as Calculation from '@/Components/Calculation'; import * as Calculation from '@/Components/Calculation';
import { Error } from '@/Components/Common/Error'; import { Error } from '@/Components/Common/Error';
import { Tabs } from '@/Components/Layout/Navigation';
import { screens } from '@/config/ui';
import { NavigationContext } from '@/context/navigation';
import * as hooks from '@/process/hooks'; import * as hooks from '@/process/hooks';
import { getPageTitle } from '@/utils/page'; import { getPageTitle } from '@/utils/page';
import { makeGetUserType } from '@/utils/user'; import { makeGetUserType } from '@/utils/user';
import { dehydrate, QueryClient } from '@tanstack/react-query'; import { dehydrate, QueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import { useContext, useEffect } from 'react';
const MediaQuery = dynamic(() => import('react-responsive'), {
ssr: false,
});
const tabs = [
{
Component: () => <Calculation.Settings />,
key: 'settings',
title: 'Расчет',
},
{
Component: () => <Calculation.Form />,
key: 'form',
title: 'Параметры',
},
{
Component: () => <Calculation.Output />,
key: 'output',
title: 'Результаты',
},
];
function Content() { function Content() {
hooks.useSentryScope(); hooks.useSentryScope();
@ -14,15 +42,32 @@ function Content() {
hooks.useInsuranceData(); hooks.useInsuranceData();
hooks.useReactions(); hooks.useReactions();
const { setCurrentTab, setTabsList } = useContext(NavigationContext);
useEffect(() => {
setTabsList(tabs);
setCurrentTab('settings');
}, [setCurrentTab, setTabsList]);
return ( return (
<Calculation.Layout> <>
<Head> <Head>
<title>{getPageTitle('Без ограничений')}</title> <title>{getPageTitle('Без ограничений')}</title>
</Head> </Head>
<Calculation.Form /> <MediaQuery maxWidth={screens.laptop}>
<Calculation.Settings /> {(match) => {
<Calculation.Output /> if (match) return <Tabs tabs={tabs} />;
</Calculation.Layout>
return (
<Calculation.Layout>
<Calculation.Form />
<Calculation.Settings />
<Calculation.Output />
</Calculation.Layout>
);
}}
</MediaQuery>
</>
); );
} }

42
pnpm-lock.yaml generated
View File

@ -177,6 +177,9 @@ importers:
react-dom: react-dom:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-responsive:
specifier: ^10.0.0
version: 10.0.0(react@18.2.0)
styled-components: styled-components:
specifier: ^5.3.11 specifier: ^5.3.11
version: 5.3.11(@babel/core@7.23.9)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) version: 5.3.11(@babel/core@7.23.9)(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0)
@ -5899,6 +5902,10 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: false dev: false
/css-mediaquery@0.1.2:
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
dev: false
/css-to-react-native@3.2.0: /css-to-react-native@3.2.0:
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
dependencies: dependencies:
@ -6956,7 +6963,7 @@ packages:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3)
'@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.3.3)
eslint: 8.57.0 eslint: 8.57.0
jest: 29.7.0(@types/node@20.11.20)(ts-node@10.9.2) jest: 29.7.0(@types/node@18.19.18)(ts-node@10.9.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
@ -8200,7 +8207,7 @@ packages:
'@graphql-tools/json-file-loader': 7.4.18(graphql@16.8.1) '@graphql-tools/json-file-loader': 7.4.18(graphql@16.8.1)
'@graphql-tools/load': 7.8.14(graphql@16.8.1) '@graphql-tools/load': 7.8.14(graphql@16.8.1)
'@graphql-tools/merge': 8.4.2(graphql@16.8.1) '@graphql-tools/merge': 8.4.2(graphql@16.8.1)
'@graphql-tools/url-loader': 7.17.18(@types/node@18.19.18)(graphql@16.8.1) '@graphql-tools/url-loader': 7.17.18(@types/node@20.11.20)(graphql@16.8.1)
'@graphql-tools/utils': 9.2.1(graphql@16.8.1) '@graphql-tools/utils': 9.2.1(graphql@16.8.1)
cosmiconfig: 8.0.0 cosmiconfig: 8.0.0
graphql: 16.8.1 graphql: 16.8.1
@ -8474,6 +8481,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/hyphenate-style-name@1.0.5:
resolution: {integrity: sha512-fedL7PRwmeVkgyhu9hLeTBaI6wcGk7JGJswdaRsa5aUbkXI1kr1xZwTPBtaYPpwf56878iDek6VbVnuWMebJmw==}
dev: false
/iconv-lite@0.4.24: /iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9320,7 +9331,7 @@ packages:
pretty-format: 29.7.0 pretty-format: 29.7.0
slash: 3.0.0 slash: 3.0.0
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
ts-node: 10.9.2(@types/node@20.11.20)(typescript@5.3.3) ts-node: 10.9.2(@types/node@18.19.18)(typescript@5.3.3)
transitivePeerDependencies: transitivePeerDependencies:
- babel-plugin-macros - babel-plugin-macros
- supports-color - supports-color
@ -10223,6 +10234,12 @@ packages:
object-visit: 1.0.1 object-visit: 1.0.1
dev: true dev: true
/matchmediaquery@0.4.2:
resolution: {integrity: sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==}
dependencies:
css-mediaquery: 0.1.2
dev: false
/media-typer@0.3.0: /media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -11829,6 +11846,19 @@ packages:
/react-is@18.2.0: /react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
/react-responsive@10.0.0(react@18.2.0):
resolution: {integrity: sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==}
engines: {node: '>=14'}
peerDependencies:
react: '>=16.8.0'
dependencies:
hyphenate-style-name: 1.0.5
matchmediaquery: 0.4.2
prop-types: 15.8.1
react: 18.2.0
shallow-equal: 3.1.0
dev: false
/react@18.2.0: /react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -12438,6 +12468,10 @@ packages:
/setprototypeof@1.2.0: /setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
/shallow-equal@3.1.0:
resolution: {integrity: sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==}
dev: false
/shallowequal@1.1.0: /shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: false dev: false
@ -13241,7 +13275,7 @@ packages:
'@babel/core': 7.23.9 '@babel/core': 7.23.9
bs-logger: 0.2.6 bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.11.20)(ts-node@10.9.2) jest: 29.7.0(@types/node@18.19.18)(ts-node@10.9.2)
jest-util: 29.7.0 jest-util: 29.7.0
json5: 2.2.3 json5: 2.2.3
lodash.memoize: 4.1.2 lodash.memoize: 4.1.2