From ece046d2fd3f57db3f57f595606495c6498c55cf Mon Sep 17 00:00:00 2001 From: NlightN22 Date: Sat, 17 Feb 2024 22:57:32 +0700 Subject: [PATCH] init --- .gitignore | 30 + README.md | 30 + nginx/default.conf | 8 + package.json | 69 + public/favicon.svg | 3 + public/index.html | 43 + public/logo.svg | 3 + public/manifest.json | 20 + public/robots.txt | 3 + src/App.test.tsx | 9 + src/App.tsx | 73 + src/AppBody.tsx | 49 + src/index.tsx | 31 + src/logo.svg | 13 + src/pages/403.tsx | 32 + src/pages/404.tsx | 32 + src/pages/Admin.tsx | 11 + src/pages/MainBody.tsx | 63 + src/pages/RetryError.tsx | 40 + src/pages/Test.tsx | 52 + src/react-app-env.d.ts | 1 + src/reportWebVitals.ts | 15 + src/router/AppRouter.tsx | 17 + src/router/frigate.routes.ts | 5 + src/router/routes.path.ts | 10 + src/router/routes.tsx | 40 + src/setupTests.ts | 5 + src/shared/components/CenterLoader.tsx | 9 + src/shared/components/CloseWithTooltip.tsx | 22 + src/shared/components/CogWheelWithText.tsx | 36 + src/shared/components/ColorSchemeToggle.tsx | 23 + src/shared/components/Currency.tsx | 13 + src/shared/components/DeliveryMethodRadio.tsx | 37 + src/shared/components/DeliveryPointRadio.tsx | 43 + src/shared/components/FullImageModal.tsx | 110 + src/shared/components/FullProductModal.tsx | 134 + src/shared/components/HeadSearch.tsx | 26 + .../components/ImageWithPlaceHolder.tsx | 23 + src/shared/components/InputModal.tsx | 90 + src/shared/components/JSMpegPlayer.tsx | 100 + src/shared/components/Logo.tsx | 12 + src/shared/components/OrderStepper.tsx | 36 + src/shared/components/OrderTotals.tsx | 73 + src/shared/components/PaymentMehodRadio.tsx | 34 + src/shared/components/PriceText.tsx | 24 + src/shared/components/ProductParameter.tsx | 27 + src/shared/components/SideBar.tsx | 115 + src/shared/components/SideBarLoader.tsx | 13 + src/shared/components/SideButton.tsx | 67 + src/shared/components/TreeLink.tsx | 30 + src/shared/components/UserMenu.tsx | 73 + src/shared/components/ViewSelector.tsx | 45 + .../filters.aps/MultiSelectFilter.tsx | 53 + .../filters.aps/OneSelectFilter.tsx | 52 + .../filters.aps/RangeSliderFilter.tsx | 43 + .../components/filters.aps/SliderFilter.tsx | 48 + .../components/filters.aps/SwitchFilter.tsx | 44 + .../components/grid.aps/BuyCounterToggle.tsx | 50 + .../components/grid.aps/CardCarousel.tsx | 31 + src/shared/components/grid.aps/GridCard.tsx | 184 + .../components/grid.aps/ProductGrid.tsx | 36 + src/shared/components/grid.aps/ProfileRow.tsx | 23 + .../components/svg/CogWheelHeartSVG.tsx | 22 + .../components/svg/ExclamationCogWheel.tsx | 20 + src/shared/components/svg/СogwheelSVG.tsx | 38 + .../table.aps/DeliveryPointsTable.tsx | 95 + .../components/table.aps/ProductTable.tsx | 156 + .../table.aps/ProductsTableHead.tsx | 53 + .../components/table.aps/RowCounter.tsx | 73 + src/shared/components/table.aps/SortedTh.tsx | 31 + src/shared/components/table.aps/TableRow.tsx | 129 + src/shared/components/СogwheelLoader.tsx | 14 + src/shared/dimensions/dimensions.ts | 6 + src/shared/env.const.ts | 29 + src/shared/services/keycloack.ts | 15 + src/shared/stores/cart.store.ts | 246 + src/shared/stores/cart.validate.ts | 15 + src/shared/stores/category.store.ts | 55 + .../stores/filters/filters.interface.ts | 55 + src/shared/stores/filters/filters.store.ts | 88 + src/shared/stores/modal.store.ts | 69 + src/shared/stores/orders.store.ts | 82 + src/shared/stores/product.store.ts | 145 + src/shared/stores/root.store.ts | 34 + src/shared/stores/sidebars.store.ts | 26 + src/shared/stores/test.store.ts | 38 + src/shared/stores/user.store.ts | 92 + src/shared/strings/header.menu.strings.ts | 4 + src/shared/strings/product.strings.ts | 14 + src/shared/strings/strings.ts | 81 + src/shared/utils/any.helper.ts | 6 + src/shared/utils/array.helper.ts | 27 + src/shared/utils/async.sleep.ts | 5 + src/shared/utils/mantine.size.convertor.ts | 18 + src/shared/utils/resize-observer.ts | 39 + src/shared/utils/resource.ts | 5 + src/shared/utils/validated.ts | 32 + src/widgets/LeftSideBar.tsx | 15 + src/widgets/RightSideBar.tsx | 18 + src/widgets/header/HeaderAction.tsx | 112 + src/widgets/header/header.links.ts | 11 + tsconfig.json | 26 + yarn.lock | 10037 ++++++++++++++++ 103 files changed, 14562 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 nginx/default.conf create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 public/index.html create mode 100644 public/logo.svg create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 src/App.test.tsx create mode 100644 src/App.tsx create mode 100644 src/AppBody.tsx create mode 100644 src/index.tsx create mode 100644 src/logo.svg create mode 100644 src/pages/403.tsx create mode 100644 src/pages/404.tsx create mode 100644 src/pages/Admin.tsx create mode 100644 src/pages/MainBody.tsx create mode 100644 src/pages/RetryError.tsx create mode 100644 src/pages/Test.tsx create mode 100644 src/react-app-env.d.ts create mode 100644 src/reportWebVitals.ts create mode 100644 src/router/AppRouter.tsx create mode 100644 src/router/frigate.routes.ts create mode 100644 src/router/routes.path.ts create mode 100644 src/router/routes.tsx create mode 100644 src/setupTests.ts create mode 100644 src/shared/components/CenterLoader.tsx create mode 100644 src/shared/components/CloseWithTooltip.tsx create mode 100644 src/shared/components/CogWheelWithText.tsx create mode 100644 src/shared/components/ColorSchemeToggle.tsx create mode 100644 src/shared/components/Currency.tsx create mode 100644 src/shared/components/DeliveryMethodRadio.tsx create mode 100644 src/shared/components/DeliveryPointRadio.tsx create mode 100644 src/shared/components/FullImageModal.tsx create mode 100644 src/shared/components/FullProductModal.tsx create mode 100644 src/shared/components/HeadSearch.tsx create mode 100644 src/shared/components/ImageWithPlaceHolder.tsx create mode 100644 src/shared/components/InputModal.tsx create mode 100644 src/shared/components/JSMpegPlayer.tsx create mode 100644 src/shared/components/Logo.tsx create mode 100644 src/shared/components/OrderStepper.tsx create mode 100644 src/shared/components/OrderTotals.tsx create mode 100644 src/shared/components/PaymentMehodRadio.tsx create mode 100644 src/shared/components/PriceText.tsx create mode 100644 src/shared/components/ProductParameter.tsx create mode 100644 src/shared/components/SideBar.tsx create mode 100644 src/shared/components/SideBarLoader.tsx create mode 100644 src/shared/components/SideButton.tsx create mode 100644 src/shared/components/TreeLink.tsx create mode 100644 src/shared/components/UserMenu.tsx create mode 100644 src/shared/components/ViewSelector.tsx create mode 100644 src/shared/components/filters.aps/MultiSelectFilter.tsx create mode 100644 src/shared/components/filters.aps/OneSelectFilter.tsx create mode 100644 src/shared/components/filters.aps/RangeSliderFilter.tsx create mode 100644 src/shared/components/filters.aps/SliderFilter.tsx create mode 100644 src/shared/components/filters.aps/SwitchFilter.tsx create mode 100644 src/shared/components/grid.aps/BuyCounterToggle.tsx create mode 100644 src/shared/components/grid.aps/CardCarousel.tsx create mode 100644 src/shared/components/grid.aps/GridCard.tsx create mode 100644 src/shared/components/grid.aps/ProductGrid.tsx create mode 100644 src/shared/components/grid.aps/ProfileRow.tsx create mode 100644 src/shared/components/svg/CogWheelHeartSVG.tsx create mode 100644 src/shared/components/svg/ExclamationCogWheel.tsx create mode 100644 src/shared/components/svg/СogwheelSVG.tsx create mode 100644 src/shared/components/table.aps/DeliveryPointsTable.tsx create mode 100644 src/shared/components/table.aps/ProductTable.tsx create mode 100644 src/shared/components/table.aps/ProductsTableHead.tsx create mode 100644 src/shared/components/table.aps/RowCounter.tsx create mode 100644 src/shared/components/table.aps/SortedTh.tsx create mode 100644 src/shared/components/table.aps/TableRow.tsx create mode 100644 src/shared/components/СogwheelLoader.tsx create mode 100644 src/shared/dimensions/dimensions.ts create mode 100644 src/shared/env.const.ts create mode 100644 src/shared/services/keycloack.ts create mode 100644 src/shared/stores/cart.store.ts create mode 100644 src/shared/stores/cart.validate.ts create mode 100644 src/shared/stores/category.store.ts create mode 100644 src/shared/stores/filters/filters.interface.ts create mode 100644 src/shared/stores/filters/filters.store.ts create mode 100644 src/shared/stores/modal.store.ts create mode 100644 src/shared/stores/orders.store.ts create mode 100644 src/shared/stores/product.store.ts create mode 100644 src/shared/stores/root.store.ts create mode 100644 src/shared/stores/sidebars.store.ts create mode 100644 src/shared/stores/test.store.ts create mode 100644 src/shared/stores/user.store.ts create mode 100644 src/shared/strings/header.menu.strings.ts create mode 100644 src/shared/strings/product.strings.ts create mode 100644 src/shared/strings/strings.ts create mode 100644 src/shared/utils/any.helper.ts create mode 100644 src/shared/utils/array.helper.ts create mode 100644 src/shared/utils/async.sleep.ts create mode 100644 src/shared/utils/mantine.size.convertor.ts create mode 100644 src/shared/utils/resize-observer.ts create mode 100644 src/shared/utils/resource.ts create mode 100644 src/shared/utils/validated.ts create mode 100644 src/widgets/LeftSideBar.tsx create mode 100644 src/widgets/RightSideBar.tsx create mode 100644 src/widgets/header/HeaderAction.tsx create mode 100644 src/widgets/header/header.links.ts create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90ba67d --- /dev/null +++ b/.gitignore @@ -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* diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c66c5c --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/nginx/default.conf b/nginx/default.conf new file mode 100644 index 0000000..72a3cd5 --- /dev/null +++ b/nginx/default.conf @@ -0,0 +1,8 @@ +server { + listen 80; + listen [::]:80; + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..83971f1 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..3d01f2a --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..b309316 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + Multi Frigate + + + +
+ + + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..3d01f2a --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..d225874 --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..2a68616 --- /dev/null +++ b/src/App.test.tsx @@ -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(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..da362b8 --- /dev/null +++ b/src/App.tsx @@ -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(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 + // } + // if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) { + // return + // } + + return ( +
+ + ({ + // placeholder: { + // backgroundColor: 'transparent', + // } + // }) + // }, + } + }} + > + + + + + + + +
+ ); +} +export default App; diff --git a/src/AppBody.tsx b/src/AppBody.tsx new file mode 100644 index 0000000..e033b0e --- /dev/null +++ b/src/AppBody.tsx @@ -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 ( + + } + navbar={ + + } + > + + + ) +}; + +export default AppBody; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..7df20e3 --- /dev/null +++ b/src/index.tsx @@ -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) + +root.render( + + + + + + + +); + +// 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(); + + diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..6b29593 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/pages/403.tsx b/src/pages/403.tsx new file mode 100644 index 0000000..d8b42e6 --- /dev/null +++ b/src/pages/403.tsx @@ -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 ( + + {strings.errors[403]} + + + + + ); +}; + +export default Forbidden; \ No newline at end of file diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..50685dc --- /dev/null +++ b/src/pages/404.tsx @@ -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 ( + + {strings.errors[404]} + + + + + ); +}; + +export default NotFound; \ No newline at end of file diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx new file mode 100644 index 0000000..b316e63 --- /dev/null +++ b/src/pages/Admin.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const AdminPage = () => { + return ( +
+ +
+ ); +}; + +export default AdminPage; \ No newline at end of file diff --git a/src/pages/MainBody.tsx b/src/pages/MainBody.tsx new file mode 100644 index 0000000..843a538 --- /dev/null +++ b/src/pages/MainBody.tsx @@ -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(
) + }, []) + + if (productsLoading || cardLoading) return + if (productsLoading || cardLoading) return
Error
// add state manager + + + let tableData: TableAdapter[] = [] + let gridData: GridAdapter[] = [] + if (products && cartProducts) { + tableData = productStore.mapToTable(products, cartProducts) + gridData = productStore.mapToGrid(products, cartProducts) + } + + return ( + + + + + + + + + + + ); +}) + +export default MainBody; \ No newline at end of file diff --git a/src/pages/RetryError.tsx b/src/pages/RetryError.tsx new file mode 100644 index 0000000..908d3b4 --- /dev/null +++ b/src/pages/RetryError.tsx @@ -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): void { + throw new Error('Function not implemented.'); + } + + return ( + + {strings.errors.somthengGoesWrong} + {ExclamationCogWheel} + {strings.youCanRetryOrGoToMain} + + + + ); +}; + +export default RetryError; \ No newline at end of file diff --git a/src/pages/Test.tsx b/src/pages/Test.tsx new file mode 100644 index 0000000..9b47588 --- /dev/null +++ b/src/pages/Test.tsx @@ -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 ( + +
+ + + +
+
+ ); +}) + +export default Test; \ No newline at end of file diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts new file mode 100644 index 0000000..49a2a16 --- /dev/null +++ b/src/reportWebVitals.ts @@ -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; diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx new file mode 100644 index 0000000..fb17456 --- /dev/null +++ b/src/router/AppRouter.tsx @@ -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.map(({ path, component }) => + + )} + } /> + + ) +} + +export default AppRouter \ No newline at end of file diff --git a/src/router/frigate.routes.ts b/src/router/frigate.routes.ts new file mode 100644 index 0000000..6a383f6 --- /dev/null +++ b/src/router/frigate.routes.ts @@ -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}` +} \ No newline at end of file diff --git a/src/router/routes.path.ts b/src/router/routes.path.ts new file mode 100644 index 0000000..e62865b --- /dev/null +++ b/src/router/routes.path.ts @@ -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', +} \ No newline at end of file diff --git a/src/router/routes.tsx b/src/router/routes.tsx new file mode 100644 index 0000000..bde54d1 --- /dev/null +++ b/src/router/routes.tsx @@ -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: , + }, + { + path: pathRoutes.ADMIN_PATH, + component: , + }, + { + path: pathRoutes.MAIN_PATH, + component: , + }, + { + path: pathRoutes.RETRY_ERROR_PATH, + component: , + }, + { + path: pathRoutes.FORBIDDEN_ERROR_PATH, + component: , + }, + { + path: pathRoutes.NOT_FOUND_ERROR_PATH, + component: , + }, +] \ No newline at end of file diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/src/setupTests.ts @@ -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'; diff --git a/src/shared/components/CenterLoader.tsx b/src/shared/components/CenterLoader.tsx new file mode 100644 index 0000000..397be5f --- /dev/null +++ b/src/shared/components/CenterLoader.tsx @@ -0,0 +1,9 @@ +import { DEFAULT_THEME, Loader, LoadingOverlay } from '@mantine/core'; +import React from 'react'; +import СogwheelSVG from './svg/СogwheelSVG'; + +const CenterLoader = () => { + return ; +}; + +export default CenterLoader; \ No newline at end of file diff --git a/src/shared/components/CloseWithTooltip.tsx b/src/shared/components/CloseWithTooltip.tsx new file mode 100644 index 0000000..bd09211 --- /dev/null +++ b/src/shared/components/CloseWithTooltip.tsx @@ -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 ( + + + + ); +}; + +export default CloseWithTooltip; \ No newline at end of file diff --git a/src/shared/components/CogWheelWithText.tsx b/src/shared/components/CogWheelWithText.tsx new file mode 100644 index 0000000..d5541ca --- /dev/null +++ b/src/shared/components/CogWheelWithText.tsx @@ -0,0 +1,36 @@ +import { Center, Text } from '@mantine/core'; +import React from 'react'; + +interface CogWheelWithTextProps { + text: string +} + +const CogWheelWithText = ({ text }: CogWheelWithTextProps) => { + return ( +
+ + + + {text} +
+ ); +}; + +export default CogWheelWithText; \ No newline at end of file diff --git a/src/shared/components/ColorSchemeToggle.tsx b/src/shared/components/ColorSchemeToggle.tsx new file mode 100644 index 0000000..46f1ea3 --- /dev/null +++ b/src/shared/components/ColorSchemeToggle.tsx @@ -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 ( + toggleColorScheme()} + size="lg" + onLabel={} + offLabel={} + /> + ); +}; + +export default ColorSchemeToggle; \ No newline at end of file diff --git a/src/shared/components/Currency.tsx b/src/shared/components/Currency.tsx new file mode 100644 index 0000000..bbdaa2d --- /dev/null +++ b/src/shared/components/Currency.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Text, TextProps } from '@mantine/core' +import { strings } from '../strings/strings'; + +const Currency = (props: TextProps) => { + return ( + + {strings.currency} + + ); +}; + +export default Currency; \ No newline at end of file diff --git a/src/shared/components/DeliveryMethodRadio.tsx b/src/shared/components/DeliveryMethodRadio.tsx new file mode 100644 index 0000000..c9b9120 --- /dev/null +++ b/src/shared/components/DeliveryMethodRadio.tsx @@ -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 ( + + + {deliveryAvailable ? + + : <> + } + + + + ); +}; + +export default DeliveryMethodRadio; \ No newline at end of file diff --git a/src/shared/components/DeliveryPointRadio.tsx b/src/shared/components/DeliveryPointRadio.tsx new file mode 100644 index 0000000..7572366 --- /dev/null +++ b/src/shared/components/DeliveryPointRadio.tsx @@ -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 => ( + + )) + + return ( + + + {radios} + + + ); +}; + +export default DeliveryPointRadio; \ No newline at end of file diff --git a/src/shared/components/FullImageModal.tsx b/src/shared/components/FullImageModal.tsx new file mode 100644 index 0000000..db854a9 --- /dev/null +++ b/src/shared/components/FullImageModal.tsx @@ -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(null) + useAnimationOffsetEffect(embla, TRANSITION_DURATION) + + const isMobile = useMediaQuery(dimensions.mobileSize) + + const handleClose = () => { + closeFullImage() + } + + const slides = fullImageData.length > 0 + ? + fullImageData.map((image) => ( + )) + : + + + return ( + + + {slides} + + + + + ); +}) + +export default FullImageModal; \ No newline at end of file diff --git a/src/shared/components/FullProductModal.tsx b/src/shared/components/FullProductModal.tsx new file mode 100644 index 0000000..338cd0f --- /dev/null +++ b/src/shared/components/FullProductModal.tsx @@ -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(null) + useAnimationOffsetEffect(embla, TRANSITION_DURATION) + + + const handleClose = () => { + closeProductDetailed() + } + + const slides = productDetailed.data && productDetailed.data.image.length > 0 + ? + productDetailed.data.image.map((image) => ( + )) + : null + + const properties = productDetailed.data?.properties?.map( property => ( + + )) + + return ( + + { + productDetailed.isLoading ? + + : +
+ + {/* */} + {/* <> */} + } + nextControlIcon={} + controlSize={40} + classNames={{ + root: classes.carousel, + controls: classes.carouselControls, + indicator: classes.carouselIndicator, + }} + > + {slides} + + + {/* Base product parameters */} + + + + + + + {/* Product properties */} + {properties} + {/* {productDetailed.data?.properties? JSON.stringify(productDetailed.data?.properties) : null} */} + + +
+ // {/* */} + } +
+ ); +}) + +export default FullProductModal; \ No newline at end of file diff --git a/src/shared/components/HeadSearch.tsx b/src/shared/components/HeadSearch.tsx new file mode 100644 index 0000000..141e1e2 --- /dev/null +++ b/src/shared/components/HeadSearch.tsx @@ -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 ( + <> + } + value={search} + onChange={handleSearchChange} + /> + + ); +}; + +export default HeadSearch; \ No newline at end of file diff --git a/src/shared/components/ImageWithPlaceHolder.tsx b/src/shared/components/ImageWithPlaceHolder.tsx new file mode 100644 index 0000000..bbc7cec --- /dev/null +++ b/src/shared/components/ImageWithPlaceHolder.tsx @@ -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) => { + if (props.src) return ( + + ) + return ( +
{ + e.stopPropagation() + }}> + { + e.stopPropagation() + }} + /> +
+ ) +} + +export default ImageWithPlaceHolder \ No newline at end of file diff --git a/src/shared/components/InputModal.tsx b/src/shared/components/InputModal.tsx new file mode 100644 index 0000000..08fa9c1 --- /dev/null +++ b/src/shared/components/InputModal.tsx @@ -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 = useRef(null) + + const handeLoaded = (event: FocusEvent) => { + 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 ( + + +
{strings.enterQuantity}
+ +
+ 0 ? handeClear()}> : null // todo move to textinput + + +
+ +
+
+
+ } + /> +
+ ); +}; + +export default InputModal; \ No newline at end of file diff --git a/src/shared/components/JSMpegPlayer.tsx b/src/shared/components/JSMpegPlayer.tsx new file mode 100644 index 0000000..d5f96ea --- /dev/null +++ b/src/shared/components/JSMpegPlayer.tsx @@ -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(null); + const containerRef = useRef(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 ( +
+
+
+ ); +} \ No newline at end of file diff --git a/src/shared/components/Logo.tsx b/src/shared/components/Logo.tsx new file mode 100644 index 0000000..3162314 --- /dev/null +++ b/src/shared/components/Logo.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Image, ImageProps } from '@mantine/core'; + +const Logo = ({ onClick }: ImageProps) => { + const src = "../logo.svg" + + return ( + Logo + ); +}; + +export default Logo; \ No newline at end of file diff --git a/src/shared/components/OrderStepper.tsx b/src/shared/components/OrderStepper.tsx new file mode 100644 index 0000000..d7af159 --- /dev/null +++ b/src/shared/components/OrderStepper.tsx @@ -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 ( + + + + + {paymentMethod.data === PaymentMethods.Enum.Online ? + + : <> + } + + + ) +}) + +export default OrderStepper; \ No newline at end of file diff --git a/src/shared/components/OrderTotals.tsx b/src/shared/components/OrderTotals.tsx new file mode 100644 index 0000000..9f3f4d5 --- /dev/null +++ b/src/shared/components/OrderTotals.tsx @@ -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 + } + + const okButton = () => { + const lastStageIndex = CartStages.length - 1 + if (confirmedStage?.stage === CartStages[lastStageIndex].stage) { + return <> + } + return ( + + ) + } + + return ( + + {strings.summary} + + {strings.positions} + {products.length} + + {strings.weight} + {totalWeight} + + {strings.total} + + + {okButton()} + {backButton()} + + ); +}; + +export default OrderTotals; \ No newline at end of file diff --git a/src/shared/components/PaymentMehodRadio.tsx b/src/shared/components/PaymentMehodRadio.tsx new file mode 100644 index 0000000..0bf9e11 --- /dev/null +++ b/src/shared/components/PaymentMehodRadio.tsx @@ -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 ( + + + + + + + + ); +}; + +export default PaymentMehodRadio; \ No newline at end of file diff --git a/src/shared/components/PriceText.tsx b/src/shared/components/PriceText.tsx new file mode 100644 index 0000000..28a4d8e --- /dev/null +++ b/src/shared/components/PriceText.tsx @@ -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 ( + + {Intl.NumberFormat().format(props.value)} + + ); +}; + +export default PriceText; \ No newline at end of file diff --git a/src/shared/components/ProductParameter.tsx b/src/shared/components/ProductParameter.tsx new file mode 100644 index 0000000..fb2d01b --- /dev/null +++ b/src/shared/components/ProductParameter.tsx @@ -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 ( + <> + + {paramName} + + + {paramValue} + + + + ); +}; + +export default ProductParameter; \ No newline at end of file diff --git a/src/shared/components/SideBar.tsx b/src/shared/components/SideBar.tsx new file mode 100644 index 0000000..22ca829 --- /dev/null +++ b/src/shared/components/SideBar.tsx @@ -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 = 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(() => { + if (children && side === 'left') return children + else if (sideBarsStore.leftSideBar) return sideBarsStore.leftSideBar + return null + }) + const [rightChildren, setRightChildren] = useState(() => { + 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 ( +
+ { + side === 'left' ? + + + {leftChildren} + + : + + } + + handleClickVisible(true)} /> +
+ ) +}) + diff --git a/src/shared/components/SideBarLoader.tsx b/src/shared/components/SideBarLoader.tsx new file mode 100644 index 0000000..14ecaba --- /dev/null +++ b/src/shared/components/SideBarLoader.tsx @@ -0,0 +1,13 @@ +import { Box, LoadingOverlay } from '@mantine/core'; +import React from 'react'; +import СogwheelSVG from './svg/СogwheelSVG'; + +const SideBarLoader = () => { + return ( + + + + ); +}; + +export default SideBarLoader; \ No newline at end of file diff --git a/src/shared/components/SideButton.tsx b/src/shared/components/SideButton.tsx new file mode 100644 index 0000000..e266804 --- /dev/null +++ b/src/shared/components/SideButton.tsx @@ -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 ( + + + ); +}; + diff --git a/src/shared/components/TreeLink.tsx b/src/shared/components/TreeLink.tsx new file mode 100644 index 0000000..acb7d02 --- /dev/null +++ b/src/shared/components/TreeLink.tsx @@ -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 ( + onClick(id)} + defaultOpened={false} + > + {children} + + ); +}; + +export default TreeLink; \ No newline at end of file diff --git a/src/shared/components/UserMenu.tsx b/src/shared/components/UserMenu.tsx new file mode 100644 index 0000000..c904124 --- /dev/null +++ b/src/shared/components/UserMenu.tsx @@ -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 ( + setUserMenuOpened(false)} + onOpen={() => setUserMenuOpened(true)} + withinPortal + > + + + + + { + isMiddleScreen ? + + {strings.changeTheme} + + + : + <> + } + + {strings.settings} + + + {strings.aboutMe} + + + {strings.logout} + + + + ); +}; + +export default UserMenu; \ No newline at end of file diff --git a/src/shared/components/ViewSelector.tsx b/src/shared/components/ViewSelector.tsx new file mode 100644 index 0000000..046dace --- /dev/null +++ b/src/shared/components/ViewSelector.tsx @@ -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 ( + + + + )}, + { + value: SelectorViewState.GRID, + label: ( +
+ +
+ )}, + ]} + /> + ); +}; + +export default ViewSelector; \ No newline at end of file diff --git a/src/shared/components/filters.aps/MultiSelectFilter.tsx b/src/shared/components/filters.aps/MultiSelectFilter.tsx new file mode 100644 index 0000000..df58e6b --- /dev/null +++ b/src/shared/components/filters.aps/MultiSelectFilter.tsx @@ -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 + label?: string + defaultValue?: string[] + textClassName?: string + selectProps?: MultiSelectProps, + display?: SystemProp + 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 ( + + + {label} + {showClose ? + + : null} + + + + ) +}; + +export default MultiSelectFilter; \ No newline at end of file diff --git a/src/shared/components/filters.aps/OneSelectFilter.tsx b/src/shared/components/filters.aps/OneSelectFilter.tsx new file mode 100644 index 0000000..a293f17 --- /dev/null +++ b/src/shared/components/filters.aps/OneSelectFilter.tsx @@ -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 + label?: string + defaultValue?: string + textClassName?: string + selectProps?: SelectProps, + display?: SystemProp + 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 ( + + + {label} + {showClose ? + : null} + +