init
This commit is contained in:
commit
ece046d2fd
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
/src/mock
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.env
|
||||||
|
.env.development
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.idea/*
|
||||||
|
|
||||||
|
#docker
|
||||||
|
docker-compose.*
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
30
README.md
Normal file
30
README.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Instruction
|
||||||
|
|
||||||
|
- download
|
||||||
|
- go to download directory
|
||||||
|
- run `yarn` to install packages
|
||||||
|
- create file: `docker-compose.yml`
|
||||||
|
```yml
|
||||||
|
version: '3.0'
|
||||||
|
|
||||||
|
services:
|
||||||
|
front:
|
||||||
|
image: nginx:alpine
|
||||||
|
volumes:
|
||||||
|
- ./build/:/usr/share/nginx/html/
|
||||||
|
- ./nginx/:/etc/nginx/conf.d/
|
||||||
|
ports:
|
||||||
|
- 8080:80 # set your port here
|
||||||
|
```
|
||||||
|
- create file: `.env.production.local`
|
||||||
|
```bash
|
||||||
|
REACT_APP_HOST=localhost
|
||||||
|
REACT_APP_PORT=4000
|
||||||
|
REACT_APP_OPENID_SERVER=https://server:port/realms/your-realm
|
||||||
|
REACT_APP_CLIENT_ID=your-client
|
||||||
|
```
|
||||||
|
- run:
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
8
nginx/default.conf
Normal file
8
nginx/default.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
69
package.json
Normal file
69
package.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@cycjimmy/jsmpeg-player": "^6.0.5",
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@mantine/carousel": "^6.0.16",
|
||||||
|
"@mantine/core": "^6.0.16",
|
||||||
|
"@mantine/dates": "^6.0.16",
|
||||||
|
"@mantine/hooks": "^6.0.16",
|
||||||
|
"@tabler/icons-react": "^2.24.0",
|
||||||
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@testing-library/react": "^13.0.0",
|
||||||
|
"@testing-library/user-event": "^13.2.1",
|
||||||
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/node": "^16.7.13",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"@types/validator": "^13.7.17",
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"cookies-next": "^4.1.1",
|
||||||
|
"dayjs": "^1.11.9",
|
||||||
|
"embla-carousel-react": "^8.0.0-rc10",
|
||||||
|
"mantine-react-table": "^1.0.0-beta.25",
|
||||||
|
"mobx": "^6.9.0",
|
||||||
|
"mobx-react-lite": "^3.4.3",
|
||||||
|
"mobx-utils": "^6.0.7",
|
||||||
|
"oidc-client-ts": "^2.2.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-oidc-context": "^2.2.2",
|
||||||
|
"react-router-dom": "^6.14.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"typescript": "^4.4.2",
|
||||||
|
"validator": "^13.9.0",
|
||||||
|
"web-vitals": "^2.1.0",
|
||||||
|
"zod": "^3.21.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
public/favicon.svg
Normal file
3
public/favicon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.5 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 933 B |
43
public/index.html
Normal file
43
public/index.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>Multi Frigate</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
public/logo.svg
Normal file
3
public/logo.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.5 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 933 B |
20
public/manifest.json
Normal file
20
public/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"short_name": "Multi Frigate",
|
||||||
|
"name": "Multi Frigate application",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.svg",
|
||||||
|
"sizes": "128x128 64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/svg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo.svg",
|
||||||
|
"type": "image/svg",
|
||||||
|
"sizes": "192x104"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
9
src/App.test.tsx
Normal file
9
src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
73
src/App.tsx
Normal file
73
src/App.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { hasAuthParams, useAuth } from 'react-oidc-context';
|
||||||
|
import CenterLoader from './shared/components/CenterLoader';
|
||||||
|
import { ColorScheme, ColorSchemeProvider, MantineProvider } from '@mantine/core';
|
||||||
|
import { useColorScheme } from '@mantine/hooks';
|
||||||
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import FullImageModal from './shared/components/FullImageModal';
|
||||||
|
import AppBody from './AppBody';
|
||||||
|
import FullProductModal from './shared/components/FullProductModal';
|
||||||
|
import Forbidden from './pages/403';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// const auth = useAuth();
|
||||||
|
const systemColorScheme = useColorScheme()
|
||||||
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme);
|
||||||
|
const toggleColorScheme = (value?: ColorScheme) => {
|
||||||
|
const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
|
||||||
|
setColorScheme(nextColorScheme)
|
||||||
|
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// automatically sign-in
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!hasAuthParams() &&
|
||||||
|
// !auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading) {
|
||||||
|
// auth.signinRedirect();
|
||||||
|
// }
|
||||||
|
// }, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect]);
|
||||||
|
|
||||||
|
// if (auth.activeNavigator || auth.isLoading) {
|
||||||
|
// return <CenterLoader />
|
||||||
|
// }
|
||||||
|
// if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) {
|
||||||
|
// return <Forbidden />
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||||
|
<MantineProvider
|
||||||
|
withGlobalStyles
|
||||||
|
withNormalizeCSS
|
||||||
|
theme={{
|
||||||
|
// fontFamily: '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji', //default system fonts
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultProps: {
|
||||||
|
radius: "xl",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Image: {
|
||||||
|
// styles: (theme) => ({
|
||||||
|
// placeholder: {
|
||||||
|
// backgroundColor: 'transparent',
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BrowserRouter>
|
||||||
|
<FullImageModal />
|
||||||
|
<FullProductModal />
|
||||||
|
<AppBody />
|
||||||
|
</BrowserRouter>
|
||||||
|
</MantineProvider >
|
||||||
|
</ColorSchemeProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default App;
|
||||||
49
src/AppBody.tsx
Normal file
49
src/AppBody.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { AppShell, useMantineTheme, } from "@mantine/core"
|
||||||
|
import LeftSideBar from './widgets/LeftSideBar';
|
||||||
|
import RightSideBar from './widgets/RightSideBar';
|
||||||
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
|
import { testHeaderLinks } from './widgets/header/header.links';
|
||||||
|
import AppRouter from './router/AppRouter';
|
||||||
|
import { SideBar } from './shared/components/SideBar';
|
||||||
|
|
||||||
|
const AppBody = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("render Main")
|
||||||
|
})
|
||||||
|
|
||||||
|
const [leftSideBar, setLeftSidebar] = useState(true)
|
||||||
|
const [rightSideBar, setRightSidebar] = useState(true)
|
||||||
|
|
||||||
|
|
||||||
|
const leftSideBarIsHidden = (isHidden: boolean) => {
|
||||||
|
setLeftSidebar(!isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
styles={{
|
||||||
|
main: {
|
||||||
|
paddingLeft: !leftSideBar ? "3em" : '',
|
||||||
|
paddingRight: !rightSideBar ? '3em' : '',
|
||||||
|
background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
navbarOffsetBreakpoint="sm"
|
||||||
|
asideOffsetBreakpoint="sm"
|
||||||
|
|
||||||
|
header={
|
||||||
|
<HeaderAction links={testHeaderLinks.links} />
|
||||||
|
}
|
||||||
|
navbar={
|
||||||
|
<SideBar isHidden={leftSideBarIsHidden} side="left" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AppRouter />
|
||||||
|
</AppShell>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppBody;
|
||||||
31
src/index.tsx
Normal file
31
src/index.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { createContext } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
import RootStore from './shared/stores/root.store';
|
||||||
|
import { AuthProvider } from 'react-oidc-context';
|
||||||
|
import { keycloakConfig } from './shared/services/keycloack';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(
|
||||||
|
document.getElementById('root') as HTMLElement
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootStore = new RootStore()
|
||||||
|
export const Context = createContext<RootStore>(rootStore)
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<Context.Provider value={rootStore}>
|
||||||
|
<AuthProvider {...keycloakConfig}>
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
</AuthProvider>
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
|
|
||||||
|
|
||||||
13
src/logo.svg
Normal file
13
src/logo.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg width="512" height="277" viewBox="0 0 512 277" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M351.453 165.267L402.027 143.76L418.889 165.473C418.889 165.473 406.253 179.683 386.04 184.964C365.827 190.244 354.501 189.967 354.501 189.967L351.453 165.267Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M321.38 121.518C324.882 122.531 329.473 125.572 331.891 126.766C348.753 122.36 364.36 116.878 372.668 114.621C367.339 109.906 360.442 108.215 358.858 108.198C357.397 108.089 332.3 113.679 320.571 117.686C314.202 119.862 313.929 119.363 321.38 121.518Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M306.169 127.001L346.299 184.218L344.499 159.372C338.763 148.148 324.865 121.467 306.169 127.001Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M378.198 225.866C383.685 233.536 398.657 236.423 405.677 232.702C405.726 232.675 389.133 224.867 382.934 218.379C377.374 217.91 378.198 225.866 378.198 225.866Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M445.604 197.955C446.616 204.839 446.4 209.827 441.471 213.297C441.431 213.323 442.661 201.7 435.765 190.463L445.604 197.955Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M344.583 201.589C344.583 201.589 323.043 173.304 318.428 169.827C327.752 186.35 342.505 205.849 342.505 205.849L344.583 201.589Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M355.983 164.849C355.983 164.849 347.005 146.683 333.491 131.653C346.999 150.99 353.168 169.884 353.168 169.884L355.983 164.849Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M402.399 153.932C402.399 153.932 399.469 151.291 396.606 146.753C396.112 145.966 400.26 145.514 399.708 144.651C393.115 134.317 382.598 123.353 376.077 116.926C384.531 122.639 393.097 130.838 403.102 143.45C411.347 153.841 417.852 164.458 417.852 164.458L402.399 153.932Z" fill="#E52129"/>
|
||||||
|
<path d="M355.031 240.153C351.637 241.249 346.344 234.272 343.214 224.567C340.084 214.863 340.299 206.109 343.696 205.013C347.089 203.918 352.383 210.895 355.513 220.6C358.646 230.303 358.428 239.059 355.031 240.153ZM302.042 164.988C298.806 165.498 295.001 158.413 293.543 149.163C292.089 139.916 293.534 132.004 296.767 131.496C300.006 130.985 303.809 138.07 305.266 147.318C306.721 156.568 305.278 164.477 302.042 164.988ZM439.338 194.568C441.24 197.376 443.086 200.166 444.74 202.833C448.046 210.085 446.707 218.621 441.416 225.131C434.741 233.339 423.627 237.756 419.149 239.247C406.85 243.854 391.594 243.597 382.789 239.298C390.682 247.987 406.035 247.993 419.449 246.777C407.859 250.736 393.869 251.395 383.461 246.247C375.911 242.515 367.624 236.031 362.709 229.691C361.921 226.181 359.561 216.575 358.258 213.75C351.58 199.232 339.611 194.465 337.648 207.852L308.024 163.055C311.614 159.112 310.402 141.565 305.999 133.848C301.597 126.13 295.81 126.447 295.81 126.447C295.81 126.447 295.755 124.066 299.285 120.566C302.812 117.066 345.78 105.107 351.795 106.82C352.816 107.18 353.813 107.564 354.825 107.934C347.662 90.3243 344.671 70.4126 341.981 42.2809C340.463 26.4195 337.836 11.7598 330.949 6.63118C316.038 -4.47922 299.77 -0.095113 284.929 8.5628C264.043 20.7337 217.421 71.1616 202.301 88.659C211.303 65.8416 216.954 35.7546 187.363 30.457H187.351C155.827 24.8039 126.59 57.6097 107.738 78.5379C76.6074 113.105 45.8408 152.871 0 175.49C25.5065 175.585 53.4734 175.069 102.375 130.908C133.284 103.001 162.438 68.3413 169.283 76.2157C174.052 81.2897 150.282 110.794 144.376 122.22L142.58 125.726C139.719 131.347 137.959 136.313 137.135 140.61C135.556 148.88 136.168 159.123 145.755 161.053C150.455 162 156.212 161.413 162.493 159.275L162.762 159.174C168.462 157.197 174.661 153.931 180.945 149.37C197.204 137.559 223.456 116.641 239.379 102.681C244.412 98.2683 249.29 93.9884 254.226 89.7477C264.343 81.0531 277.121 66.4476 282.508 65.335C287.529 64.1763 288.035 70.5026 286.992 76.4572C270.503 170.733 306.26 229.007 369.636 272.206C370.051 270.043 423.915 300.083 512 222.271C485.315 215.639 460.938 206.365 439.338 194.568Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M344.832 209.794C348.068 209.286 351.871 216.37 353.325 225.617C353.567 227.137 353.725 228.621 353.813 230.039C352.216 221.466 347.901 215.906 345.014 216.359C342.123 216.814 341.396 221.605 341.841 228.838C341.757 228.386 341.681 227.929 341.605 227.462C340.151 218.215 341.596 210.305 344.832 209.794Z" fill="#E52129"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M293.295 129.367C297.779 128.662 301.524 142.162 301.13 150.578C298.446 140.982 295.219 136.445 292.331 136.899C289.441 137.353 288.713 142.144 289.159 149.377C289.074 148.924 289.686 152.857 288.922 148.002C288.159 143.148 290.059 129.875 293.295 129.367Z" fill="#E52129"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
32
src/pages/403.tsx
Normal file
32
src/pages/403.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Button, Flex, Text } from '@mantine/core';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||||
|
import { strings } from '../shared/strings/strings';
|
||||||
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
|
import { pathRoutes } from '../router/routes.path';
|
||||||
|
import { Context } from '..';
|
||||||
|
|
||||||
|
const Forbidden = () => {
|
||||||
|
|
||||||
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sideBarsStore.setLeftSidebar(null)
|
||||||
|
sideBarsStore.setRightSidebar(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleGoToMain = () => {
|
||||||
|
window.location.replace(pathRoutes.MAIN_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
|
||||||
|
<Text fz='lg' fw={700}>{strings.errors[403]}</Text>
|
||||||
|
<CogWheelWithText text='403' />
|
||||||
|
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Forbidden;
|
||||||
32
src/pages/404.tsx
Normal file
32
src/pages/404.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Button, Flex, Text } from '@mantine/core';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||||
|
import { strings } from '../shared/strings/strings';
|
||||||
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
|
import { pathRoutes } from '../router/routes.path';
|
||||||
|
import { Context } from '..';
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
|
||||||
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sideBarsStore.setLeftSidebar(null)
|
||||||
|
sideBarsStore.setRightSidebar(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleGoToMain = () => {
|
||||||
|
window.location.replace(pathRoutes.MAIN_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
|
||||||
|
<Text fz='lg' fw={700}>{strings.errors[404]}</Text>
|
||||||
|
<CogWheelWithText text='404' />
|
||||||
|
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
11
src/pages/Admin.tsx
Normal file
11
src/pages/Admin.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const AdminPage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPage;
|
||||||
63
src/pages/MainBody.tsx
Normal file
63
src/pages/MainBody.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Container, Flex, Group, Text } from '@mantine/core';
|
||||||
|
import ProductTable, { TableAdapter } from '../shared/components/table.aps/ProductTable';
|
||||||
|
import HeadSearch from '../shared/components/HeadSearch';
|
||||||
|
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
||||||
|
import { useContext, useState, useEffect } from 'react';
|
||||||
|
import ProductGrid, { GridAdapter } from '../shared/components/grid.aps/ProductGrid';
|
||||||
|
import { getCookie, setCookie } from 'cookies-next';
|
||||||
|
import { Context } from '..';
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
|
|
||||||
|
const MainBody = observer(() => {
|
||||||
|
const { productStore, cartStore, sideBarsStore } = useContext(Context)
|
||||||
|
const { updateProductFromServer, products, isLoading: productsLoading } = productStore
|
||||||
|
const { updateCartFromServer, products: cartProducts, isLoading: cardLoading } = cartStore
|
||||||
|
|
||||||
|
const [viewState, setTableState] = useState(getCookie('aps-main-view') as SelectorViewState || SelectorViewState.GRID)
|
||||||
|
const handleToggleState = (state: SelectorViewState) => {
|
||||||
|
setCookie('aps-main-view', state, { maxAge: 60 * 60 * 24 * 30 });
|
||||||
|
setTableState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateProductFromServer()
|
||||||
|
updateCartFromServer()
|
||||||
|
sideBarsStore.setLeftSidebar(<div />)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (productsLoading || cardLoading) return <CenterLoader />
|
||||||
|
if (productsLoading || cardLoading) return <div>Error</div> // add state manager
|
||||||
|
|
||||||
|
|
||||||
|
let tableData: TableAdapter[] = []
|
||||||
|
let gridData: GridAdapter[] = []
|
||||||
|
if (products && cartProducts) {
|
||||||
|
tableData = productStore.mapToTable(products, cartProducts)
|
||||||
|
gridData = productStore.mapToGrid(products, cartProducts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction='column' h='100%'>
|
||||||
|
<Flex justify='space-between' align='center' w='100%'>
|
||||||
|
<Group
|
||||||
|
w='25%'
|
||||||
|
>
|
||||||
|
</Group>
|
||||||
|
<Group
|
||||||
|
w='50%'
|
||||||
|
style={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
><HeadSearch /></Group>
|
||||||
|
<Group
|
||||||
|
w='25%'
|
||||||
|
position="right">
|
||||||
|
<ViewSelector state={viewState} onChange={handleToggleState} />
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MainBody;
|
||||||
40
src/pages/RetryError.tsx
Normal file
40
src/pages/RetryError.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Flex, Button, Text } from '@mantine/core';
|
||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import { pathRoutes } from '../router/routes.path';
|
||||||
|
import { CogWheelHeartSVG } from '../shared/components/svg/CogWheelHeartSVG';
|
||||||
|
import { strings } from '../shared/strings/strings';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||||
|
import { Context } from '..';
|
||||||
|
|
||||||
|
const RetryError = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sideBarsStore.setLeftSidebar(null)
|
||||||
|
sideBarsStore.setRightSidebar(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleGoToMain = () => {
|
||||||
|
navigate(pathRoutes.MAIN_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRetry(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex h='100%' direction='column' justify='center' align='center' gap='1rem'>
|
||||||
|
<Text fz='lg' fw={700}>{strings.errors.somthengGoesWrong}</Text>
|
||||||
|
{ExclamationCogWheel}
|
||||||
|
<Text fz='lg' fw={700}>{strings.youCanRetryOrGoToMain}</Text>
|
||||||
|
<Button onClick={handleRetry}>{strings.retry}</Button>
|
||||||
|
<Button onClick={handleGoToMain}>{strings.goToMainPage}</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RetryError;
|
||||||
52
src/pages/Test.tsx
Normal file
52
src/pages/Test.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { Fragment, useContext, useEffect } from 'react';
|
||||||
|
import { Context } from '..';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import JSMpegPlayer from '../shared/components/JSMpegPlayer';
|
||||||
|
import { cameraLiveViewURL } from '../router/frigate.routes';
|
||||||
|
|
||||||
|
const Test = observer(() => {
|
||||||
|
// const { postStore } = useContext(Context)
|
||||||
|
// const { getPostsAction, posts } = postStore
|
||||||
|
|
||||||
|
// useEffect( () => {
|
||||||
|
// console.log("render Test")
|
||||||
|
// getPostsAction()
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
|
||||||
|
const test = {
|
||||||
|
camera: 'Buhgalteria',
|
||||||
|
host: 'localhost:5000',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||||
|
}
|
||||||
|
const test2 = {
|
||||||
|
camera: 'IT',
|
||||||
|
host: 'localhost:5000',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||||
|
}
|
||||||
|
const test3 = {
|
||||||
|
camera: 'Magazin1',
|
||||||
|
host: 'localhost:5001',
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(posts)
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div>
|
||||||
|
<JSMpegPlayer url={test.url()} camera={test.camera} width={test.width} height={test.height} />
|
||||||
|
<JSMpegPlayer url={test2.url()} camera={test2.camera} width={test2.width} height={test2.height} />
|
||||||
|
<JSMpegPlayer url={test3.url()} camera={test3.camera} width={test3.width} height={test3.height} />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Test;
|
||||||
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
17
src/router/AppRouter.tsx
Normal file
17
src/router/AppRouter.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import { routes } from "./routes";
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { pathRoutes } from "./routes.path";
|
||||||
|
|
||||||
|
const AppRouter = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{routes.map(({ path, component }) =>
|
||||||
|
<Route key={uuidv4()} path={path} element={component} />
|
||||||
|
)}
|
||||||
|
<Route key={uuidv4()} path="*" element={<Navigate to={pathRoutes.MAIN_PATH} replace />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRouter
|
||||||
5
src/router/frigate.routes.ts
Normal file
5
src/router/frigate.routes.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { hostURL } from "../shared/env.const"
|
||||||
|
|
||||||
|
export const cameraLiveViewURL = (host: string, cameraName: string) => {
|
||||||
|
return `ws://${hostURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${host}`
|
||||||
|
}
|
||||||
10
src/router/routes.path.ts
Normal file
10
src/router/routes.path.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const pathRoutes = {
|
||||||
|
MAIN_PATH: '/',
|
||||||
|
ADMIN_PATH: '/admin',
|
||||||
|
THANKS_PATH: '/thanks',
|
||||||
|
USER_DETAILED_PATH: '/user',
|
||||||
|
RETRY_ERROR_PATH: '/retry_error',
|
||||||
|
TEST_PATH: '/test',
|
||||||
|
FORBIDDEN_ERROR_PATH: '/403',
|
||||||
|
NOT_FOUND_ERROR_PATH: '/404',
|
||||||
|
}
|
||||||
40
src/router/routes.tsx
Normal file
40
src/router/routes.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {JSX} from "react";
|
||||||
|
import Test from "../pages/Test"
|
||||||
|
import MainBody from "../pages/MainBody";
|
||||||
|
import {pathRoutes} from "./routes.path";
|
||||||
|
import RetryError from "../pages/RetryError";
|
||||||
|
import Forbidden from "../pages/403";
|
||||||
|
import NotFound from "../pages/404";
|
||||||
|
import AdminPage from "../pages/Admin";
|
||||||
|
|
||||||
|
interface IRoute {
|
||||||
|
path: string,
|
||||||
|
component: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routes: IRoute[] = [
|
||||||
|
{ //todo delete
|
||||||
|
path: pathRoutes.TEST_PATH,
|
||||||
|
component: <Test />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.ADMIN_PATH,
|
||||||
|
component: <AdminPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.MAIN_PATH,
|
||||||
|
component: <MainBody />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.RETRY_ERROR_PATH,
|
||||||
|
component: <RetryError />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.FORBIDDEN_ERROR_PATH,
|
||||||
|
component: <Forbidden />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.NOT_FOUND_ERROR_PATH,
|
||||||
|
component: <NotFound />,
|
||||||
|
},
|
||||||
|
]
|
||||||
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
9
src/shared/components/CenterLoader.tsx
Normal file
9
src/shared/components/CenterLoader.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { DEFAULT_THEME, Loader, LoadingOverlay } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import СogwheelSVG from './svg/СogwheelSVG';
|
||||||
|
|
||||||
|
const CenterLoader = () => {
|
||||||
|
return <LoadingOverlay loader={СogwheelSVG} visible />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CenterLoader;
|
||||||
22
src/shared/components/CloseWithTooltip.tsx
Normal file
22
src/shared/components/CloseWithTooltip.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Tooltip, CloseButton, CloseButtonProps } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
|
||||||
|
|
||||||
|
interface CloseWithTooltipProps {
|
||||||
|
label: string
|
||||||
|
onClose?(): void
|
||||||
|
buttonProps?: CloseButtonProps
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const CloseWithTooltip = ({ label, onClose, buttonProps }: CloseWithTooltipProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={label} transitionProps={{ transition: 'slide-up', duration: 300 }} openDelay={200}>
|
||||||
|
<CloseButton {...buttonProps} onClick={onClose} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloseWithTooltip;
|
||||||
36
src/shared/components/CogWheelWithText.tsx
Normal file
36
src/shared/components/CogWheelWithText.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Center, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CogWheelWithTextProps {
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CogWheelWithText = ({ text }: CogWheelWithTextProps) => {
|
||||||
|
return (
|
||||||
|
<Center pos='relative'>
|
||||||
|
<svg
|
||||||
|
width={284}
|
||||||
|
height={269}
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M246.427 113.72l-15.039-34.325L251.6 56.8l-27.4-25.9-23.774 19.204-37.084-14.416L154.814 5H128.04l-8.657 31.095L83.15 50.532 59.8 30.9 32.4 56.8l19.911 23.166-14.808 34.414L5 121.55v25.9l32.895 8.489 15.27 34.242L32.4 212.2l27.4 25.9 24.539-18.903 35.7 13.882L128.3 264h27.4l8.282-30.909 36.313-14.215c6.053 4.089 23.905 19.224 23.905 19.224l27.4-25.9-20.332-22.67 15.042-34.336 32.689-8.039.001-25.605-32.573-7.83z"
|
||||||
|
stroke="#C92A2A"
|
||||||
|
strokeWidth={10}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<Text
|
||||||
|
pos='absolute'
|
||||||
|
top='50%'
|
||||||
|
left='50%'
|
||||||
|
sx = {{transform: 'translate(-50%, -50%)'}}
|
||||||
|
fz='3rem'
|
||||||
|
fw={800}>{text}</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CogWheelWithText;
|
||||||
23
src/shared/components/ColorSchemeToggle.tsx
Normal file
23
src/shared/components/ColorSchemeToggle.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useMantineColorScheme, useMantineTheme, Switch, MantineStyleSystemProps, DefaultProps } from '@mantine/core';
|
||||||
|
import { IconSun, IconMoonStars } from '@tabler/icons-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ColorSchemeToggleProps extends MantineStyleSystemProps, DefaultProps {}
|
||||||
|
|
||||||
|
|
||||||
|
const ColorSchemeToggle = ( props: ColorSchemeToggleProps ) => {
|
||||||
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
{...props}
|
||||||
|
checked={colorScheme === 'dark'}
|
||||||
|
onChange={() => toggleColorScheme()}
|
||||||
|
size="lg"
|
||||||
|
onLabel={<IconSun color={theme.white} size="1.25rem" stroke={1.5} />}
|
||||||
|
offLabel={<IconMoonStars color={theme.colors.gray[6]} size="1.25rem" stroke={1.5} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColorSchemeToggle;
|
||||||
13
src/shared/components/Currency.tsx
Normal file
13
src/shared/components/Currency.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text, TextProps } from '@mantine/core'
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
|
||||||
|
const Currency = (props: TextProps) => {
|
||||||
|
return (
|
||||||
|
<Text pl='0.2rem' fz="md" fw={500} {...props} >
|
||||||
|
{strings.currency}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Currency;
|
||||||
37
src/shared/components/DeliveryMethodRadio.tsx
Normal file
37
src/shared/components/DeliveryMethodRadio.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DeliveryMethods, DeliveryMethod } from '../stores/orders.store';
|
||||||
|
import { Radio, Group } from '@mantine/core';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
|
||||||
|
interface DeliveryMethodRadioProps {
|
||||||
|
deliveryAvailable: boolean
|
||||||
|
onChange(value: string): void
|
||||||
|
deliveryMethod?: DeliveryMethod
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeliveryMethodRadio = ( {deliveryAvailable, deliveryMethod, onChange, error}: DeliveryMethodRadioProps ) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
size='lg'
|
||||||
|
name="deliveryMethod"
|
||||||
|
label={strings.delivery}
|
||||||
|
description={strings.selectDeliveryMethod}
|
||||||
|
withAsterisk
|
||||||
|
onChange={onChange}
|
||||||
|
value={deliveryMethod}
|
||||||
|
error={error}
|
||||||
|
>
|
||||||
|
<Group mt="lg">
|
||||||
|
{deliveryAvailable ?
|
||||||
|
<Radio value={DeliveryMethods.Enum.delivery} label={strings.courierDelivery} />
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
<Radio value={DeliveryMethods.Enum.pickup} label={strings.pickUpByMyself} />
|
||||||
|
</Group>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeliveryMethodRadio;
|
||||||
43
src/shared/components/DeliveryPointRadio.tsx
Normal file
43
src/shared/components/DeliveryPointRadio.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Radio, Group } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { it } from 'node:test';
|
||||||
|
import { DeliveryPoint } from '../stores/user.store';
|
||||||
|
|
||||||
|
interface DeliveryPointRadioProps {
|
||||||
|
data: DeliveryItem[],
|
||||||
|
currentPoint?: DeliveryPoint,
|
||||||
|
onChange(pointId: string): void,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeliveryItem {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeliveryPointRadio = ( {data, currentPoint, onChange, error}:DeliveryPointRadioProps ) => {
|
||||||
|
|
||||||
|
const radios = data.map( item => (
|
||||||
|
<Radio key={item.id} value={item.id} label={item.name} />
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
size='lg'
|
||||||
|
name="deliveryPoints"
|
||||||
|
label={strings.deliveryPoint}
|
||||||
|
description={strings.selectYourDeliveryAddress}
|
||||||
|
withAsterisk
|
||||||
|
onChange={onChange}
|
||||||
|
defaultValue={currentPoint?.id}
|
||||||
|
error={error}
|
||||||
|
>
|
||||||
|
<Group mt="lg">
|
||||||
|
{radios}
|
||||||
|
</Group>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeliveryPointRadio;
|
||||||
110
src/shared/components/FullImageModal.tsx
Normal file
110
src/shared/components/FullImageModal.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Carousel, Embla, useAnimationOffsetEffect } from '@mantine/carousel';
|
||||||
|
import { Modal, createStyles, getStylesRef, rem } from '@mantine/core';
|
||||||
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import CardCarousel from './grid.aps/CardCarousel';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
|
|
||||||
|
// change to http://react-responsive-carousel.js.org/
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
modal: {
|
||||||
|
display: 'flex'
|
||||||
|
},
|
||||||
|
carousel: {
|
||||||
|
height: "100%",
|
||||||
|
flex: 1,
|
||||||
|
'&:hover': {
|
||||||
|
[`& .${getStylesRef('carouselControls')}`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselControls: {
|
||||||
|
ref: getStylesRef('carouselControls'),
|
||||||
|
transition: 'opacity 150ms ease',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselIndicator: {
|
||||||
|
width: rem(4),
|
||||||
|
height: rem(4),
|
||||||
|
transition: 'width 250ms ease',
|
||||||
|
|
||||||
|
'&[data-active]': {
|
||||||
|
width: rem(16),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface FullImageModalProps {
|
||||||
|
images?: string[]
|
||||||
|
opened?: boolean
|
||||||
|
open?(): void
|
||||||
|
close?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullImageModal = observer(({ images, opened, open, close }: FullImageModalProps) => {
|
||||||
|
const { modalStore } = useContext(Context)
|
||||||
|
const { isFullImageOpened, fullImageData, closeFullImage } = modalStore
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
const TRANSITION_DURATION = 100
|
||||||
|
const [embla, setEmbla] = useState<Embla | null>(null)
|
||||||
|
useAnimationOffsetEffect(embla, TRANSITION_DURATION)
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery(dimensions.mobileSize)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
closeFullImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const slides = fullImageData.length > 0
|
||||||
|
?
|
||||||
|
fullImageData.map((image) => (
|
||||||
|
<CardCarousel
|
||||||
|
key={uuidv4()}
|
||||||
|
image={image}
|
||||||
|
ratio={1}
|
||||||
|
height="100%"
|
||||||
|
/>))
|
||||||
|
:
|
||||||
|
<CardCarousel
|
||||||
|
key={uuidv4()}
|
||||||
|
image=''
|
||||||
|
ratio={1}
|
||||||
|
height="100%" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size="55%"
|
||||||
|
opened={isFullImageOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
withCloseButton={true}
|
||||||
|
centered
|
||||||
|
fullScreen={isMobile}
|
||||||
|
className={classes.modal}
|
||||||
|
transitionProps={{ duration: TRANSITION_DURATION }}
|
||||||
|
>
|
||||||
|
<Carousel
|
||||||
|
getEmblaApi={setEmbla}
|
||||||
|
withIndicators
|
||||||
|
loop
|
||||||
|
classNames={{
|
||||||
|
root: classes.carousel,
|
||||||
|
controls: classes.carouselControls,
|
||||||
|
indicator: classes.carouselIndicator,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{slides}
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FullImageModal;
|
||||||
134
src/shared/components/FullProductModal.tsx
Normal file
134
src/shared/components/FullProductModal.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import CardCarousel from './grid.aps/CardCarousel';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { Carousel, Embla, useAnimationOffsetEffect } from '@mantine/carousel';
|
||||||
|
import { Modal, createStyles, getStylesRef, rem, Text, Box, Flex, Grid, Divider, Center } from '@mantine/core';
|
||||||
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import KomponentLoader from './СogwheelLoader';
|
||||||
|
import ProductParameter from './ProductParameter';
|
||||||
|
import { productString } from '../strings/product.strings';
|
||||||
|
import { IconArrowBadgeLeft, IconArrowBadgeRight } from '@tabler/icons-react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
modal: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
},
|
||||||
|
carousel: {
|
||||||
|
flex: 1,
|
||||||
|
'&:hover': {
|
||||||
|
[`& .${getStylesRef('carouselControls')}`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselControls: {
|
||||||
|
ref: getStylesRef('carouselControls'),
|
||||||
|
transition: 'opacity 150ms ease',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselIndicator: {
|
||||||
|
width: rem(4),
|
||||||
|
height: rem(4),
|
||||||
|
transition: 'width 250ms ease',
|
||||||
|
|
||||||
|
'&[data-active]': {
|
||||||
|
width: rem(16),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const FullProductModal = observer(() => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const { modalStore } = useContext(Context)
|
||||||
|
const { productDetailed, isProductDetailedOpened, closeProductDetailed } = modalStore
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery(dimensions.mobileSize)
|
||||||
|
|
||||||
|
const TRANSITION_DURATION = 100
|
||||||
|
const [embla, setEmbla] = useState<Embla | null>(null)
|
||||||
|
useAnimationOffsetEffect(embla, TRANSITION_DURATION)
|
||||||
|
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
closeProductDetailed()
|
||||||
|
}
|
||||||
|
|
||||||
|
const slides = productDetailed.data && productDetailed.data.image.length > 0
|
||||||
|
?
|
||||||
|
productDetailed.data.image.map((image) => (
|
||||||
|
<CardCarousel
|
||||||
|
key={uuidv4()}
|
||||||
|
image={image}
|
||||||
|
ratio={1}
|
||||||
|
height="100%"
|
||||||
|
/>))
|
||||||
|
: null
|
||||||
|
|
||||||
|
const properties = productDetailed.data?.properties?.map( property => (
|
||||||
|
<ProductParameter key={property.id} paramName={property.name} paramValue={property.value} />
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
// size='auto'
|
||||||
|
size='45%'
|
||||||
|
opened={isProductDetailedOpened}
|
||||||
|
onClose={handleClose}
|
||||||
|
withCloseButton={true}
|
||||||
|
centered
|
||||||
|
fullScreen={isMobile}
|
||||||
|
className={classes.modal}
|
||||||
|
transitionProps={{ duration: TRANSITION_DURATION }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
productDetailed.isLoading ?
|
||||||
|
<KomponentLoader />
|
||||||
|
:
|
||||||
|
<Center>
|
||||||
|
<Flex w="100%" direction='column' align='center'>
|
||||||
|
{/* <Flex h='40rem' w='30rem' direction='column' align='center'> */}
|
||||||
|
{/* <> */}
|
||||||
|
<Carousel
|
||||||
|
w='60%' // change image size
|
||||||
|
getEmblaApi={setEmbla}
|
||||||
|
withIndicators
|
||||||
|
loop
|
||||||
|
previousControlIcon={<IconArrowBadgeLeft />}
|
||||||
|
nextControlIcon={<IconArrowBadgeRight />}
|
||||||
|
controlSize={40}
|
||||||
|
classNames={{
|
||||||
|
root: classes.carousel,
|
||||||
|
controls: classes.carouselControls,
|
||||||
|
indicator: classes.carouselIndicator,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{slides}
|
||||||
|
</Carousel>
|
||||||
|
<Grid mt="1rem">
|
||||||
|
{/* Base product parameters */}
|
||||||
|
<ProductParameter paramName={productString.name} paramValue={productDetailed.data?.name} />
|
||||||
|
<ProductParameter paramName={productString.number} paramValue={productDetailed.data?.number} />
|
||||||
|
<ProductParameter paramName={productString.oem} paramValue={productDetailed.data?.oem} />
|
||||||
|
<ProductParameter paramName={productString.stock} paramValue={productDetailed.data?.stock} />
|
||||||
|
<ProductParameter paramName={productString.discount} paramValue={productDetailed.data?.discount? strings.true : strings.false} />
|
||||||
|
<Divider w='100%' size="sm" />
|
||||||
|
{/* Product properties */}
|
||||||
|
{properties}
|
||||||
|
{/* {productDetailed.data?.properties? JSON.stringify(productDetailed.data?.properties) : null} */}
|
||||||
|
</Grid>
|
||||||
|
</Flex>
|
||||||
|
</Center>
|
||||||
|
// {/* </> */}
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FullProductModal;
|
||||||
26
src/shared/components/HeadSearch.tsx
Normal file
26
src/shared/components/HeadSearch.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Flex, TextInput } from '@mantine/core';
|
||||||
|
import { IconSearch } from '@tabler/icons-react';
|
||||||
|
import React from 'react';
|
||||||
|
import ViewSelector from './ViewSelector';
|
||||||
|
|
||||||
|
interface HeadSearchProps {
|
||||||
|
search?: string
|
||||||
|
handleSearchChange?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeadSearch = ({ search, handleSearchChange }: HeadSearchProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
maw={400}
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
placeholder="Search..."
|
||||||
|
icon={<IconSearch size="0.9rem" stroke={1.5} />}
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeadSearch;
|
||||||
23
src/shared/components/ImageWithPlaceHolder.tsx
Normal file
23
src/shared/components/ImageWithPlaceHolder.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ImageProps, Image, Center } from '@mantine/core';
|
||||||
|
import { IconPhotoOff } from '@tabler/icons-react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const ImageWithPlaceHolder = (props: ImageProps & React.RefAttributes<HTMLDivElement>) => {
|
||||||
|
if (props.src) return (
|
||||||
|
<Image {...props} />
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Center
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}>
|
||||||
|
<IconPhotoOff
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageWithPlaceHolder
|
||||||
90
src/shared/components/InputModal.tsx
Normal file
90
src/shared/components/InputModal.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core';
|
||||||
|
import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks';
|
||||||
|
import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { IconAlertCircle, IconX } from '@tabler/icons-react';
|
||||||
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
rightSection: {
|
||||||
|
width: '3rem',
|
||||||
|
marginRight: '0.2rem',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface InputModalProps {
|
||||||
|
inValue: number
|
||||||
|
putValue?(value: number): void
|
||||||
|
opened: boolean
|
||||||
|
open(): void
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps) => {
|
||||||
|
const { classes } = useStyles()
|
||||||
|
const [value, setValue] = useState(inValue)
|
||||||
|
const isMobile = useMediaQuery(dimensions.mobileSize)
|
||||||
|
|
||||||
|
const refInput: React.LegacyRef<HTMLInputElement> = useRef(null)
|
||||||
|
|
||||||
|
const handeLoaded = (event: FocusEvent<HTMLInputElement, Element>) => {
|
||||||
|
event.target.select()
|
||||||
|
}
|
||||||
|
const handeClear = () => {
|
||||||
|
setValue(0)
|
||||||
|
refInput.current?.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetValue = (value: number | "") => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
setValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (putValue) putValue(value)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={handleClose}
|
||||||
|
withCloseButton={false}
|
||||||
|
centered
|
||||||
|
fullScreen={isMobile}
|
||||||
|
>
|
||||||
|
<Flex justify="space-between">
|
||||||
|
<div>{strings.enterQuantity}</div>
|
||||||
|
<CloseButton size="lg" onClick={handleClose} />
|
||||||
|
</Flex>
|
||||||
|
<NumberInput
|
||||||
|
ref={refInput}
|
||||||
|
classNames={classes}
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={handleSetValue}
|
||||||
|
data-autofocus
|
||||||
|
placeholder={strings.quantity}
|
||||||
|
hideControls
|
||||||
|
min={0}
|
||||||
|
onFocus={handeLoaded}
|
||||||
|
onKeyDown={
|
||||||
|
getHotkeyHandler([
|
||||||
|
['Enter', handleClose]
|
||||||
|
])
|
||||||
|
}
|
||||||
|
rightSection={ // value.toString().length > 0 ? <ActionIcon onClick={(event) => handeClear()}><IconX size="1.4rem" /></ActionIcon> : null // todo move to textinput
|
||||||
|
<Flex w='100%' h='100%' justify='right' align='center'>
|
||||||
|
<Tooltip label={strings.tooltip_close} position="top-end" withArrow>
|
||||||
|
<div>
|
||||||
|
<IconAlertCircle size="1.4rem" style={{ display: 'block', opacity: 0.5 }} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputModal;
|
||||||
100
src/shared/components/JSMpegPlayer.tsx
Normal file
100
src/shared/components/JSMpegPlayer.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// @ts-ignore we know this doesn't have types
|
||||||
|
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
||||||
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { useResizeObserver } from "../utils/resize-observer";
|
||||||
|
|
||||||
|
type JSMpegPlayerProps = {
|
||||||
|
className?: string;
|
||||||
|
url: string;
|
||||||
|
camera: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function JSMpegPlayer({
|
||||||
|
camera,
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
className,
|
||||||
|
}: JSMpegPlayerProps) {
|
||||||
|
const playerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
|
useResizeObserver(containerRef);
|
||||||
|
|
||||||
|
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
|
||||||
|
// https://github.com/blakeblackshear/frigate/issues/1657
|
||||||
|
let scrollBarWidth = 0;
|
||||||
|
if (window.innerWidth && document.body.offsetWidth) {
|
||||||
|
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
|
||||||
|
}
|
||||||
|
const availableWidth = scrollBarWidth
|
||||||
|
? containerWidth + scrollBarWidth
|
||||||
|
: containerWidth;
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
|
||||||
|
const scaledHeight = useMemo(() => {
|
||||||
|
const scaledHeight = Math.floor(availableWidth / aspectRatio);
|
||||||
|
const finalHeight = Math.min(scaledHeight, height);
|
||||||
|
|
||||||
|
if (containerHeight < finalHeight) {
|
||||||
|
return containerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalHeight > 0) {
|
||||||
|
return finalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}, [availableWidth, aspectRatio, height]);
|
||||||
|
const scaledWidth = useMemo(
|
||||||
|
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
|
||||||
|
[scaledHeight, aspectRatio, scrollBarWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!playerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const video = new JSMpeg.VideoElement(
|
||||||
|
playerRef.current,
|
||||||
|
url,
|
||||||
|
{},
|
||||||
|
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const fullscreen = () => {
|
||||||
|
if (video.els.canvas.webkitRequestFullScreen) {
|
||||||
|
video.els.canvas.webkitRequestFullScreen();
|
||||||
|
} else {
|
||||||
|
video.els.canvas.mozRequestFullScreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
video.els.canvas.addEventListener("click", fullscreen);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (playerRef.current) {
|
||||||
|
try {
|
||||||
|
video.destroy();
|
||||||
|
} catch (e) {}
|
||||||
|
playerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} ref={containerRef}>
|
||||||
|
<div
|
||||||
|
ref={playerRef}
|
||||||
|
className={`jsmpeg`}
|
||||||
|
style={{
|
||||||
|
height: `${600}px`,
|
||||||
|
width: `${800}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/shared/components/Logo.tsx
Normal file
12
src/shared/components/Logo.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Image, ImageProps } from '@mantine/core';
|
||||||
|
|
||||||
|
const Logo = ({ onClick }: ImageProps) => {
|
||||||
|
const src = "../logo.svg"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image sx={{ cursor: "pointer" }} height={40} alt='Logo' withPlaceholder src={src} onClick={onClick} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
36
src/shared/components/OrderStepper.tsx
Normal file
36
src/shared/components/OrderStepper.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Flex, Stepper } from '@mantine/core';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { PaymentMethod, PaymentMethods } from '../stores/orders.store';
|
||||||
|
|
||||||
|
const OrderStepper = observer(() => {
|
||||||
|
|
||||||
|
const { sideBarsStore, cartStore } = useContext(Context)
|
||||||
|
|
||||||
|
const { confirmedStage, paymentMethod } = cartStore
|
||||||
|
|
||||||
|
const stage = confirmedStage ? confirmedStage.stage + 1 : 0
|
||||||
|
|
||||||
|
const pb = '5rem'
|
||||||
|
return (
|
||||||
|
<Flex direction='column' justify='center' h='100%'>
|
||||||
|
<Stepper
|
||||||
|
h='20rem'
|
||||||
|
active={stage}
|
||||||
|
orientation="vertical"
|
||||||
|
allowNextStepsSelect={false}
|
||||||
|
>
|
||||||
|
<Stepper.Step pb={pb} label={strings.cart} description={strings.confirmOrder} />
|
||||||
|
<Stepper.Step pb={pb} label={strings.orderParams} description={strings.chooseParams} />
|
||||||
|
{paymentMethod.data === PaymentMethods.Enum.Online ?
|
||||||
|
<Stepper.Step pb={pb} label={strings.payment} description={strings.inputPaymentValues} />
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</Stepper>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default OrderStepper;
|
||||||
73
src/shared/components/OrderTotals.tsx
Normal file
73
src/shared/components/OrderTotals.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Button, Divider, Flex, Text } from '@mantine/core';
|
||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import PriceText from './PriceText';
|
||||||
|
import Currency from './Currency';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const OrderTotals = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { cartStore } = useContext(Context)
|
||||||
|
const { isLoading, products, totalWeight, totalSum,
|
||||||
|
currentStage, confirmStage, confirmedStage, CartStages } = cartStore
|
||||||
|
|
||||||
|
const sizeBetween = '0.5rem'
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
const maxIndex = cartStore.CartStages.length - 1
|
||||||
|
const validate = confirmStage(currentStage)
|
||||||
|
if (currentStage.stage === maxIndex) {
|
||||||
|
console.log("currentStage.stage === maxIndex")
|
||||||
|
return
|
||||||
|
if (validate) {
|
||||||
|
// todo send to server
|
||||||
|
// navigate to main
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentStage.stage < maxIndex) {
|
||||||
|
const nextStageIndex = currentStage.stage + 1
|
||||||
|
console.log("navigate")
|
||||||
|
if (validate) {
|
||||||
|
navigate(cartStore.CartStages[nextStageIndex].path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backButton = () => {
|
||||||
|
if (currentStage.stage === 0) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return <Button onClick={() => navigate(-1)}>{strings.back}</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
const okButton = () => {
|
||||||
|
const lastStageIndex = CartStages.length - 1
|
||||||
|
if (confirmedStage?.stage === CartStages[lastStageIndex].stage) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button onClick={handleConfirm} mb={sizeBetween}>{strings.confirm}</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction='column' h='100%' justify='center' align='center' gap='0.2rem'>
|
||||||
|
<Text weight={700}>{strings.summary}</Text>
|
||||||
|
<Divider pb={sizeBetween} w='100%' />
|
||||||
|
<Text >{strings.positions}</Text>
|
||||||
|
<Text>{products.length}</Text>
|
||||||
|
<Divider pb={sizeBetween} w='100%' />
|
||||||
|
<Text>{strings.weight}</Text>
|
||||||
|
<Text >{totalWeight}</Text>
|
||||||
|
<Divider pb={sizeBetween} w='100%' />
|
||||||
|
<Text tt="uppercase">{strings.total}</Text>
|
||||||
|
<Flex pb={sizeBetween}><PriceText value={totalSum} /><Currency /></Flex>
|
||||||
|
<Divider pb={sizeBetween} w='100%' />
|
||||||
|
{okButton()}
|
||||||
|
{backButton()}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderTotals;
|
||||||
34
src/shared/components/PaymentMehodRadio.tsx
Normal file
34
src/shared/components/PaymentMehodRadio.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Radio, Group } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { PaymentMethod, PaymentMethods } from '../stores/orders.store';
|
||||||
|
|
||||||
|
interface PaymentMehodRadioProps {
|
||||||
|
onChange(value: PaymentMethod): void
|
||||||
|
currentValue?: PaymentMethod
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentMehodRadio = ( {onChange, currentValue, error}: PaymentMehodRadioProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
size='lg'
|
||||||
|
name="paymentMethod"
|
||||||
|
label={strings.paymentMethod}
|
||||||
|
description={strings.selectPaymentMethod}
|
||||||
|
withAsterisk
|
||||||
|
onChange={onChange}
|
||||||
|
value={currentValue}
|
||||||
|
error={error}
|
||||||
|
>
|
||||||
|
<Group mt="lg">
|
||||||
|
<Radio value={PaymentMethods.Enum.Cash} label={strings.cashToCourier} />
|
||||||
|
<Radio value={PaymentMethods.Enum.BankTransfer} label={strings.bankTransfer} />
|
||||||
|
<Radio value={PaymentMethods.Enum.Online} label={strings.onlineByCard} />
|
||||||
|
</Group>
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentMehodRadio;
|
||||||
24
src/shared/components/PriceText.tsx
Normal file
24
src/shared/components/PriceText.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Text, TextProps, createStyles } from '@mantine/core';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
price: {
|
||||||
|
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface PriceTextProps extends TextProps {
|
||||||
|
value: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PriceText = (props: PriceTextProps, ) => {
|
||||||
|
const { classes } = useStyles()
|
||||||
|
return (
|
||||||
|
<Text {...props} className={classes.price}>
|
||||||
|
{Intl.NumberFormat().format(props.value)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PriceText;
|
||||||
27
src/shared/components/ProductParameter.tsx
Normal file
27
src/shared/components/ProductParameter.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Divider, Flex, Grid, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ProductParameterProps {
|
||||||
|
paramName?: string | number
|
||||||
|
paramValue?: string | number | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductParameter = ({paramName, paramValue}: ProductParameterProps) => {
|
||||||
|
|
||||||
|
// if (!paramValue) return null
|
||||||
|
|
||||||
|
const pl='1rem', pr='0.2rem', pt='0.1rem', pb='0.2rem'
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid.Col pl={pl} pr={pr} pt={pt} pb={pb} span={6}>
|
||||||
|
<Text fw={500}>{paramName}</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col pl={pl} pr={pr} pt={pt} pb={pb} span={6}>
|
||||||
|
<Text>{paramValue}</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
<Divider w='100%' size="xs" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductParameter;
|
||||||
115
src/shared/components/SideBar.tsx
Normal file
115
src/shared/components/SideBar.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React, { FC, JSX, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Aside, Button, createStyles, Navbar } from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import { useMantineSize } from '../utils/mantine.size.convertor';
|
||||||
|
import { SideButton } from './SideButton';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
export interface SideBarProps {
|
||||||
|
isHidden: (isHidden: boolean) => void,
|
||||||
|
side: 'left' | 'right',
|
||||||
|
children?: React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme,
|
||||||
|
{ visible }: { visible: boolean }) => ({
|
||||||
|
navbar: {
|
||||||
|
transition: 'transform 0.3s ease-in-out',
|
||||||
|
transform: visible ? 'translateX(0)' : 'translateX(-100%)',
|
||||||
|
},
|
||||||
|
|
||||||
|
aside: {
|
||||||
|
transition: 'transform 0.3s ease-in-out',
|
||||||
|
transform: visible ? 'translateX(0)' : 'translateX(+100%)',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
export const SideBar = observer(({ isHidden, side, children }: SideBarProps) => {
|
||||||
|
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
||||||
|
const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx);
|
||||||
|
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null)
|
||||||
|
|
||||||
|
const { classes } = useStyles({ visible })
|
||||||
|
|
||||||
|
const handleClickVisible = (state: boolean) => {
|
||||||
|
manualVisible.current = state
|
||||||
|
if (state) open()
|
||||||
|
else close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
|
const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
|
||||||
|
if (children && side === 'left') return children
|
||||||
|
else if (sideBarsStore.leftSideBar) return sideBarsStore.leftSideBar
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
|
||||||
|
if (children && side === 'right') return children
|
||||||
|
else if (sideBarsStore.rightSideBar) return sideBarsStore.rightSideBar
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
setLeftChildren(sideBarsStore.leftSideBar)
|
||||||
|
}, [sideBarsStore.leftSideBar])
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
setRightChildren(sideBarsStore.rightSideBar)
|
||||||
|
}, [sideBarsStore.rightSideBar])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isHidden(!visible)
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
}, [manualVisible.current])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkWindowSize = () => {
|
||||||
|
if (window.innerWidth <= hideSizePx && visible) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
if (window.innerWidth > hideSizePx && !visible && manualVisible.current === null) {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', checkWindowSize);
|
||||||
|
|
||||||
|
// Cleanup function to remove event listener
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', checkWindowSize);
|
||||||
|
}
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
side === 'left' ?
|
||||||
|
<Navbar
|
||||||
|
className={classes.navbar}
|
||||||
|
p={dimensions.hideSidebarsSize}
|
||||||
|
width={{ sm: 200, lg: 300, }}
|
||||||
|
>
|
||||||
|
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button>
|
||||||
|
{leftChildren}
|
||||||
|
</Navbar>
|
||||||
|
:
|
||||||
|
<Aside
|
||||||
|
className={classes.aside}
|
||||||
|
p={dimensions.hideSidebarsSize}
|
||||||
|
width={{ sm: 200, lg: 300 }}>
|
||||||
|
<Button onClick={() => handleClickVisible(false)}>{strings.hide}</Button>
|
||||||
|
{rightChildren}
|
||||||
|
</Aside>
|
||||||
|
}
|
||||||
|
|
||||||
|
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
13
src/shared/components/SideBarLoader.tsx
Normal file
13
src/shared/components/SideBarLoader.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Box, LoadingOverlay } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import СogwheelSVG from './svg/СogwheelSVG';
|
||||||
|
|
||||||
|
const SideBarLoader = () => {
|
||||||
|
return (
|
||||||
|
<Box pos='fixed' top='3.2rem' h='100%' w='95%'>
|
||||||
|
<LoadingOverlay h='100%' children loader={СogwheelSVG} visible={true} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideBarLoader;
|
||||||
67
src/shared/components/SideButton.tsx
Normal file
67
src/shared/components/SideButton.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import {IconArrowBadgeLeft, IconArrowBadgeRight} from "@tabler/icons-react";
|
||||||
|
import {Button, ButtonProps, createStyles} from "@mantine/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param side left or right
|
||||||
|
*/
|
||||||
|
interface SideButtonProps extends ButtonProps {
|
||||||
|
side: 'left' | 'right',
|
||||||
|
onClick?: () => void,
|
||||||
|
hide?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Styles {
|
||||||
|
side: 'left' | 'right',
|
||||||
|
hide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHide = (side:'left' | 'right', hide?:boolean) => {
|
||||||
|
if (hide) {
|
||||||
|
if (side === 'left') return 'translateX(-100%)'
|
||||||
|
else return 'translateX(+100%)'
|
||||||
|
}
|
||||||
|
return 'translateX(0)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme, {side, hide}: Styles ) => ({
|
||||||
|
side_button: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
listStyle: "none",
|
||||||
|
margin: "0",
|
||||||
|
padding: "0",
|
||||||
|
paddingLeft: side === 'left' ? "0.7em" : '',
|
||||||
|
paddingRight: side === 'right' ? "0.7em" : '',
|
||||||
|
position: "fixed",
|
||||||
|
left: side === 'left' ? '-1em' : '',
|
||||||
|
right: side === 'right' ? '-1em' : '',
|
||||||
|
top: "50%",
|
||||||
|
height: "5em",
|
||||||
|
width: "2.5em",
|
||||||
|
transition: 'transform 0.3s ease-in-out',
|
||||||
|
transform: handleHide(side,hide),
|
||||||
|
|
||||||
|
ul: {
|
||||||
|
paddingLeft: "0em"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const SideButton = (props: SideButtonProps) => {
|
||||||
|
const {classes, cx} = useStyles({side: props.side, hide: props.hide})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
radius="lg"
|
||||||
|
className={cx(classes.side_button, props.className,)}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li> {props.side === 'left' ? <IconArrowBadgeRight/> : <IconArrowBadgeLeft/>}</li>
|
||||||
|
<li>{props.side === 'left' ? <IconArrowBadgeRight/> : <IconArrowBadgeLeft/>}</li>
|
||||||
|
</ul>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
30
src/shared/components/TreeLink.tsx
Normal file
30
src/shared/components/TreeLink.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { NavLink } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TreeLinkProps {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
selected: boolean,
|
||||||
|
opened?: boolean,
|
||||||
|
onClick(selectedId: string): void,
|
||||||
|
children?: (JSX.Element | undefined)[]
|
||||||
|
}
|
||||||
|
const TreeLink = ({id, label, selected, opened, onClick, children}: TreeLinkProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
opened={opened}
|
||||||
|
pt='2px'
|
||||||
|
pb='2px'
|
||||||
|
active={selected}
|
||||||
|
label={label}
|
||||||
|
childrenOffset={18}
|
||||||
|
onClick={() => onClick(id)}
|
||||||
|
defaultOpened={false}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeLink;
|
||||||
73
src/shared/components/UserMenu.tsx
Normal file
73
src/shared/components/UserMenu.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Avatar, createStyles, Group, Menu, UnstyledButton, Text, Button, Flex } from "@mantine/core";
|
||||||
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
import { keycloakConfig } from '../services/keycloack';
|
||||||
|
import { strings } from '../strings/strings';
|
||||||
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
|
import ColorSchemeToggle from './ColorSchemeToggle';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface UserMenuProps {
|
||||||
|
user: { name: string; image: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserMenu = ({ user }: UserMenuProps) => {
|
||||||
|
const [userMenuOpened, setUserMenuOpened] = useState(false);
|
||||||
|
const auth = useAuth()
|
||||||
|
const isMiddleScreen = useMediaQuery(dimensions.middleScreenSize)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleAboutMe = () => {
|
||||||
|
navigate(`USER_DETAILED_PATH`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await auth.removeUser()
|
||||||
|
const id_token_hint = auth.user?.id_token
|
||||||
|
await auth.signoutRedirect({ post_logout_redirect_uri: keycloakConfig.redirect_uri, id_token_hint: id_token_hint })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
width={260}
|
||||||
|
transitionProps={{ transition: 'pop-top-right' }}
|
||||||
|
onClose={() => setUserMenuOpened(false)}
|
||||||
|
onOpen={() => setUserMenuOpened(true)}
|
||||||
|
withinPortal
|
||||||
|
>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button variant="subtle" uppercase pl={0}>
|
||||||
|
<Group spacing={7}>
|
||||||
|
<Avatar src={user.image} alt={user.name} radius="xl" size={33} mr={5} />
|
||||||
|
<Text weight={600} size="sm" sx={{ lineHeight: 1 }} mr={3}>
|
||||||
|
{user.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{
|
||||||
|
isMiddleScreen ?
|
||||||
|
<Flex w='100%' justify='space-between' align='center'>
|
||||||
|
<Text fz='sm' ml='0.7rem'>{strings.changeTheme}</Text>
|
||||||
|
<ColorSchemeToggle />
|
||||||
|
</Flex>
|
||||||
|
:
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
<Menu.Item>
|
||||||
|
{strings.settings}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item onClick={handleAboutMe}>
|
||||||
|
{strings.aboutMe}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item onClick={handleLogout}>
|
||||||
|
{strings.logout}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserMenu;
|
||||||
45
src/shared/components/ViewSelector.tsx
Normal file
45
src/shared/components/ViewSelector.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Center, SegmentedControl } from '@mantine/core';
|
||||||
|
import { IconColumns, IconLayoutGrid } from '@tabler/icons-react';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export enum SelectorViewState {
|
||||||
|
TABLE = "table",
|
||||||
|
GRID = "grid",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewSelectorProps {
|
||||||
|
state: SelectorViewState
|
||||||
|
onChange(state: SelectorViewState): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewSelector = ({state, onChange} : ViewSelectorProps) => {
|
||||||
|
|
||||||
|
const handleToggle = ( value: string ) => {
|
||||||
|
onChange(value as SelectorViewState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentedControl
|
||||||
|
value={state}
|
||||||
|
onChange={handleToggle}
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
value: SelectorViewState.TABLE,
|
||||||
|
label: (
|
||||||
|
<Center>
|
||||||
|
<IconColumns />
|
||||||
|
</Center>
|
||||||
|
)},
|
||||||
|
{
|
||||||
|
value: SelectorViewState.GRID,
|
||||||
|
label: (
|
||||||
|
<Center>
|
||||||
|
<IconLayoutGrid />
|
||||||
|
</Center>
|
||||||
|
)},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewSelector;
|
||||||
53
src/shared/components/filters.aps/MultiSelectFilter.tsx
Normal file
53
src/shared/components/filters.aps/MultiSelectFilter.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { SystemProp, SpacingValue, Box, Flex, CloseButton, MultiSelect, SelectItem, MultiSelectProps, Text, Tooltip } from '@mantine/core';
|
||||||
|
import React, { CSSProperties, useState } from 'react';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
import CloseWithTooltip from '../CloseWithTooltip';
|
||||||
|
|
||||||
|
interface MultiSelectFilterProps {
|
||||||
|
id: string
|
||||||
|
data: SelectItem[]
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
label?: string
|
||||||
|
defaultValue?: string[]
|
||||||
|
textClassName?: string
|
||||||
|
selectProps?: MultiSelectProps,
|
||||||
|
display?: SystemProp<CSSProperties['display']>
|
||||||
|
showClose?: boolean,
|
||||||
|
changedState?(id: string, value: string[]): void
|
||||||
|
onClose?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelectFilter = ({
|
||||||
|
id, data, spaceBetween,
|
||||||
|
label, defaultValue, textClassName,
|
||||||
|
selectProps, display, showClose, changedState, onClose
|
||||||
|
}: MultiSelectFilterProps) => {
|
||||||
|
|
||||||
|
const handleOnChange = (value: string[]) => {
|
||||||
|
if (changedState) {
|
||||||
|
changedState(id, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex justify='space-between'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
{showClose ?
|
||||||
|
<CloseWithTooltip label={strings.hide} onClose={onClose} />
|
||||||
|
: null}
|
||||||
|
</Flex>
|
||||||
|
<MultiSelect
|
||||||
|
{...selectProps}
|
||||||
|
mt={spaceBetween}
|
||||||
|
data={data}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultiSelectFilter;
|
||||||
52
src/shared/components/filters.aps/OneSelectFilter.tsx
Normal file
52
src/shared/components/filters.aps/OneSelectFilter.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { SelectItem, SystemProp, SpacingValue, SelectProps, Box, Flex, CloseButton, Text, Select } from '@mantine/core';
|
||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
import CloseWithTooltip from '../CloseWithTooltip';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
interface OneSelectFilterProps {
|
||||||
|
id: string
|
||||||
|
data: SelectItem[]
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
label?: string
|
||||||
|
defaultValue?: string
|
||||||
|
textClassName?: string
|
||||||
|
selectProps?: SelectProps,
|
||||||
|
display?: SystemProp<CSSProperties['display']>
|
||||||
|
showClose?: boolean,
|
||||||
|
changedState?(id: string, value: string): void
|
||||||
|
onClose?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const OneSelectFilter = ({
|
||||||
|
id, data, spaceBetween,
|
||||||
|
label, defaultValue, textClassName,
|
||||||
|
selectProps, display, showClose, changedState, onClose
|
||||||
|
}: OneSelectFilterProps) => {
|
||||||
|
|
||||||
|
const handleOnChange = (value: string) => {
|
||||||
|
if (changedState) {
|
||||||
|
changedState(id, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex justify='space-between'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} />
|
||||||
|
: null}
|
||||||
|
</Flex>
|
||||||
|
<Select
|
||||||
|
{...selectProps}
|
||||||
|
mt={spaceBetween}
|
||||||
|
data={data}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OneSelectFilter;
|
||||||
43
src/shared/components/filters.aps/RangeSliderFilter.tsx
Normal file
43
src/shared/components/filters.aps/RangeSliderFilter.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { SystemProp, SpacingValue, SliderProps, Box, RangeSlider, RangeSliderProps, Text, Flex, CloseButton } from '@mantine/core';
|
||||||
|
import React, { CSSProperties, useState } from 'react';
|
||||||
|
import CloseWithTooltip from '../CloseWithTooltip';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
|
||||||
|
interface SliderFilterProps {
|
||||||
|
id: string
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
value?: [number, number]
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
label?: string
|
||||||
|
defaultValue?: [number, number]
|
||||||
|
textClassName?: string
|
||||||
|
sliderProps?: RangeSliderProps
|
||||||
|
display?: SystemProp<CSSProperties['display']>
|
||||||
|
showClose?: boolean,
|
||||||
|
changedState?(id: string, value: [number, number]): void
|
||||||
|
onClose?():void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const RangeSliderFilter = ({ id, min, max, value, spaceBetween,
|
||||||
|
label, defaultValue, textClassName,
|
||||||
|
sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => {
|
||||||
|
const handleOnChange = (value: [number, number]) => {
|
||||||
|
if (changedState) {
|
||||||
|
changedState(id, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex justify='space-between'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
{showClose? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null}
|
||||||
|
</Flex>
|
||||||
|
<RangeSlider {...sliderProps} value={value} onChangeEnd={handleOnChange} min={min} max={max} defaultValue={defaultValue} pl='1rem' mt='0.5rem' />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RangeSliderFilter;
|
||||||
48
src/shared/components/filters.aps/SliderFilter.tsx
Normal file
48
src/shared/components/filters.aps/SliderFilter.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Box, CloseButton, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||||
|
import React, { CSSProperties, useState, } from 'react';
|
||||||
|
import CloseWithTooltip from '../CloseWithTooltip';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
|
||||||
|
interface SliderFilterProps {
|
||||||
|
id: string
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
value?: number
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
label?: string
|
||||||
|
defaultValue?: number
|
||||||
|
textClassName?: string
|
||||||
|
sliderProps?: SliderProps,
|
||||||
|
display?: SystemProp<CSSProperties['display']>
|
||||||
|
showClose?: boolean,
|
||||||
|
changedState?(id: string, value: number): void
|
||||||
|
onClose?():void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SliderFilter = ({ id, min, max, value, spaceBetween, label, defaultValue, textClassName, sliderProps, display, showClose, changedState, onClose }: SliderFilterProps) => {
|
||||||
|
|
||||||
|
|
||||||
|
const handleOnChange = (value: number) => {
|
||||||
|
if (changedState) {
|
||||||
|
changedState(id, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex justify='space-between'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null}
|
||||||
|
</Flex>
|
||||||
|
<Slider {...sliderProps}
|
||||||
|
onChangeEnd={handleOnChange}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
pl='1rem' mt='0.5rem' />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SliderFilter;
|
||||||
44
src/shared/components/filters.aps/SwitchFilter.tsx
Normal file
44
src/shared/components/filters.aps/SwitchFilter.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { SystemProp, SpacingValue, Flex, Switch, Text, CloseButton, Group, Box } from '@mantine/core';
|
||||||
|
import React, { CSSProperties, ChangeEvent } from 'react';
|
||||||
|
import { boolean } from 'zod';
|
||||||
|
import CloseWithTooltip from '../CloseWithTooltip';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
|
||||||
|
interface SwitchFilterProps {
|
||||||
|
id: string
|
||||||
|
value?: boolean,
|
||||||
|
defaultValue?: boolean,
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
label?: string
|
||||||
|
textClassName?: string
|
||||||
|
display?: SystemProp<CSSProperties['display']>
|
||||||
|
showClose?: boolean
|
||||||
|
changedState?(id: string, value: boolean): void
|
||||||
|
onClose?():void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchChangeState {
|
||||||
|
itemId: string,
|
||||||
|
value: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SwitchFilter = ({ id, value, defaultValue, spaceBetween, label, textClassName, display, showClose, changedState, onClose }: SwitchFilterProps) => {
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement> | undefined) => {
|
||||||
|
const checked = event?.currentTarget.checked
|
||||||
|
if (changedState && typeof checked === 'boolean') {
|
||||||
|
changedState(id, checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex align='center' w='100%'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
<Switch onChange={handleChange} checked={value} defaultChecked={defaultValue} ml='lg' mr='md' />
|
||||||
|
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} /> : null}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SwitchFilter;
|
||||||
50
src/shared/components/grid.aps/BuyCounterToggle.tsx
Normal file
50
src/shared/components/grid.aps/BuyCounterToggle.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useCounter } from '@mantine/hooks';
|
||||||
|
import RowCounter from '../table.aps/RowCounter';
|
||||||
|
import { Button, createStyles } from '@mantine/core';
|
||||||
|
import { productString } from '../../strings/product.strings';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
counter: {
|
||||||
|
height: Button.defaultProps?.h?.toString() || '36px'
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
interface BuyCounterToggleProps {
|
||||||
|
counter?: number
|
||||||
|
setValue?(value: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuyCounterToggle = ({ counter, setValue }: BuyCounterToggleProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
// const [count, handlers] = useCounter(counter, { min: 0 })
|
||||||
|
|
||||||
|
const handleSetCount = (value: number) => {
|
||||||
|
if (setValue) setValue(value)
|
||||||
|
// else handlers.set(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBuyClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSetCount(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("render BuyCounterToggle")
|
||||||
|
// })
|
||||||
|
|
||||||
|
if (counter && counter > 0) {
|
||||||
|
return (
|
||||||
|
<div className={classes.counter}>
|
||||||
|
<RowCounter key={uuidv4()} counter={counter} setValue={handleSetCount} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button w="100%" onClick={handleBuyClick}
|
||||||
|
> {productString.buy}</Button >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BuyCounterToggle;
|
||||||
|
|
||||||
31
src/shared/components/grid.aps/CardCarousel.tsx
Normal file
31
src/shared/components/grid.aps/CardCarousel.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Carousel } from '@mantine/carousel';
|
||||||
|
import { AspectRatio, Image, createStyles, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
||||||
|
|
||||||
|
interface CardCarouselProps {
|
||||||
|
image: string
|
||||||
|
onClick?(): void
|
||||||
|
ratio: number
|
||||||
|
height?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardCarousel = ({ image, onClick, ratio, height }: CardCarouselProps) => {
|
||||||
|
const handleImageClick = () => {
|
||||||
|
if (onClick) onClick()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Carousel.Slide>
|
||||||
|
<AspectRatio ratio={ratio}>
|
||||||
|
<ImageWithPlaceHolder
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleImageClick()
|
||||||
|
}}
|
||||||
|
src={image} height={height} />
|
||||||
|
</AspectRatio>
|
||||||
|
</Carousel.Slide>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CardCarousel;
|
||||||
184
src/shared/components/grid.aps/GridCard.tsx
Normal file
184
src/shared/components/grid.aps/GridCard.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Card, Group, createStyles, getStylesRef, rem, Text, Container, Badge, ColSpan, Grid, Flex } from '@mantine/core';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { Carousel } from '@mantine/carousel';
|
||||||
|
import { GridAdapter } from './ProductGrid';
|
||||||
|
import CardCarousel from './CardCarousel';
|
||||||
|
import BuyCounterToggle from './BuyCounterToggle';
|
||||||
|
import Currency from '../Currency';
|
||||||
|
import { Context } from '../../..';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import PriceText from '../PriceText';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
|
||||||
|
mainCard: {
|
||||||
|
borderWidth: '1px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
width: '12rem',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.colorScheme === 'dark' ? theme.fn.darken(theme.colors.cyan[9], 0.5) : theme.colors.cyan[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
bottomGroup: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
},
|
||||||
|
|
||||||
|
priceContainer: {
|
||||||
|
marginTop: '0.8rem',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
gap: "0.5rem",
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
price: {
|
||||||
|
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||||
|
fontSize: "lg",
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
productNameGroup: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
gap: "0.5rem",
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
productName: {
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
carousel: {
|
||||||
|
'&:hover': {
|
||||||
|
[`& .${getStylesRef('carouselControls')}`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselControls: {
|
||||||
|
ref: getStylesRef('carouselControls'),
|
||||||
|
transition: 'opacity 150ms ease',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
carouselIndicator: {
|
||||||
|
width: rem(4),
|
||||||
|
height: rem(4),
|
||||||
|
transition: 'width 250ms ease',
|
||||||
|
|
||||||
|
'&[data-active]': {
|
||||||
|
width: rem(16),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
interface GridCardProps {
|
||||||
|
span?: ColSpan,
|
||||||
|
item: GridAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridCard = observer(({ span, item }: GridCardProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
|
||||||
|
const { modalStore, cartStore } = useContext(Context)
|
||||||
|
const { openFullImage, openProductDetailed } = modalStore
|
||||||
|
|
||||||
|
const prodId = item.id
|
||||||
|
const prodName: string = item.name
|
||||||
|
const prodImages: string[] = item.image
|
||||||
|
const prodPrice: number = item.cost
|
||||||
|
const prodDiscount: boolean = item.discount
|
||||||
|
const prodQty: number = item.qty
|
||||||
|
//todo replace to real price
|
||||||
|
const min = 1.0;
|
||||||
|
const max = 1.99;
|
||||||
|
const randomNumber = Math.random() * (max - min) + min;
|
||||||
|
const prodDiscountPrice: number = Number((item.cost * randomNumber).toFixed(2))
|
||||||
|
const prodDiscountPercent: number = parseFloat(((prodPrice / prodDiscountPrice - 1) * 100).toFixed(0))
|
||||||
|
|
||||||
|
const slides = prodImages.length > 0
|
||||||
|
?
|
||||||
|
prodImages.map((image) => (
|
||||||
|
<CardCarousel
|
||||||
|
key={uuidv4()}
|
||||||
|
image={image}
|
||||||
|
ratio={1}
|
||||||
|
height={190}
|
||||||
|
onClick={() => openFullImage(item.image)}
|
||||||
|
/>))
|
||||||
|
:
|
||||||
|
<CardCarousel
|
||||||
|
key={uuidv4()}
|
||||||
|
image=''
|
||||||
|
ratio={100 / 100}
|
||||||
|
height={190} />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid.Col span={span} >
|
||||||
|
<Card radius="lg" padding='4px' withBorder className={classes.mainCard}>
|
||||||
|
<Card.Section>
|
||||||
|
<Carousel
|
||||||
|
withIndicators
|
||||||
|
loop
|
||||||
|
classNames={{
|
||||||
|
root: classes.carousel,
|
||||||
|
controls: classes.carouselControls,
|
||||||
|
indicator: classes.carouselIndicator,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{slides}
|
||||||
|
</Carousel>
|
||||||
|
</Card.Section>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
onClick={() => openProductDetailed(prodId)}
|
||||||
|
className={classes.priceContainer}
|
||||||
|
fluid>
|
||||||
|
{prodDiscount ?
|
||||||
|
<Group pl="0.3rem" style={{ display: 'flex', flexWrap: 'nowrap', gap: '0', alignItems: 'center' }}>
|
||||||
|
<PriceText td='line-through' fs='oblique' fz='sm' value={prodDiscountPrice} />
|
||||||
|
<Currency fz='sm' />
|
||||||
|
<Badge style={{ alignSelf: 'center' }} color='red' size='lg' p="0">{prodDiscountPercent}%</Badge>
|
||||||
|
</Group>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<Group style={{ display: 'flex', flexWrap: 'nowrap', gap: '0', alignItems: 'flex-end' }}>
|
||||||
|
<PriceText value={prodPrice} />
|
||||||
|
<Currency />
|
||||||
|
</Group>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Group
|
||||||
|
onClick={() => openProductDetailed(prodId)}
|
||||||
|
mt='0.5rem'
|
||||||
|
className={classes.productNameGroup}
|
||||||
|
position="apart">
|
||||||
|
<Text fz='sm' align="center" fw='500' className={classes.productName}>
|
||||||
|
{prodName}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group
|
||||||
|
className={classes.bottomGroup}>
|
||||||
|
<Flex w='100%' justify='center' mt="0.5rem">
|
||||||
|
<BuyCounterToggle counter={item.qty} setValue={(value) => {
|
||||||
|
console.log("value", value)
|
||||||
|
cartStore.setToCart(item, value)
|
||||||
|
}} />
|
||||||
|
</Flex>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col >
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default GridCard;
|
||||||
36
src/shared/components/grid.aps/ProductGrid.tsx
Normal file
36
src/shared/components/grid.aps/ProductGrid.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Card, Grid, rem } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import GridCard from './GridCard';
|
||||||
|
import BuyCounterToggle from './BuyCounterToggle';
|
||||||
|
import RowCounter from '../table.aps/RowCounter';
|
||||||
|
|
||||||
|
export type GridAdapter = {
|
||||||
|
id: string,
|
||||||
|
number: number,
|
||||||
|
manufactory: string,
|
||||||
|
oem: string,
|
||||||
|
stock: number,
|
||||||
|
receipt_date: string,
|
||||||
|
name: string,
|
||||||
|
cost: number,
|
||||||
|
image: string[],
|
||||||
|
discount: boolean,
|
||||||
|
qty: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductGridProps {
|
||||||
|
gridData: GridAdapter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductGrid = ({ gridData }: ProductGridProps) => {
|
||||||
|
const span = "content"
|
||||||
|
const grids = gridData.map( item => ( <GridCard key={item.id} span={span} item={item} />))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid pt="1rem" justify='center' align='stretch'>
|
||||||
|
{grids}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductGrid;
|
||||||
23
src/shared/components/grid.aps/ProfileRow.tsx
Normal file
23
src/shared/components/grid.aps/ProfileRow.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Grid, Flex, Text } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
|
||||||
|
interface ProfileRowProps {
|
||||||
|
name?: string
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProfileRow = ({ name, value }: ProfileRowProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Text fz='md'>{name}</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Text align='center' fz='md'>{value}</Text>
|
||||||
|
</Grid.Col>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileRow;
|
||||||
22
src/shared/components/svg/CogWheelHeartSVG.tsx
Normal file
22
src/shared/components/svg/CogWheelHeartSVG.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export const CogWheelHeartSVG = (
|
||||||
|
<svg
|
||||||
|
width={284}
|
||||||
|
height={269}
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M203.495 123.118c0 9.429-3.623 18.485-10.092 25.185-14.891 15.426-29.334 31.512-44.782 46.379-3.54 3.359-9.157 3.236-12.545-.274l-44.505-46.105c-13.452-13.936-13.452-36.434 0-50.37 13.584-14.073 35.714-14.073 49.298 0l1.618 1.676 1.617-1.675c6.513-6.751 15.383-10.559 24.649-10.559 9.267 0 18.136 3.808 24.65 10.558 6.47 6.7 10.092 15.756 10.092 25.185z"
|
||||||
|
stroke="#C92A2A"
|
||||||
|
strokeWidth={12.324}
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M246.427 113.72l-15.039-34.325L251.6 56.8l-27.4-25.9-23.774 19.204-37.084-14.416L154.814 5H128.04l-8.657 31.095L83.15 50.532 59.8 30.9 32.4 56.8l19.911 23.166-14.808 34.414L5 121.55v25.9l32.895 8.489 15.27 34.242L32.4 212.2l27.4 25.9 24.539-18.903 35.7 13.882L128.3 264h27.4l8.282-30.909 36.313-14.215c6.053 4.089 23.905 19.224 23.905 19.224l27.4-25.9-20.332-22.67 15.042-34.336 32.689-8.039.001-25.605-32.573-7.83z"
|
||||||
|
stroke="#C92A2A"
|
||||||
|
strokeWidth={10}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
20
src/shared/components/svg/ExclamationCogWheel.tsx
Normal file
20
src/shared/components/svg/ExclamationCogWheel.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const ExclamationCogWheel = (
|
||||||
|
<svg
|
||||||
|
width={284}
|
||||||
|
height={269}
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M246.427 113.72l-15.039-34.325L251.6 56.8l-27.4-25.9-23.774 19.204-37.084-14.416L154.814 5H128.04l-8.657 31.095L83.15 50.532 59.8 30.9 32.4 56.8l19.911 23.166-14.808 34.414L5 121.55v25.9l32.895 8.489 15.27 34.242L32.4 212.2l27.4 25.9 24.539-18.903 35.7 13.882L128.3 264h27.4l8.282-30.909 36.313-14.215c6.053 4.089 23.905 19.224 23.905 19.224l27.4-25.9-20.332-22.67 15.042-34.336 32.689-8.039.001-25.605-32.573-7.83z"
|
||||||
|
stroke="#C92A2A"
|
||||||
|
strokeWidth={10}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M142.101 164.994c-2.551 0-4.564-.807-6.041-2.423-1.342-1.616-2.147-3.904-2.416-6.866l-6.04-77.145c-.403-4.981.671-8.953 3.222-11.915 2.55-3.097 6.308-4.645 11.275-4.645 4.832 0 8.456 1.548 10.872 4.645 2.55 2.962 3.624 6.934 3.222 11.915l-6.041 77.145c-.134 2.962-.939 5.25-2.416 6.866-1.342 1.616-3.221 2.423-5.637 2.423zm0 42.006c-4.564 0-8.255-1.414-11.074-4.241-2.685-2.827-4.027-6.462-4.027-10.905 0-4.308 1.342-7.809 4.027-10.502 2.819-2.827 6.51-4.241 11.074-4.241 4.698 0 8.322 1.414 10.872 4.241 2.685 2.693 4.027 6.194 4.027 10.502 0 4.443-1.342 8.078-4.027 10.905-2.55 2.827-6.174 4.241-10.872 4.241z"
|
||||||
|
fill="#C92A2A"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
38
src/shared/components/svg/СogwheelSVG.tsx
Normal file
38
src/shared/components/svg/СogwheelSVG.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { DEFAULT_THEME } from '@mantine/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const СogwheelSVG = (
|
||||||
|
<svg
|
||||||
|
width="86"
|
||||||
|
height="86"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke={DEFAULT_THEME.colors.blue[6]}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M43 55c6.628 0 12-5.372 12-12s-5.372-12-12-12-12 5.372-12 12 5.372 12 12 12z"
|
||||||
|
stroke="#228BE6"
|
||||||
|
strokeWidth={5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M73.49 36.582l-4.391-10.603L75 19l-8-8-6.941 5.932-10.828-4.453L46.741 3h-7.817l-2.528 9.604-10.578 4.46L19 11l-8 8 5.814 7.155-4.324 10.63L3 39v8l9.604 2.622 4.459 10.577L11 67l8 8 7.165-5.839 10.423 4.288L39 83h8l2.418-9.547 10.602-4.391C61.788 70.325 67 75 67 75l8-8-5.936-7.002 4.392-10.606L83 46.909V39l-9.51-2.418z"
|
||||||
|
stroke="#228BE6"
|
||||||
|
strokeWidth={5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
type="rotate"
|
||||||
|
from="0 43 43"
|
||||||
|
to="360 43 43"
|
||||||
|
dur="3s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default СogwheelSVG;
|
||||||
95
src/shared/components/table.aps/DeliveryPointsTable.tsx
Normal file
95
src/shared/components/table.aps/DeliveryPointsTable.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Button, Table } from '@mantine/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import SortedTh from './SortedTh';
|
||||||
|
import { DeliveryPoint } from '../../stores/user.store';
|
||||||
|
import { strings } from '../../strings/strings';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
|
||||||
|
interface DeliveryPointsTableProps {
|
||||||
|
data: DeliveryPoint[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
|
||||||
|
|
||||||
|
const [tableData, setData] = useState(data)
|
||||||
|
|
||||||
|
const [reversed, setReversed] = useState(false)
|
||||||
|
const [sortedName, setSortedName] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleSort = (headName: string, dataIndex: number) => {
|
||||||
|
const reverse = headName === sortedName ? !reversed : false;
|
||||||
|
setReversed(reverse)
|
||||||
|
if (reverse) {
|
||||||
|
setData(sortByKey(data, dataIndex).reverse())
|
||||||
|
} else {
|
||||||
|
setData(sortByKey(data, dataIndex))
|
||||||
|
}
|
||||||
|
setSortedName(headName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortByKey = (deliveryPoints: DeliveryPoint[], keyIndex: number): DeliveryPoint[] => {
|
||||||
|
const keys = Object.keys(deliveryPoints[0]) as Array<keyof DeliveryPoint>
|
||||||
|
return deliveryPoints.sort((a, b) => {
|
||||||
|
const valueA = a[keys[keyIndex]].toLowerCase();
|
||||||
|
const valueB = b[keys[keyIndex]].toLowerCase();
|
||||||
|
|
||||||
|
if (valueA < valueB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (valueA > valueB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const headTitle = [
|
||||||
|
{ dataIndex:1, title: strings.name },
|
||||||
|
{ dataIndex:2, title: strings.schedule },
|
||||||
|
{ dataIndex:3, title: strings.address },
|
||||||
|
{ title: '', sorting: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const tableHead = headTitle.map(head => {
|
||||||
|
return (
|
||||||
|
<SortedTh
|
||||||
|
key={uuidv4()}
|
||||||
|
title={head.title}
|
||||||
|
reversed={reversed}
|
||||||
|
sortedName={sortedName}
|
||||||
|
onSort={() => handleSort(head.title, head.dataIndex ? head.dataIndex : 0)}
|
||||||
|
sorting={head.sorting} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = tableData.map(item => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>{item.schedule}</td>
|
||||||
|
<td>{item.address}</td>
|
||||||
|
<td><Button>{strings.edit}</Button></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table >
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{tableHead}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeliveryPointsTable;
|
||||||
156
src/shared/components/table.aps/ProductTable.tsx
Normal file
156
src/shared/components/table.aps/ProductTable.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { Table, } from '@mantine/core';
|
||||||
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
import { useDisclosure, useHotkeys, } from '@mantine/hooks';
|
||||||
|
import TableRow from './TableRow';
|
||||||
|
import InputModal from '../InputModal';
|
||||||
|
import { Context } from '../../..';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import ProductsTableHead from './ProductsTableHead';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
export type TableAdapter = {
|
||||||
|
id: string,
|
||||||
|
number: number,
|
||||||
|
manufactory: string,
|
||||||
|
oem: string,
|
||||||
|
stock: number,
|
||||||
|
receipt_date: string,
|
||||||
|
name: string,
|
||||||
|
cost: number,
|
||||||
|
image: string[],
|
||||||
|
discount: boolean,
|
||||||
|
qty: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductTableProps {
|
||||||
|
tableData?: TableAdapter[],
|
||||||
|
showDelete?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ProductTable = ({tableData, showDelete}: ProductTableProps) => {
|
||||||
|
|
||||||
|
const { cartStore } = useContext(Context)
|
||||||
|
const { sortCart } = cartStore
|
||||||
|
|
||||||
|
const data = tableData ? tableData : []
|
||||||
|
|
||||||
|
const [sortBy, setSortBy] = useState<string | null>(null);
|
||||||
|
const [reverseSortDirection, setReverseSortDirection] = useState(false);
|
||||||
|
|
||||||
|
// const [data, setData] = useState(tableData)
|
||||||
|
|
||||||
|
const [selectedId, setSelectedId] = useState<string>("")
|
||||||
|
const [qtyValue, setQtyValue] = useState<number>(0)
|
||||||
|
|
||||||
|
const setSorting = (title: string) => {
|
||||||
|
const reversed = title === sortBy ? !reverseSortDirection : false;
|
||||||
|
setReverseSortDirection(reversed)
|
||||||
|
setSortBy(title)
|
||||||
|
sortCart(reversed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUpDown = (key: string) => {
|
||||||
|
if (!selectedId) setSelectedId(data[0].id)
|
||||||
|
if (selectedId) {
|
||||||
|
const currentIndex = data.findIndex(value => value.id === selectedId)
|
||||||
|
switch (key) {
|
||||||
|
case "up": {
|
||||||
|
if (currentIndex !== 0) setSelectedId(data[currentIndex - 1].id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "down": {
|
||||||
|
if (currentIndex !== data.length - 1) setSelectedId(data[currentIndex + 1].id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLeftRight = (key: string) => {
|
||||||
|
if (!selectedId) setSelectedId(data[0].id)
|
||||||
|
if (selectedId) {
|
||||||
|
if (key === 'right') {
|
||||||
|
increaseData(selectedId)
|
||||||
|
}
|
||||||
|
if (key === 'left') {
|
||||||
|
decreaseData(selectedId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const increaseData = (selectedId: string) => {
|
||||||
|
setQtyData(selectedId, qtyValue+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const decreaseData = (selectedId: string) => {
|
||||||
|
if (qtyValue > 0) setQtyData(selectedId, qtyValue-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQtyData = (selectedId: string, value: number) => {
|
||||||
|
// setData(data.map(element => {
|
||||||
|
// if (element.id === selectedId) return {
|
||||||
|
// ...element,
|
||||||
|
// qty: value
|
||||||
|
// }
|
||||||
|
// return element
|
||||||
|
// }))
|
||||||
|
const tableItem = data.find( item => item.id === selectedId)
|
||||||
|
if (tableItem) cartStore.setToCart(tableItem, value)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
cartStore.deleteFromCart(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenInputQty = () => {
|
||||||
|
if (selectedId) open()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [opened, { open, close }] = useDisclosure(false)
|
||||||
|
|
||||||
|
const handleInputModalValue = (value: number) => {
|
||||||
|
setQtyData(selectedId, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.length !== 0 && selectedId) {
|
||||||
|
const qty = data.find( (element) => element.id === selectedId)?.qty || 0
|
||||||
|
setQtyValue(qty)
|
||||||
|
}
|
||||||
|
}, [selectedId, data])
|
||||||
|
|
||||||
|
useHotkeys([
|
||||||
|
['ArrowUp', () => handleUpDown('up')],
|
||||||
|
['ArrowDown', () => handleUpDown('down')],
|
||||||
|
['ArrowRight', () => handleLeftRight('right')],
|
||||||
|
['ArrowLeft', () => handleLeftRight('left')],
|
||||||
|
['mod+Enter', () => handleOpenInputQty()],
|
||||||
|
])
|
||||||
|
|
||||||
|
const rows = data.map(element =>
|
||||||
|
<TableRow
|
||||||
|
key={element.id}
|
||||||
|
element={element}
|
||||||
|
selected={selectedId}
|
||||||
|
setQty={setQtyData}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
showDelete={showDelete}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputModal key={uuidv4()} inValue={qtyValue} putValue={handleInputModalValue} opened={opened} open={open} close={close} />
|
||||||
|
<Table >
|
||||||
|
<ProductsTableHead reverseSortDirection={reverseSortDirection} sortBy={sortBy} setSorting={setSorting} />
|
||||||
|
<tbody>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductTable;
|
||||||
53
src/shared/components/table.aps/ProductsTableHead.tsx
Normal file
53
src/shared/components/table.aps/ProductsTableHead.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { productString } from '../../strings/product.strings';
|
||||||
|
import SortedTh from './SortedTh';
|
||||||
|
|
||||||
|
interface TableHeadProps {
|
||||||
|
reverseSortDirection: boolean
|
||||||
|
sortBy: string | null
|
||||||
|
setSorting: (title: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductsTableHead = ({ reverseSortDirection, sortBy, setSorting }: TableHeadProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<SortedTh
|
||||||
|
title={productString.name}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
sortedName={sortBy}
|
||||||
|
onSort={setSorting}
|
||||||
|
/>
|
||||||
|
<SortedTh
|
||||||
|
title={productString.cost}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
sortedName={sortBy}
|
||||||
|
onSort={setSorting}
|
||||||
|
/>
|
||||||
|
<SortedTh
|
||||||
|
title={productString.image}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
sortedName={sortBy}
|
||||||
|
onSort={setSorting}
|
||||||
|
textProps={{ w: '6rem', truncate: true }}
|
||||||
|
/>
|
||||||
|
<SortedTh
|
||||||
|
title={productString.qty}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
sortedName={sortBy}
|
||||||
|
onSort={setSorting}
|
||||||
|
textProps={{ w: '1rem', truncate: true }}
|
||||||
|
/>
|
||||||
|
<SortedTh
|
||||||
|
title={productString.buy}
|
||||||
|
reversed={reverseSortDirection}
|
||||||
|
sortedName={sortBy}
|
||||||
|
onSort={setSorting}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductsTableHead;
|
||||||
73
src/shared/components/table.aps/RowCounter.tsx
Normal file
73
src/shared/components/table.aps/RowCounter.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ActionIcon, Badge, Box, Flex, Text, useMantineTheme } from '@mantine/core';
|
||||||
|
import { useCounter, useDisclosure } from '@mantine/hooks';
|
||||||
|
import { IconMinus, IconPlus, IconX } from '@tabler/icons-react';
|
||||||
|
import InputModal from '../InputModal';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface RowCounterProps {
|
||||||
|
counter?: number
|
||||||
|
setValue?(value: number): void,
|
||||||
|
showDelete?: boolean
|
||||||
|
onDelete?(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RowCounter = ({ counter, setValue, showDelete, onDelete }: RowCounterProps) => {
|
||||||
|
const [opened, { open, close }] = useDisclosure(false)
|
||||||
|
// const [count, handlers] = useCounter(counter, { min: 0 })
|
||||||
|
const count = counter || 0
|
||||||
|
|
||||||
|
const handleSetValue = (value: number) => {
|
||||||
|
if (setValue) setValue(value)
|
||||||
|
// else handlers.set(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInrease = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSetValue(count + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDerease = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSetValue(count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (onDelete) onDelete()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputModal key={uuidv4()} inValue={counter ? counter : count} putValue={handleSetValue} opened={opened} open={open} close={close} />
|
||||||
|
<Flex direction="row">
|
||||||
|
<ActionIcon onClick={handleDerease} mt="0.1rem" color="red.3" size="md" radius="xl" variant="filled">
|
||||||
|
<IconMinus size="1.125rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
<Box w="3rem">
|
||||||
|
<Badge size="xl" pl="0.2rem" pr="0.2rem" fullWidth onClick={handleOpen}>
|
||||||
|
{count}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<ActionIcon onClick={handleInrease} mt="0.1rem" color="blue.6" size="md" radius="xl" variant="filled">
|
||||||
|
<IconPlus size="1.125rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
{
|
||||||
|
showDelete ?
|
||||||
|
<ActionIcon onClick={handleDelete} ml='0.1rem' mt="0.1rem" color="red" size="md" radius="xl" variant="filled">
|
||||||
|
<IconX size="1.125rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
:
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RowCounter;
|
||||||
31
src/shared/components/table.aps/SortedTh.tsx
Normal file
31
src/shared/components/table.aps/SortedTh.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Center, Flex, MantineStyleSystemProps, Text, TextProps, Tooltip } from '@mantine/core';
|
||||||
|
import { IconChevronDown, IconChevronUp, IconSelector, } from '@tabler/icons-react';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
|
||||||
|
interface SortedThProps {
|
||||||
|
title: string,
|
||||||
|
reversed: boolean,
|
||||||
|
sortedName: string | null,
|
||||||
|
textProps?: TextProps & React.RefAttributes<HTMLDivElement>
|
||||||
|
sorting?: boolean
|
||||||
|
onSort: (title: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortedTh = ({ sortedName, reversed, title, onSort, textProps, sorting=true }: SortedThProps) => {
|
||||||
|
const sorted = sortedName === title
|
||||||
|
const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;
|
||||||
|
return (
|
||||||
|
<th style={{paddingLeft: 5, paddingRight: 5}}>
|
||||||
|
<Center onClick={() => onSort(title)}>
|
||||||
|
<Tooltip label={title} transitionProps={{ transition: 'slide-up', duration: 300 }} openDelay={500}>
|
||||||
|
<Text {...textProps}>{title}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
{
|
||||||
|
sorting ? <Icon /> : null
|
||||||
|
}
|
||||||
|
</Center>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortedTh;
|
||||||
129
src/shared/components/table.aps/TableRow.tsx
Normal file
129
src/shared/components/table.aps/TableRow.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import RowCounter from './RowCounter';
|
||||||
|
import { Badge, Center, Flex, Group, Text, createStyles } from '@mantine/core';
|
||||||
|
import { TableAdapter } from './ProductTable';
|
||||||
|
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
||||||
|
import Currency from '../Currency';
|
||||||
|
import { Context } from '../../..';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import PriceText from '../PriceText';
|
||||||
|
|
||||||
|
interface TableRowProps {
|
||||||
|
element: TableAdapter
|
||||||
|
selected: string
|
||||||
|
increase?(id: string): void
|
||||||
|
decrease?(id: string): void
|
||||||
|
setQty?(id: string, value: number): void
|
||||||
|
onDelete?(id: string): void
|
||||||
|
showDelete?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
tableRow: {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[1],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowSelected: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.fn.darken(theme.colors.cyan[9], 0.5)
|
||||||
|
: theme.colors.cyan[1],
|
||||||
|
},
|
||||||
|
|
||||||
|
price: {
|
||||||
|
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||||
|
fontSize: "lg",
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
discountPrice: {
|
||||||
|
fontSize: "sm",
|
||||||
|
fontWeight: 500,
|
||||||
|
textDecoration: "line-through",
|
||||||
|
fontStyle: 'oblique',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
const TableRow = observer(({ element, selected, increase, decrease, setQty, onDelete, showDelete }: TableRowProps) => {
|
||||||
|
const { classes, cx } = useStyles()
|
||||||
|
const mainRef: React.LegacyRef<HTMLTableRowElement> = useRef(null)
|
||||||
|
|
||||||
|
const { modalStore } = useContext(Context)
|
||||||
|
const { openFullImage, openProductDetailed } = modalStore
|
||||||
|
|
||||||
|
//todo replace to real price
|
||||||
|
const min = 1.0;
|
||||||
|
const max = 1.99;
|
||||||
|
const randomNumber = Math.random() * (max - min) + min;
|
||||||
|
const prodDiscountPrice: number = Number((element.cost * randomNumber).toFixed(2))
|
||||||
|
const prodDiscountPercent: number = parseFloat(((element.cost / prodDiscountPrice - 1) * 100).toFixed(0))
|
||||||
|
|
||||||
|
const handleSetQty = (value: number) => {
|
||||||
|
if (setQty) setQty(element.id, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (onDelete) onDelete(element.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr ref={mainRef}
|
||||||
|
className={cx(
|
||||||
|
{ [classes.rowSelected]: selected === element.id },
|
||||||
|
classes.tableRow,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td onClick={() => {openProductDetailed(element.id)}}><Text fz='sm' fw='500'>{element.name}</Text></td>
|
||||||
|
<td>
|
||||||
|
< Flex direction='column' wrap='nowrap' gap='0' align='center' >
|
||||||
|
{element.discount ?
|
||||||
|
<Flex wrap='nowrap' gap='0' align='center' >
|
||||||
|
<Text fz='sm' className={classes.discountPrice}>
|
||||||
|
{Intl.NumberFormat().format(prodDiscountPrice)}
|
||||||
|
</Text>
|
||||||
|
<Currency fz='sm' />
|
||||||
|
<Badge style={{ alignSelf: 'center' }} color='red' size='md' p="0">{prodDiscountPercent}%</Badge>
|
||||||
|
</Flex>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
<Flex wrap='nowrap' gap='0' align='flex-end' >
|
||||||
|
<PriceText value={element.cost} fz='sm' fw='500' />
|
||||||
|
<Currency fz='sm' />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</td>
|
||||||
|
<td style={{ backgroundSize: "cover" }}>
|
||||||
|
<ImageWithPlaceHolder
|
||||||
|
onClick={() => openFullImage(element.image)}
|
||||||
|
mah="4rem"
|
||||||
|
mih="2rem"
|
||||||
|
height="3rem"
|
||||||
|
src={element.image[0]} />
|
||||||
|
</td>
|
||||||
|
<td style={{paddingLeft: 5, paddingRight: 5}}>
|
||||||
|
<Center>
|
||||||
|
<Text fz='sm' fw='500'>
|
||||||
|
{Intl.NumberFormat().format(element.stock)}
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
</td>
|
||||||
|
<td >
|
||||||
|
<Center>
|
||||||
|
<RowCounter
|
||||||
|
counter={element.qty}
|
||||||
|
setValue={handleSetQty}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
showDelete={showDelete}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</td>
|
||||||
|
</tr >
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default TableRow;
|
||||||
14
src/shared/components/СogwheelLoader.tsx
Normal file
14
src/shared/components/СogwheelLoader.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Center, DEFAULT_THEME } from '@mantine/core';
|
||||||
|
import СogwheelSVG from './svg/СogwheelSVG';
|
||||||
|
|
||||||
|
|
||||||
|
const СogwheelLoader = () => {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
{СogwheelSVG}
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default СogwheelLoader;
|
||||||
6
src/shared/dimensions/dimensions.ts
Normal file
6
src/shared/dimensions/dimensions.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const dimensions = {
|
||||||
|
mobileSize: "(max-width: 50em)",
|
||||||
|
middleScreenSize: "(max-width: 68em)",
|
||||||
|
hideSidebarsSize: "sm",
|
||||||
|
spaceBetweenFilters: "0.4rem",
|
||||||
|
}
|
||||||
29
src/shared/env.const.ts
Normal file
29
src/shared/env.const.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export const appMode = process.env.NODE_ENV
|
||||||
|
const isProduction = appMode === "production"
|
||||||
|
if (isProduction && typeof process.env.REACT_APP_HOST === 'undefined') {
|
||||||
|
throw new Error('REACT_APP_HOST environment variable is undefined');
|
||||||
|
}
|
||||||
|
export const host = process.env.REACT_APP_HOST || 'localhost'
|
||||||
|
|
||||||
|
if (isProduction && typeof process.env.REACT_APP_PORT === 'undefined') {
|
||||||
|
throw new Error('REACT_APP_PORT environment variable is undefined');
|
||||||
|
}
|
||||||
|
export const port = process.env.REACT_APP_PORT || '4000'
|
||||||
|
export const hostURL = new URL('http://' + host + ':' + port)
|
||||||
|
|
||||||
|
if (typeof process.env.REACT_APP_FRIGATE_PROXY === 'undefined') {
|
||||||
|
throw new Error('REACT_APP_FRIGATE_PROXY environment variable is undefined');
|
||||||
|
}
|
||||||
|
export const proxyURL = new URL(process.env.REACT_APP_FRIGATE_PROXY)
|
||||||
|
|
||||||
|
if (typeof process.env.REACT_APP_OPENID_SERVER === 'undefined') {
|
||||||
|
throw new Error('REACT_APP_OPENID_SERVER environment variable is undefined');
|
||||||
|
}
|
||||||
|
export const openIdServer = process.env.REACT_APP_OPENID_SERVER
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof process.env.REACT_APP_CLIENT_ID === 'undefined') {
|
||||||
|
throw new Error('REACT_APP_CLIENT_ID environment variable is undefined');
|
||||||
|
}
|
||||||
|
export const clientId = process.env.REACT_APP_CLIENT_ID
|
||||||
|
export const redirectURL = hostURL.toString()
|
||||||
15
src/shared/services/keycloack.ts
Normal file
15
src/shared/services/keycloack.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { AuthProviderProps } from "react-oidc-context";
|
||||||
|
import { openIdServer, clientId, redirectURL } from "../env.const";
|
||||||
|
|
||||||
|
export const keycloakConfig: AuthProviderProps = {
|
||||||
|
authority: openIdServer,
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: redirectURL,
|
||||||
|
onSigninCallback: () => {
|
||||||
|
window.history.replaceState(
|
||||||
|
{},
|
||||||
|
document.title,
|
||||||
|
window.location.pathname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/shared/stores/cart.store.ts
Normal file
246
src/shared/stores/cart.store.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
import { Product } from "./product.store";
|
||||||
|
import { sleep } from "../utils/async.sleep";
|
||||||
|
import { TableAdapter } from "../components/table.aps/ProductTable";
|
||||||
|
import { DeliveryMethod, DeliveryMethods, PaymentMethod, PaymentMethods } from "./orders.store";
|
||||||
|
import { addItem, removeItemById } from "../utils/array.helper";
|
||||||
|
import { strings } from "../strings/strings";
|
||||||
|
import { Validated } from "../utils/validated";
|
||||||
|
import { DeliveryPoint, DeliveryPointSchema } from "./user.store";
|
||||||
|
|
||||||
|
export interface CartProduct extends Product {
|
||||||
|
qty: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderingStage {
|
||||||
|
stage: number,
|
||||||
|
path: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO delete
|
||||||
|
export class CartStore {
|
||||||
|
|
||||||
|
|
||||||
|
readonly cartStage1: OrderingStage = { stage: 0, path: 'CART_PATH' }
|
||||||
|
readonly cartStage2: OrderingStage = { stage: 1, path: 'CART_METHOD_PATH' }
|
||||||
|
readonly cartStage3: OrderingStage = { stage: 2, path: 'PAYMENT_PATH' }
|
||||||
|
|
||||||
|
private _CartStages: OrderingStage[] = [
|
||||||
|
this.cartStage1,
|
||||||
|
this.cartStage2
|
||||||
|
]
|
||||||
|
|
||||||
|
public get CartStages() {
|
||||||
|
return this._CartStages
|
||||||
|
}
|
||||||
|
|
||||||
|
private _confirmedStage?: OrderingStage
|
||||||
|
public get confirmedStage() {
|
||||||
|
return this._confirmedStage;
|
||||||
|
}
|
||||||
|
private _currentStage: OrderingStage = this._CartStages[0]
|
||||||
|
public get currentStage() {
|
||||||
|
return this._currentStage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _paymentMethod = new Validated<PaymentMethod>
|
||||||
|
public get paymentMethod() {
|
||||||
|
return this._paymentMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deliveryMethod = new Validated<DeliveryMethod>
|
||||||
|
public get deliveryMethod() {
|
||||||
|
return this._deliveryMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deliveryDate = new Validated<Date>
|
||||||
|
public get deliveryDate() {
|
||||||
|
return this._deliveryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deliveryPoint = new Validated<DeliveryPoint>
|
||||||
|
public get deliveryPoint() {
|
||||||
|
return this._deliveryPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _products: CartProduct[] = []
|
||||||
|
public get products() {
|
||||||
|
return this._products;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get totalSum(): number {
|
||||||
|
const totalSum = this._products.reduce((sum, productCart) => {
|
||||||
|
const productCost = productCart.cost
|
||||||
|
const productQty = productCart.qty
|
||||||
|
return sum + productCost * productQty
|
||||||
|
}, 0);
|
||||||
|
return totalSum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get totalWeight(): number {
|
||||||
|
const totalWeight = this._products.reduce((sum, product) => {
|
||||||
|
const weight = product.weight
|
||||||
|
const qty = product.qty
|
||||||
|
if (weight) return sum + weight * qty
|
||||||
|
else return sum
|
||||||
|
}, 0)
|
||||||
|
return totalWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false
|
||||||
|
public get isLoading() {
|
||||||
|
return this._isLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPaymentMethod = (method: PaymentMethod) => {
|
||||||
|
console.log("setPaymentMethod", method)
|
||||||
|
if (method === PaymentMethods.Enum.Online) {
|
||||||
|
this._CartStages = addItem(this.cartStage3, this._CartStages)
|
||||||
|
} else {
|
||||||
|
this._CartStages = this._CartStages.filter(item => item.stage !== this.cartStage3.stage)
|
||||||
|
}
|
||||||
|
this._paymentMethod = this._paymentMethod.set(method)
|
||||||
|
}
|
||||||
|
setDeliveryPoint = (point: DeliveryPoint) => {
|
||||||
|
console.log("setDeliveryPoint", point)
|
||||||
|
this._deliveryPoint = this.deliveryPoint.set(point)
|
||||||
|
}
|
||||||
|
setDeliveryDate = (date: Date) => {
|
||||||
|
console.log("setDeliveryDate", date)
|
||||||
|
this._deliveryDate = this.deliveryDate.set(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeliveryMethod = (method: DeliveryMethod) => {
|
||||||
|
console.log("setDeliveryMethod", method)
|
||||||
|
|
||||||
|
if (method == DeliveryMethods.Enum.pickup) {
|
||||||
|
this._deliveryDate = this._deliveryDate.set(undefined)
|
||||||
|
this._deliveryPoint = this._deliveryPoint.set(undefined)
|
||||||
|
}
|
||||||
|
this._deliveryMethod = this.deliveryMethod.set(method)
|
||||||
|
}
|
||||||
|
|
||||||
|
isPreviosStageConfirmed = (currentStage: OrderingStage): boolean => {
|
||||||
|
const previosStage = this._CartStages[currentStage.stage - 1]
|
||||||
|
if (this.confirmedStage && this._confirmedStage!.stage >= previosStage.stage) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmStage = (stage: OrderingStage) => {
|
||||||
|
console.log("confirmStage", stage)
|
||||||
|
if (stage.stage == this._confirmedStage?.stage) {
|
||||||
|
console.log("same Stage")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch (stage.stage) {
|
||||||
|
case 0: {
|
||||||
|
// return this._products.length > 0
|
||||||
|
this._confirmedStage = stage
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
this.validateStage1()
|
||||||
|
const validateAll = !this._deliveryMethod.error && !this._paymentMethod.error
|
||||||
|
&& !this._deliveryPoint.error && !this._deliveryDate.error
|
||||||
|
if (validateAll) {
|
||||||
|
this._confirmedStage = stage
|
||||||
|
return true // todo send to server
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmed = (stage?: OrderingStage) => {
|
||||||
|
this._confirmedStage = stage
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentStage = (stage: OrderingStage) => {
|
||||||
|
this._currentStage = stage
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateStage1() {
|
||||||
|
const isPayment = PaymentMethods.safeParse(this._paymentMethod.data).success
|
||||||
|
this._paymentMethod = this._paymentMethod.validate(isPayment, strings.errors.choosePaymentMethod)
|
||||||
|
const isDelivery = DeliveryMethods.safeParse(this._deliveryMethod.data).success
|
||||||
|
this._deliveryMethod = this._deliveryMethod.validate(isDelivery, strings.errors.chooseDeliveryMethod)
|
||||||
|
if (this._deliveryMethod.data === DeliveryMethods.Enum.delivery) {
|
||||||
|
const isDeliveryPoint = DeliveryPointSchema.safeParse(this._deliveryPoint.data).success
|
||||||
|
this._deliveryPoint = this._deliveryPoint.validate(isDeliveryPoint, strings.errors.chooseDeliveryPoint)
|
||||||
|
const isDeliveryDate = this._deliveryDate.data instanceof Date
|
||||||
|
this._deliveryDate = this._deliveryDate.validate(isDeliveryDate, strings.errors.chooseDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCartFromServer = async () => {
|
||||||
|
try {
|
||||||
|
this._isLoading = true
|
||||||
|
const res = await this.fetchCartFromServer()
|
||||||
|
runInAction(() => {
|
||||||
|
this._products = res
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
runInAction(() => {
|
||||||
|
this._isLoading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setToCart = async (product: Product, productQty: number) => {
|
||||||
|
if (product) {
|
||||||
|
const currentValue = this._products.find(productCart => productCart.id === product.id)
|
||||||
|
if (currentValue) {
|
||||||
|
if (productQty === 0) this._products = this._products.filter(item => item !== currentValue)
|
||||||
|
if (productQty > 0) this._products = this._products.map(item => {
|
||||||
|
if (item.id === currentValue.id) return { ...item, qty: productQty }
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
} else if (productQty > 0) {
|
||||||
|
this._products.push({ ...product, qty: productQty })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFromCart = (id: string) => {
|
||||||
|
this._products = removeItemById(id, this._products)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortCart = (reverse: boolean) => {
|
||||||
|
this._products = this._products.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendCartToServer(card: CartProduct) {
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteCartFromServer(card: CartProduct) {
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchCartFromServer(): Promise<CartProduct[]> {
|
||||||
|
await sleep(300)
|
||||||
|
if (this._products) return this._products
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
mapFromTable(tableItem: TableAdapter) {
|
||||||
|
const cart: CartProduct = {
|
||||||
|
...tableItem
|
||||||
|
}
|
||||||
|
return cart
|
||||||
|
}
|
||||||
|
|
||||||
|
mapToTable(cartProduct: CartProduct): TableAdapter {
|
||||||
|
return {
|
||||||
|
...cartProduct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
src/shared/stores/cart.validate.ts
Normal file
15
src/shared/stores/cart.validate.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { OrderingStage } from "./cart.store"
|
||||||
|
|
||||||
|
export const validate = (stage: OrderingStage) => {
|
||||||
|
console.log("confirmStage", stage)
|
||||||
|
switch (stage.stage) {
|
||||||
|
case 0: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return validate
|
||||||
|
}
|
||||||
|
|
||||||
55
src/shared/stores/category.store.ts
Normal file
55
src/shared/stores/category.store.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
|
import { sleep } from "../utils/async.sleep"
|
||||||
|
|
||||||
|
export type Category = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
childs: string[],
|
||||||
|
parent?: string,
|
||||||
|
isAcive: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CategoryStore {
|
||||||
|
private _categories: Category[] = []
|
||||||
|
public get categories(): Category[] {
|
||||||
|
return this._categories
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectedCategory: string = ""
|
||||||
|
public get selectedCategory(): string {
|
||||||
|
return this._selectedCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isLoading = false
|
||||||
|
public get isLoading() {
|
||||||
|
return this._isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateCategories = async () => {
|
||||||
|
try {
|
||||||
|
this._isLoading = true
|
||||||
|
const res = await this.fetchCategoriesFromServer()
|
||||||
|
runInAction( () => {
|
||||||
|
this._categories = res
|
||||||
|
this._isLoading = false
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this._isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCategory = (categoryId: string) => {
|
||||||
|
if (this._selectedCategory === categoryId) this._selectedCategory = ''
|
||||||
|
else this._selectedCategory = categoryId
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchCategoriesFromServer(): Promise<Category[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
55
src/shared/stores/filters/filters.interface.ts
Normal file
55
src/shared/stores/filters/filters.interface.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const NumberFilterSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
defualtValue: z.union([
|
||||||
|
z.number().optional(),
|
||||||
|
z.tuple([z.number(), z.number()]).optional()
|
||||||
|
]),
|
||||||
|
min: z.number(),
|
||||||
|
max: z.number(),
|
||||||
|
range: z.boolean(),
|
||||||
|
defaultVisible: z.boolean().optional(),
|
||||||
|
priority: z.number().optional(),
|
||||||
|
// UI controlled
|
||||||
|
visible: z.boolean().optional(),
|
||||||
|
alwaysVisible: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type NumberFilter = z.infer<typeof NumberFilterSchema>
|
||||||
|
|
||||||
|
export const SelectValueSchema = z.object({
|
||||||
|
valueId: z.string(),
|
||||||
|
valueName: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SelectFilterSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
defualtValueId: z.string().optional(),
|
||||||
|
multi: z.boolean(),
|
||||||
|
values: z.array(SelectValueSchema),
|
||||||
|
defaultVisible: z.boolean().optional(),
|
||||||
|
priority: z.number().optional(),
|
||||||
|
// UI controlled
|
||||||
|
visible: z.boolean().optional(),
|
||||||
|
alwaysVisible: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SelectFilter = z.infer<typeof SelectFilterSchema>
|
||||||
|
|
||||||
|
export const SwitchFilterSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
defualtValue: z.boolean().optional(),
|
||||||
|
defaultVisible: z.boolean().optional(),
|
||||||
|
priority: z.number().optional(),
|
||||||
|
// UI controlled
|
||||||
|
visible: z.boolean().optional(),
|
||||||
|
alwaysVisible: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SwitchStore = z.infer<typeof SwitchFilterSchema>
|
||||||
|
|
||||||
|
export type ServerFilter = (SwitchStore | NumberFilter | SelectFilter)
|
||||||
88
src/shared/stores/filters/filters.store.ts
Normal file
88
src/shared/stores/filters/filters.store.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
|
import { sleep } from "../../utils/async.sleep"
|
||||||
|
import { ServerFilter } from "./filters.interface"
|
||||||
|
import { addItem, removeFilter, removeItem } from "../../utils/array.helper"
|
||||||
|
import { valueIsNotEmpty } from "../../utils/any.helper"
|
||||||
|
|
||||||
|
|
||||||
|
export class FiltersStore {
|
||||||
|
private _filters: ServerFilter[] = []
|
||||||
|
public get filters(): ServerFilter[] {
|
||||||
|
return this._filters
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showAll: boolean = false
|
||||||
|
public get showAll(): boolean {
|
||||||
|
return this._showAll
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isLoading = false
|
||||||
|
public get isLoading() {
|
||||||
|
return this._isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFilters = async (categoryId: string) => {
|
||||||
|
this._isLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.fetchFiltersFromServer(categoryId)
|
||||||
|
runInAction(() => {
|
||||||
|
this._filters = res.sort((a, b) => (a.priority || 0) - (b.priority || 0))
|
||||||
|
this.setDefaultVisible()
|
||||||
|
this._isLoading = false
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this._isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChangeFilter = (id: string, value: any) => {
|
||||||
|
const changedFilter = this._filters.find(filter => filter.id === id)
|
||||||
|
if (changedFilter) {
|
||||||
|
console.log(changedFilter)
|
||||||
|
if (valueIsNotEmpty(value)) this.setFilterVisible(changedFilter, true)
|
||||||
|
// todo add send state to product store and to update products from server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offAlwaysVisible = (id: string) => {
|
||||||
|
const changedFilter = this._filters.find(filter => filter.id === id)
|
||||||
|
if (changedFilter) {
|
||||||
|
this.setFilterVisible(changedFilter, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpoilerVisible = (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
this.showAllFilters()
|
||||||
|
} else {
|
||||||
|
this.setDefaultVisible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setFilterVisible = (filter: ServerFilter, value: boolean) => {
|
||||||
|
this._filters = this._filters.map(item => {
|
||||||
|
if (item.id === filter.id && !item.defaultVisible) return { ...item, visible: value, alwaysVisible: value }
|
||||||
|
else return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAllFilters = () => {
|
||||||
|
this._filters = this._filters.map(filter => ({ ...filter, visible: true }))
|
||||||
|
this._showAll = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDefaultVisible() {
|
||||||
|
this._filters = this._filters.map(filter => ({ ...filter, visible: filter.alwaysVisible ? filter.alwaysVisible : filter.defaultVisible }))
|
||||||
|
this._showAll = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchFiltersFromServer(categoryId: string): Promise<ServerFilter[]> {
|
||||||
|
await sleep(500)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
src/shared/stores/modal.store.ts
Normal file
69
src/shared/stores/modal.store.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
|
import RootStore from "./root.store"
|
||||||
|
import { Product } from "./product.store"
|
||||||
|
import { Resource } from "../utils/resource"
|
||||||
|
|
||||||
|
export class ModalStore {
|
||||||
|
|
||||||
|
private rootStore: RootStore
|
||||||
|
|
||||||
|
private _isFullImageOpened = false
|
||||||
|
public get isFullImageOpened() {
|
||||||
|
return this._isFullImageOpened
|
||||||
|
}
|
||||||
|
private _fullImageData: string[] = []
|
||||||
|
public get fullImageData(): string[] {
|
||||||
|
return this._fullImageData
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isProductDetailedOpened = false
|
||||||
|
public get isProductDetailedOpened() {
|
||||||
|
return this._isProductDetailedOpened
|
||||||
|
}
|
||||||
|
|
||||||
|
private _productDetailed = new Resource<Product>
|
||||||
|
public get productDetailed(): Resource<Product> {
|
||||||
|
return this._productDetailed
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.rootStore = rootStore
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
openFullImage = (image: string[]) => {
|
||||||
|
this._fullImageData = image
|
||||||
|
this._isFullImageOpened = true
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFullImage = () => {
|
||||||
|
this._fullImageData = []
|
||||||
|
this._isFullImageOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
closeProductDetailed = () => {
|
||||||
|
this._productDetailed = { isLoading: false, data: undefined }
|
||||||
|
this._isProductDetailedOpened = false
|
||||||
|
}
|
||||||
|
|
||||||
|
openProductDetailed = async (id: string) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this._productDetailed.isLoading = true
|
||||||
|
this._isProductDetailedOpened = true
|
||||||
|
})
|
||||||
|
const res = await this.rootStore.productStore.getProductDetailed(id) // todo make this one instance or aborted
|
||||||
|
runInAction(() => {
|
||||||
|
if (res) {
|
||||||
|
this._productDetailed = { ...this._productDetailed, data: res }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) this._productDetailed.error = error
|
||||||
|
} finally {
|
||||||
|
this._productDetailed.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
82
src/shared/stores/orders.store.ts
Normal file
82
src/shared/stores/orders.store.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { makeAutoObservable } from "mobx"
|
||||||
|
import { Resource } from "../utils/resource"
|
||||||
|
import { DeliveryPoint } from "./user.store"
|
||||||
|
import { Product } from "./product.store"
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export interface Order {
|
||||||
|
id: string, // uuid
|
||||||
|
number: string,
|
||||||
|
date: string,
|
||||||
|
name: string,
|
||||||
|
sum: number,
|
||||||
|
status: OrderStatus,
|
||||||
|
paymentStatus?: PaymentStatus,
|
||||||
|
paymentMethod?: PaymentMethod,
|
||||||
|
deliveryMethod?: DeliveryMethod,
|
||||||
|
deliveryPoint?: DeliveryPoint,
|
||||||
|
deliveryDate?: Date,
|
||||||
|
products?: Product[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrterProduct extends Product{
|
||||||
|
qty: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentMethods = z.enum([
|
||||||
|
'Cash',
|
||||||
|
'BankTransfer',
|
||||||
|
'Online',
|
||||||
|
])
|
||||||
|
export type PaymentMethod = z.infer<typeof PaymentMethods>
|
||||||
|
|
||||||
|
|
||||||
|
export const DeliveryMethods = z.enum(['pickup','delivery'])
|
||||||
|
export type DeliveryMethod = z.infer<typeof DeliveryMethods>
|
||||||
|
|
||||||
|
export enum OrderStatus {
|
||||||
|
Draft = 'draft',
|
||||||
|
Сonfirmed = 'confirmed',
|
||||||
|
Processed = 'processed',
|
||||||
|
Delivering = 'delivering',
|
||||||
|
Finished = 'finished',
|
||||||
|
Deleted = 'deleted',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PaymentStatus {
|
||||||
|
Paid = 'paid',
|
||||||
|
NotPaid = 'notpaid',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OrdersStore {
|
||||||
|
|
||||||
|
private _orders = new Resource<Order[]>
|
||||||
|
public get orders() {
|
||||||
|
return this._orders
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isLoading = false
|
||||||
|
public get isLoading() {
|
||||||
|
return this._isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrders = async () => {
|
||||||
|
this._isLoading = true
|
||||||
|
try {
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this._isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchOrdersFromServer () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
145
src/shared/stores/product.store.ts
Normal file
145
src/shared/stores/product.store.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
|
import { TableAdapter } from "../components/table.aps/ProductTable";
|
||||||
|
import { GridAdapter } from "../components/grid.aps/ProductGrid";
|
||||||
|
import { sleep } from "../utils/async.sleep";
|
||||||
|
import { CartProduct } from "./cart.store";
|
||||||
|
import RootStore from "./root.store";
|
||||||
|
|
||||||
|
export type Product = {
|
||||||
|
id: string,
|
||||||
|
number: number,
|
||||||
|
manufactory: string,
|
||||||
|
oem: string,
|
||||||
|
stock: number,
|
||||||
|
receipt_date: string,
|
||||||
|
name: string,
|
||||||
|
cost: number,
|
||||||
|
image: string[],
|
||||||
|
discount: boolean,
|
||||||
|
weight?: number,
|
||||||
|
properties?: ProductProperty[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductProperty = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
value: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProductSortTypes {
|
||||||
|
byName,
|
||||||
|
byCost,
|
||||||
|
byQty,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProductFilterTypes {
|
||||||
|
byCategory,
|
||||||
|
byName,
|
||||||
|
byCost,
|
||||||
|
byOem,
|
||||||
|
byManufactory,
|
||||||
|
byStock,
|
||||||
|
byDiscount
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProductStore {
|
||||||
|
private rootStore: RootStore
|
||||||
|
|
||||||
|
private _products: Product[] = []
|
||||||
|
public get products() {
|
||||||
|
return this._products
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isLoading = false
|
||||||
|
public get isLoading() {
|
||||||
|
return this._isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectedSroting: ProductSortTypes = ProductSortTypes.byName
|
||||||
|
public get selectedSroting(): ProductSortTypes {
|
||||||
|
return this._selectedSroting
|
||||||
|
}
|
||||||
|
public set selectedSorting(value: ProductSortTypes) {
|
||||||
|
this._selectedSroting = value
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.rootStore = rootStore
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProductFromServer = async () => {
|
||||||
|
try {
|
||||||
|
this._isLoading = true
|
||||||
|
const res = await this.fetchProductFromServer()
|
||||||
|
runInAction(() => {
|
||||||
|
this._products = res
|
||||||
|
this._isLoading = false
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this._isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchProductFromServer(): Promise<Product[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullImageLinks = async (productId: string) => {
|
||||||
|
const res = await this.fetchFullImageLinksFromServer(productId)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductDetailed = async(id: string| undefined) => {
|
||||||
|
if (id) {
|
||||||
|
const res = await this.fetchProductDetailedFromServer(id)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchProductDetailedFromServer(id: string): Promise<Product> {
|
||||||
|
// const main = serverProducts.find(product => product.id === id)
|
||||||
|
// const properties = detailedParams
|
||||||
|
// if (main) main.properties = properties
|
||||||
|
return {} as Product
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchFullImageLinksFromServer(productId: string) {
|
||||||
|
return this._products.find( product => product.id === productId)?.image
|
||||||
|
}
|
||||||
|
|
||||||
|
mapFromTable(tableItem: TableAdapter) {
|
||||||
|
const product: Product = {
|
||||||
|
id: tableItem.id,
|
||||||
|
number: tableItem.number,
|
||||||
|
manufactory: tableItem.manufactory,
|
||||||
|
oem: tableItem.oem,
|
||||||
|
stock: tableItem.stock,
|
||||||
|
receipt_date: tableItem.receipt_date,
|
||||||
|
name: tableItem.name,
|
||||||
|
cost: tableItem.cost,
|
||||||
|
image: tableItem.image,
|
||||||
|
discount: tableItem.discount,
|
||||||
|
}
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
|
mapToTable(products: Product[], cartProducts: CartProduct[]): TableAdapter[] {
|
||||||
|
return this.addQtyFromCardData(products, cartProducts)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapToGrid(products: Product[], cartProducts: CartProduct[]): GridAdapter[] {
|
||||||
|
return this.addQtyFromCardData(products, cartProducts)
|
||||||
|
}
|
||||||
|
|
||||||
|
private addQtyFromCardData(products: Product[], cartProducts: CartProduct[]) {
|
||||||
|
return products.map((product) => {
|
||||||
|
const cart = cartProducts.find(cart => cart.id === product.id)
|
||||||
|
if (cart) return { ...product, qty: cart.qty }
|
||||||
|
return { ...product, qty: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
34
src/shared/stores/root.store.ts
Normal file
34
src/shared/stores/root.store.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { CartStore } from "./cart.store";
|
||||||
|
import { CategoryStore } from "./category.store";
|
||||||
|
import { FiltersStore } from "./filters/filters.store";
|
||||||
|
import { ModalStore } from "./modal.store";
|
||||||
|
import { OrdersStore } from "./orders.store";
|
||||||
|
import { ProductStore } from "./product.store";
|
||||||
|
import { SideBarsStore } from "./sidebars.store";
|
||||||
|
import PostStore from "./test.store";
|
||||||
|
import { UserStore } from "./user.store";
|
||||||
|
|
||||||
|
class RootStore {
|
||||||
|
userStore: UserStore
|
||||||
|
productStore: ProductStore
|
||||||
|
cartStore: CartStore
|
||||||
|
postStore: PostStore
|
||||||
|
modalStore: ModalStore
|
||||||
|
categoryStore: CategoryStore
|
||||||
|
filtersStore: FiltersStore
|
||||||
|
sideBarsStore: SideBarsStore
|
||||||
|
ordersStore: OrdersStore
|
||||||
|
constructor() {
|
||||||
|
this.userStore = new UserStore()
|
||||||
|
this.productStore = new ProductStore(this)
|
||||||
|
this.cartStore = new CartStore()
|
||||||
|
this.postStore = new PostStore()
|
||||||
|
this.modalStore = new ModalStore(this)
|
||||||
|
this.categoryStore = new CategoryStore()
|
||||||
|
this.filtersStore = new FiltersStore()
|
||||||
|
this.sideBarsStore = new SideBarsStore()
|
||||||
|
this.ordersStore = new OrdersStore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RootStore
|
||||||
26
src/shared/stores/sidebars.store.ts
Normal file
26
src/shared/stores/sidebars.store.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { makeAutoObservable } from "mobx"
|
||||||
|
|
||||||
|
export class SideBarsStore {
|
||||||
|
private _leftSideBar: React.ReactNode = null
|
||||||
|
public get leftSideBar(): React.ReactNode {
|
||||||
|
return this._leftSideBar
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private _rightSideBar: React.ReactNode = null
|
||||||
|
public get rightSideBar(): React.ReactNode {
|
||||||
|
return this._rightSideBar
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
setRightSidebar = (value: React.ReactNode) => {
|
||||||
|
this._rightSideBar = value
|
||||||
|
}
|
||||||
|
|
||||||
|
setLeftSidebar = (value: React.ReactNode) => {
|
||||||
|
this._leftSideBar = value
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/shared/stores/test.store.ts
Normal file
38
src/shared/stores/test.store.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
|
|
||||||
|
|
||||||
|
//todo delete
|
||||||
|
type Posts = {
|
||||||
|
userId: number,
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPosts = async () =>
|
||||||
|
(await axios.get<Posts[]>("https://jsonplaceholder.typicode.com/posts")).data
|
||||||
|
|
||||||
|
|
||||||
|
class PostStore {
|
||||||
|
posts: Posts[] = []
|
||||||
|
isLoading = false
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostsAction = async () => {
|
||||||
|
try {
|
||||||
|
this.isLoading = true
|
||||||
|
const res = await getPosts()
|
||||||
|
runInAction( () => {
|
||||||
|
this.posts = res
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostStore
|
||||||
92
src/shared/stores/user.store.ts
Normal file
92
src/shared/stores/user.store.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
|
import { User } from "oidc-client-ts";
|
||||||
|
import { keycloakConfig } from "../services/keycloack";
|
||||||
|
import { Resource } from "../utils/resource"
|
||||||
|
import { sleep } from "../utils/async.sleep";
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
|
||||||
|
export interface UserServer {
|
||||||
|
id: string
|
||||||
|
shortName: string
|
||||||
|
firstName?: string
|
||||||
|
middleName?: string
|
||||||
|
lastName?: string
|
||||||
|
avatar?: string
|
||||||
|
email: string
|
||||||
|
phone: string
|
||||||
|
warehouse: string
|
||||||
|
defaultCondition: string
|
||||||
|
managerName?: string
|
||||||
|
managerPhone?: string
|
||||||
|
taxNumber?: string
|
||||||
|
deliveryPoints?: DeliveryPoint[]
|
||||||
|
}
|
||||||
|
export const DeliveryPointSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
schedule: z.string(),
|
||||||
|
address: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type DeliveryPoint = z.infer<typeof DeliveryPointSchema>
|
||||||
|
|
||||||
|
export class UserStore {
|
||||||
|
private _user: Resource<UserServer> = new Resource<UserServer>
|
||||||
|
public get user() {
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get fullname() {
|
||||||
|
return `${this._user.data?.firstName} ${this._user.data?.middleName} ${this._user.data?.lastName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public get userAbbrevation() {
|
||||||
|
const firstChar = this._user.data?.firstName ? this._user.data.firstName.charAt(0) : this._user.data?.shortName.charAt(0)
|
||||||
|
const secondChar = this._user.data?.middleName ? this._user.data.middleName.charAt(0) : this._user.data?.lastName?.charAt(0)
|
||||||
|
return `${firstChar}${secondChar}`
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser = async () => {
|
||||||
|
this._user.isLoading = true
|
||||||
|
const res = await this.fetchUserFromServer()
|
||||||
|
try {
|
||||||
|
runInAction( () => {
|
||||||
|
this._user = {...this._user, data: res}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this._user.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUserFromServer(): Promise<UserServer> {
|
||||||
|
await sleep(500)
|
||||||
|
return {} as UserServer
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionStorage() {
|
||||||
|
const oidcStorage = sessionStorage.getItem(`oidc.user:${keycloakConfig.authority}:${keycloakConfig.client_id}`)
|
||||||
|
if (!oidcStorage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return User.fromStorageString(oidcStorage)
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser() {
|
||||||
|
return this.getSessionStorage()?.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessToken() {
|
||||||
|
return this.getSessionStorage()?.access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserStore
|
||||||
4
src/shared/strings/header.menu.strings.ts
Normal file
4
src/shared/strings/header.menu.strings.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const headerMenu = {
|
||||||
|
home:"На главную",
|
||||||
|
test:"Тест",
|
||||||
|
}
|
||||||
14
src/shared/strings/product.strings.ts
Normal file
14
src/shared/strings/product.strings.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const productString = {
|
||||||
|
name: "Наименование",
|
||||||
|
cost: "Цена",
|
||||||
|
image: "Изображение",
|
||||||
|
qty: "Количество",
|
||||||
|
buy: "Купить",
|
||||||
|
number: "Артикул",
|
||||||
|
manufactory: "Производитель",
|
||||||
|
oem: "OEM",
|
||||||
|
stock: "Наличие",
|
||||||
|
receiptDate: "Дата поступления",
|
||||||
|
discount:"Скидка",
|
||||||
|
parameters: "Характеристики",
|
||||||
|
}
|
||||||
81
src/shared/strings/strings.ts
Normal file
81
src/shared/strings/strings.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
export const strings = {
|
||||||
|
// user section
|
||||||
|
aboutMe: "Обо мне",
|
||||||
|
settings: "Настройки",
|
||||||
|
changeTheme: "Изменить тему",
|
||||||
|
logout: "Выйти",
|
||||||
|
userProfile: "Профиль пользователя",
|
||||||
|
firstName: "Имя",
|
||||||
|
middleName: "Фамилия",
|
||||||
|
lastName: "Отчетство",
|
||||||
|
taxNumber: "ИНН",
|
||||||
|
phone: "Телефон",
|
||||||
|
managerName: "Имя менеджера",
|
||||||
|
managerPhone: "Телефон менеджера",
|
||||||
|
defaultContract: "Договор по умолчанию",
|
||||||
|
myShippingAddresses: "Мои адреса доставки:",
|
||||||
|
myConditions: "Мои условия:",
|
||||||
|
warehouse: "Склад",
|
||||||
|
name: "Имя",
|
||||||
|
schedule: "Расписание",
|
||||||
|
address: "Адрес",
|
||||||
|
edit: "Изменить",
|
||||||
|
error: "Ошибка",
|
||||||
|
discounts: "Скидки",
|
||||||
|
delivery: "Доставка",
|
||||||
|
selectDeliveryMethod: "Выберите метод доставки:",
|
||||||
|
pickUpByMyself: "Я заберу самостоятельно",
|
||||||
|
courierDelivery: "Доставка курьером",
|
||||||
|
deliveryPoint:"Адрес доставки",
|
||||||
|
selectYourDeliveryAddress: "Выберите свой адрес доставки:",
|
||||||
|
deliveryDate:"Дата доставки",
|
||||||
|
selectDeliveryDate: "Выберите дату доставки:",
|
||||||
|
enterQuantity: "Введите количество:",
|
||||||
|
quantity:"Количество",
|
||||||
|
tooltip_close:"нажмите Enter",
|
||||||
|
currency:"₽",
|
||||||
|
category: "Категории:",
|
||||||
|
collapse: "Свернуть",
|
||||||
|
hide: "Скрыть",
|
||||||
|
show: "Показать",
|
||||||
|
showAll: "Показать всё",
|
||||||
|
true: "Да",
|
||||||
|
false: "Нет",
|
||||||
|
// order section
|
||||||
|
cart: "Корзина",
|
||||||
|
order: "Заказ",
|
||||||
|
confirmOrder: "Подтвердить заказ",
|
||||||
|
orderParams: "Параметры",
|
||||||
|
chooseParams: "Выбрать параметры заказа",
|
||||||
|
payment: "Оплата",
|
||||||
|
inputPaymentValues: "Ввести платежные данные",
|
||||||
|
summary: "Общие итоги",
|
||||||
|
positions: "Позиции:",
|
||||||
|
weight: "Вес:",
|
||||||
|
total: "Итого:",
|
||||||
|
confirm: "Подтвердить",
|
||||||
|
next: "Далее",
|
||||||
|
back: "Назад",
|
||||||
|
paymentMethod: "Метод оплаты",
|
||||||
|
selectPaymentMethod: "Выберите метод оплаты",
|
||||||
|
cashToCourier: "Наличными курьеру",
|
||||||
|
bankTransfer: "Банковским переводом",
|
||||||
|
onlineByCard: "Онлайн банковской картой",
|
||||||
|
thanksForYourPurchase: "Спасибо за вашу покупку!",
|
||||||
|
orderConfirmed: (order: string) => `Ордер ${order} подтвержден!`,
|
||||||
|
goToMainPage: "Вернуться на главную",
|
||||||
|
goToOrder: "Посмотреть ордер",
|
||||||
|
retry: "Повторить",
|
||||||
|
youCanRetryOrGoToMain: "Вы можете повторить или вернуться на главную",
|
||||||
|
errors: {
|
||||||
|
somthengGoesWrong: "Что-то пошло не так",
|
||||||
|
cartIsEmpty: "Корзина пуста",
|
||||||
|
choosePaymentMethod: "Выберите метод оплаты",
|
||||||
|
chooseDeliveryMethod: "Выберите метод доставки",
|
||||||
|
chooseDeliveryPoint: "Выберите адрес доставки",
|
||||||
|
chooseDate: "Выберите дату",
|
||||||
|
403: "Извините у вас нет доступа",
|
||||||
|
404: "Извините мы не можем найти такую страницу",
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/shared/utils/any.helper.ts
Normal file
6
src/shared/utils/any.helper.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export function valueIsNotEmpty(value: any) {
|
||||||
|
if (value) {
|
||||||
|
return true
|
||||||
|
} else if( typeof value === 'boolean') return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
27
src/shared/utils/array.helper.ts
Normal file
27
src/shared/utils/array.helper.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export function removeItem<T extends {id: string}>( item:T, array:T[]): T [] {
|
||||||
|
const index = array.indexOf(item)
|
||||||
|
return array.filter( old => old.id !== item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeItemById<T extends {id: string}>( id:string, array:T[]): T [] {
|
||||||
|
return array.filter( old => old.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFilter<T extends {propertyId: string}>( item:T, array:T[]): T [] {
|
||||||
|
const index = array.indexOf(item)
|
||||||
|
return array.filter( old => old.propertyId !== item.propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addItem<T>( item:T, array:T[]): T [] {
|
||||||
|
return [...array, item]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class List<T> extends Array<T> {
|
||||||
|
delete(toDelete: T) {
|
||||||
|
return this.filter( item => item !== toDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(toAdd: T) {
|
||||||
|
return [...this, toAdd]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/shared/utils/async.sleep.ts
Normal file
5
src/shared/utils/async.sleep.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const sleep = (duration: number) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
18
src/shared/utils/mantine.size.convertor.ts
Normal file
18
src/shared/utils/mantine.size.convertor.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { px, useMantineTheme } from "@mantine/core"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return size in px number
|
||||||
|
* @param size use mantine size like md, lg, sm
|
||||||
|
*/
|
||||||
|
export const useMantineSize = (size: string) => {
|
||||||
|
const theme = useMantineTheme()
|
||||||
|
const hideSize = theme.fn.smallerThan(size)
|
||||||
|
const numberMatch = hideSize.match(/(\d+(\.\d+)?)(?=em)/)
|
||||||
|
|
||||||
|
if (numberMatch) {
|
||||||
|
const number = Number(numberMatch[0])
|
||||||
|
return px(`${number}rem`)
|
||||||
|
} else {
|
||||||
|
throw Error("Need em or rem mantine size number")
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/shared/utils/resize-observer.ts
Normal file
39
src/shared/utils/resize-observer.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { MutableRefObject, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
export function useResizeObserver(...refs: MutableRefObject<Element | null>[]) {
|
||||||
|
const [dimensions, setDimensions] = useState(
|
||||||
|
new Array(refs.length).fill({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
x: -Infinity,
|
||||||
|
y: -Infinity,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const resizeObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new ResizeObserver((entries) => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
setDimensions(entries.map((entry) => entry.contentRect));
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refs.forEach((ref) => {
|
||||||
|
if (ref.current) {
|
||||||
|
resizeObserver.observe(ref.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
refs.forEach((ref) => {
|
||||||
|
if (ref.current) {
|
||||||
|
resizeObserver.unobserve(ref.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [refs, resizeObserver]);
|
||||||
|
|
||||||
|
return dimensions;
|
||||||
|
}
|
||||||
5
src/shared/utils/resource.ts
Normal file
5
src/shared/utils/resource.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export class Resource<T> {
|
||||||
|
isLoading: boolean = false
|
||||||
|
data: T | undefined
|
||||||
|
error?: Error
|
||||||
|
}
|
||||||
32
src/shared/utils/validated.ts
Normal file
32
src/shared/utils/validated.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { makeAutoObservable } from "mobx"
|
||||||
|
|
||||||
|
export class Validated<T> {
|
||||||
|
private _data?: T | undefined
|
||||||
|
public get data(): T | undefined {
|
||||||
|
return this._data
|
||||||
|
}
|
||||||
|
public set data(value: T | undefined) {
|
||||||
|
this._data = value
|
||||||
|
}
|
||||||
|
error?: string
|
||||||
|
|
||||||
|
validate( condition: boolean, error: string): Validated<T> {
|
||||||
|
let newValidated = new Validated<T>()
|
||||||
|
if (!condition) {
|
||||||
|
newValidated.data = this._data
|
||||||
|
newValidated.error = error
|
||||||
|
return newValidated
|
||||||
|
}
|
||||||
|
newValidated.data = this._data
|
||||||
|
newValidated.error = undefined
|
||||||
|
return newValidated
|
||||||
|
}
|
||||||
|
|
||||||
|
set(data?: T) {
|
||||||
|
let newValidated = new Validated<T>()
|
||||||
|
newValidated.data = data
|
||||||
|
newValidated.error = this.error
|
||||||
|
return newValidated
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
src/widgets/LeftSideBar.tsx
Normal file
15
src/widgets/LeftSideBar.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SideBar, SideBarProps, } from '../shared/components/SideBar';
|
||||||
|
|
||||||
|
|
||||||
|
interface LeftSideBarProps {
|
||||||
|
isHidden: (isHidden: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeftSideBar = ({ isHidden }: LeftSideBarProps) => {
|
||||||
|
return (
|
||||||
|
<SideBar isHidden={isHidden} side="left" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeftSideBar;
|
||||||
18
src/widgets/RightSideBar.tsx
Normal file
18
src/widgets/RightSideBar.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SideBar, SideBarProps } from '../shared/components/SideBar';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
|
||||||
|
interface RightSideBarProps {
|
||||||
|
isHidden: (isHidden: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RightSideBar = ({ isHidden }: RightSideBarProps) => {
|
||||||
|
return (
|
||||||
|
<SideBar isHidden={isHidden} side="right">
|
||||||
|
</SideBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RightSideBar;
|
||||||
112
src/widgets/header/HeaderAction.tsx
Normal file
112
src/widgets/header/HeaderAction.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Burger, createStyles, Header, rem, Menu, Container, Group, Button, Flex } from "@mantine/core";
|
||||||
|
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
||||||
|
import UserMenu from '../../shared/components/UserMenu';
|
||||||
|
import { useAuth } from 'react-oidc-context';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
|
||||||
|
import Logo from "../../shared/components/Logo";
|
||||||
|
import { pathRoutes } from "../../router/routes.path";
|
||||||
|
|
||||||
|
const HEADER_HEIGHT = rem(60)
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
inner: {
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
leftMenu: {
|
||||||
|
flexWrap: "nowrap"
|
||||||
|
},
|
||||||
|
|
||||||
|
leftLinksMenu: {
|
||||||
|
[theme.fn.largerThan('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
[theme.fn.smallerThan('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
centerLinksMenu: {
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
[theme.fn.smallerThan('md')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
burger: {
|
||||||
|
[theme.fn.largerThan('sm')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
colorToggle: {
|
||||||
|
[theme.fn.smallerThan('md')]: {display:'none'}
|
||||||
|
}
|
||||||
|
|
||||||
|
}))
|
||||||
|
|
||||||
|
export interface HeaderActionProps {
|
||||||
|
links: { link: string; label: string; links: { link: string; label: string }[] }[],
|
||||||
|
logo?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderAction = ({ links }: HeaderActionProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [opened, { toggle }] = useDisclosure(false)
|
||||||
|
const isMiddleScreen = useMediaQuery('md')
|
||||||
|
const auth = useAuth()
|
||||||
|
|
||||||
|
const handleNavigate = (link: string) => {
|
||||||
|
navigate(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = links.map((link) => {
|
||||||
|
const menuItems = link.links?.map((item) => (
|
||||||
|
<Menu.Item key={item.link}>{item.label}</Menu.Item>
|
||||||
|
))
|
||||||
|
|
||||||
|
if (menuItems) {
|
||||||
|
return (
|
||||||
|
<Menu key={link.label} trigger="hover" transitionProps={{ exitDuration: 0 }} withinPortal>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button variant="subtle" uppercase onClick={() => handleNavigate(link.link)}>
|
||||||
|
{link.label}
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
</Menu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
|
||||||
|
<Container className={classes.inner} fluid>
|
||||||
|
<Flex wrap='nowrap' >
|
||||||
|
<Logo onClick={() => handleNavigate(pathRoutes.MAIN_PATH)} />
|
||||||
|
<Burger opened={opened} onClick={toggle} className={classes.burger} size="sm" />
|
||||||
|
<Flex className={classes.leftLinksMenu}>
|
||||||
|
{ items }
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Container className={classes.centerLinksMenu}>
|
||||||
|
{items}
|
||||||
|
</Container>
|
||||||
|
<Group position="right">
|
||||||
|
<ColorSchemeToggle className={classes.colorToggle} />
|
||||||
|
<UserMenu user={{ name: auth.user?.profile.preferred_username || "", image: "" }} />
|
||||||
|
</Group>
|
||||||
|
</Container>
|
||||||
|
</Header >
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user