ilya_shyian 2 vuotta sitten
vanhempi
commit
3660ec961b
36 muutettua tiedostoa jossa 4496 lisäystä ja 4963 poistoa
  1. 3556 4928
      package-lock.json
  2. 6 0
      package.json
  3. 15 18
      src/App.js
  4. 13 0
      src/actions/actionPageStart.js
  5. 96 0
      src/actions/actionRootCats.js
  6. 0 0
      src/components/MainPage/index.js
  7. 41 0
      src/components/Root/index.js
  8. 7 0
      src/components/admin/AdminLayoutPage/index.js
  9. 15 0
      src/components/common/Categories/Category.js
  10. 20 0
      src/components/common/Categories/index.js
  11. 15 0
      src/components/common/ProtectedRoute/index.js
  12. 100 0
      src/components/common/SearchBar/SearchBar.js
  13. 19 0
      src/components/common/SearchBar/SearchCategoryResultItem.js
  14. 29 0
      src/components/common/SearchBar/SearchGoodResultItem.js
  15. 52 0
      src/components/common/SearchBar/SearchResults.js
  16. 7 0
      src/components/common/SearchBar/index.js
  17. 0 0
      src/components/layout/Aside/CAdminCategories.js
  18. 8 0
      src/components/layout/Aside/CCategories.js
  19. 18 0
      src/components/layout/Aside/index.js
  20. 5 0
      src/components/layout/Content/index.js
  21. 94 0
      src/components/layout/Footer/index.js
  22. 42 0
      src/components/layout/Header/index.js
  23. 17 0
      src/helpers/getQuery.js
  24. 7 0
      src/helpers/index.js
  25. 9 0
      src/helpers/jwtDecode.js
  26. 12 0
      src/helpers/mock.js
  27. 3 0
      src/images/shopping-logo-svgrepo-com 1.svg:Zone.Identifier
  28. 7 0
      src/images/shopping-logo.svg
  29. 0 13
      src/index.css
  30. 4 4
      src/index.js
  31. 80 0
      src/index.scss
  32. 34 0
      src/reducers/authReducer.js
  33. 37 0
      src/reducers/cartReducer.js
  34. 55 0
      src/reducers/feedReducer.js
  35. 45 0
      src/reducers/index.js
  36. 28 0
      src/reducers/promiseReducer.js

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 3556 - 4928
package-lock.json


+ 6 - 0
package.json

@@ -3,15 +3,21 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@emotion/react": "^11.9.0",
+    "@emotion/styled": "^11.8.1",
+    "@mui/material": "^5.6.4",
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^13.2.0",
     "@testing-library/user-event": "^13.5.0",
+    "node-sass": "^7.0.1",
     "react": "^18.1.0",
     "react-dom": "^18.1.0",
+    "react-icons": "^4.3.1",
     "react-redux": "^8.0.1",
     "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",
     "redux": "^4.2.0",
+    "redux-devtools-extension": "^2.13.9",
     "redux-thunk": "^2.4.1",
     "web-vitals": "^2.1.4"
   },

+ 15 - 18
src/App.js

@@ -1,25 +1,22 @@
 import logo from './logo.svg';
 import './App.css';
+import { Box } from '@mui/material';
+import { BrowserRouter } from 'react-router-dom';
+import { Router } from 'react-router-dom';
+import { Root } from './components/Root';
+import { Provider } from 'react-redux';
+import { store } from './reducers';
 
 function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
+    return (
+        <Provider store={store}>
+            <BrowserRouter>
+                <Box className="App">
+                    <Root />
+                </Box>
+            </BrowserRouter>
+        </Provider>
+    );
 }
 
 export default App;

+ 13 - 0
src/actions/actionPageStart.js

@@ -0,0 +1,13 @@
+import { actionRootCats } from './actionRootCats';
+
+export const actionPageStart = () => async (dispatch, getState) => {
+    dispatch(actionRootCats());
+
+    // const {
+    //     auth: { token },
+    // } = getState();
+
+    // if (token) {
+    //     dispatch(actionAboutMe());
+    // }
+};

+ 96 - 0
src/actions/actionRootCats.js

@@ -0,0 +1,96 @@
+import { mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionRootCats = () => async (dispatch, getState) => {
+    dispatch(
+        actionPromise(
+            'rootCats',
+            new Promise((resolve) => {
+                setTimeout(
+                    Math.random() > 0.01
+                        ? resolve({
+                              data: [
+                                  {
+                                      id: 1,
+                                      name: 'Categoty 1',
+                                  },
+                                  {
+                                      id: 2,
+                                      name: 'Categoty 2',
+                                  },
+                                  {
+                                      id: 3,
+                                      name: 'Categoty 3',
+                                  },
+                                  {
+                                      id: 4,
+                                      name: 'Categoty 4',
+                                  },
+                              ],
+                          })
+                        : resolve({
+                              errors: [{ message: 'Error adsasdadas' }],
+                          }),
+                    400
+                );
+            })
+                // .then((res) => res.json())
+                .then((data) => {
+                    console.log(data);
+                    if (data.errors) {
+                        throw new Error(JSON.stringify(data.errors));
+                    } else return data.data;
+                })
+        )
+    );
+};
+
+// () => ({
+//     data: [
+//         {
+//             id: 1,
+//             name: 'Categoty 1',
+//         },
+//         {
+//             id: 2,
+//             name: 'Categoty 2',
+//         },
+//         {
+//             id: 3,
+//             name: 'Categoty 3',
+//         },
+//         {
+//             id: 4,
+//             name: 'Categoty 4',
+//         },
+//     ],
+// }),
+
+// .then((data) => {
+//     if (data.errors) {
+//         throw new Error(JSON.stringify(data.errors));
+//     } else return Object.values(data.data)[0];
+// })
+// fetch(backendURL + "/api/reception/patient/", {
+//     method: "GET",
+//     headers: {
+//         "Content-Type": "application/json",
+//         ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}),
+//     },
+// });
+
+// actionPromise("patientAll",    fetch(url, {
+//     type:"GET",
+//     headers: {
+//         "Content-Type": "application/json",
+//         ...(localStorage.authToken ? { Authorization: "Bearer " + localStorage.authToken } : {}),
+//     },
+//     body: data,
+// })
+//     .then((res) => res.json())
+//     .then((data) => {
+//         if (typeof data === "string") {
+//             throw new Error(data);
+//         } else return Object.values(data);
+//     }))

+ 0 - 0
src/components/MainPage/index.js


+ 41 - 0
src/components/Root/index.js

@@ -0,0 +1,41 @@
+import { Route, Router, Routes } from 'react-router-dom';
+import { useDispatch } from 'react-redux';
+import { Container, Box, Stack } from '@mui/material';
+import { Fragment } from 'react';
+import { CProtectedRoute } from '../common/ProtectedRoute';
+import { AdminLayoutPage } from '../admin/AdminLayoutPage';
+import { actionRootCats } from '../../actions/actionRootCats';
+import { Aside } from '../layout/Aside';
+import Content from '../layout/Content';
+
+import { store } from '../../reducers';
+import { Header } from '../layout/Header';
+import { Footer } from '../layout/Footer';
+
+const Root = ({ user = {} }) => {
+    const isSignIn = true;
+    store.dispatch(actionRootCats());
+
+    return (
+        <Box className="Root">
+            <Header />
+            <Stack direction="row">
+                <Aside />
+                <Content>
+                    {/* <Routes>
+                        <Route path="/" exact />
+                        <Route path="/good/:id" />
+                        <Route path="/category/:id" />
+                        <Route path="/category/" />
+                        <Route path="/good/" />
+
+                        <CProtectedRoute path="/admin" component={AdminLayoutPage} roles={['admin']} />
+                    </Routes> */}
+                </Content>
+            </Stack>
+            <Footer />
+        </Box>
+    );
+};
+
+export { Root };

+ 7 - 0
src/components/admin/AdminLayoutPage/index.js

@@ -0,0 +1,7 @@
+import { Box } from '@mui/material';
+
+const AdminLayoutPage = () => {
+    return <Box></Box>;
+};
+
+export { AdminLayoutPage };

+ 15 - 0
src/components/common/Categories/Category.js

@@ -0,0 +1,15 @@
+import { Divider, ListItem, ListItemButton, ListItemText } from '@mui/material';
+import { Fragment } from 'react';
+import { Link } from 'react-router-dom';
+
+export const Category = ({ category = {} }) => (
+    <Fragment>
+        <ListItem disablePadding>
+            <Link to={`/category/${category.id}`}></Link>
+            <ListItemButton>
+                <ListItemText primary={category.name || ''} />
+            </ListItemButton>
+        </ListItem>
+        <Divider />
+    </Fragment>
+);

+ 20 - 0
src/components/common/Categories/index.js

@@ -0,0 +1,20 @@
+import { Paper, List, ListItem, ListItemButton, ListItemText, Box } from '@mui/material';
+import { useEffect } from 'react';
+import { Category } from './Category';
+
+const Categories = ({ categories = [] }) => {
+    useEffect(() => {
+        console.log(categories);
+    }, [categories]);
+    return (
+        <Box className="Categories">
+            <List>
+                {(categories || []).map((cat) => (
+                    <Category category={cat} key={cat.id} />
+                ))}
+            </List>
+        </Box>
+    );
+};
+
+export { Categories };

+ 15 - 0
src/components/common/ProtectedRoute/index.js

@@ -0,0 +1,15 @@
+import { Navigate, Route } from 'react-router-dom';
+import { connect } from 'react-redux';
+export const ProtectedRoute = ({ roles = [], fallback = '/', component, auth, ...routeProps }) => {
+    const WrapperComponent = (renderProps) => {
+        const C = component;
+        !!auth.length || (auth = ['anon']);
+        if (!auth.filter((role) => roles.includes(role)).length) {
+            return <Navigate to={fallback} />;
+        }
+        return <C {...renderProps} />;
+    };
+    return <Route {...routeProps} element={<WrapperComponent />} />;
+};
+
+export const CProtectedRoute = connect((state) => ({ auth: state.auth?.payload?.sub?.acl || [] }))(ProtectedRoute);

+ 100 - 0
src/components/common/SearchBar/SearchBar.js

@@ -0,0 +1,100 @@
+import { useEffect, useState, useRef } from 'react';
+import { connect, useDispatch } from 'react-redux';
+import { Link } from 'react-router-dom';
+import { AiOutlineCloseCircle } from 'react-icons/ai';
+
+import { Box, TextField, Button, Container, Stack, IconButton } from '@mui/material';
+
+// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+// import { faSearch } from "@fortawesome/free-solid-svg-icons";
+
+export const SearchBar = ({
+    onSearch,
+    onSearchButtonClick,
+    render = null,
+    renderParams = {},
+    searchLink = '/search/',
+} = {}) => {
+    const ref = useRef();
+    const [inputValue, setInputValue] = useState('');
+    const [isChildrenOpen, setIsChildrenOpen] = useState(false);
+    const [inputTimeout, setInputTimeout] = useState(null);
+    const R = render;
+
+    useEffect(() => {
+        if (inputTimeout) {
+            clearTimeout(inputTimeout);
+        }
+
+        const checkClickOutsideHeaderSearchBar = (e) => {
+            if (ref.current && !ref.current.contains(e.target)) {
+                setIsChildrenOpen(false);
+            } else {
+                inputValue.length && setIsChildrenOpen(true);
+            }
+        };
+
+        inputValue && onSearch(inputValue);
+        setIsChildrenOpen(!!inputValue?.length);
+        document.addEventListener('mousedown', checkClickOutsideHeaderSearchBar);
+        return () => {
+            document.removeEventListener('mousedown', checkClickOutsideHeaderSearchBar);
+        };
+    }, [inputValue]);
+
+    return (
+        <Box className={`SearchBar ${!isChildrenOpen && 'hide'}`} ref={ref}>
+            <Stack direction="row" alignItems="center">
+                <TextField
+                    variant="standard"
+                    value={inputValue}
+                    placeholder="Пошук"
+                    onChange={(e) => setInputValue(e.target.value)}
+                    className="SearchBarInput"
+                    InputProps={{
+                        endAdornment: (
+                            <IconButton onClick={() => setInputValue('')} edge="end">
+                                {inputValue && <AiOutlineCloseCircle />}
+                            </IconButton>
+                        ),
+                    }}
+                />
+
+                {!!inputValue ? (
+                    <Link
+                        className="Link"
+                        to={`${searchLink}${inputValue}`}
+                        onClick={() => {
+                            setInputValue('');
+                            onSearchButtonClick();
+                        }}
+                    >
+                        <Button variant="text" color="inherit">
+                            Пошук
+                        </Button>
+                    </Link>
+                ) : (
+                    <Button variant="text" color="inherit">
+                        Пошук
+                    </Button>
+                )}
+            </Stack>
+            <Stack direction="row" justifyContent="center">
+                {isChildrenOpen && (
+                    <R
+                        onItemClick={() => {
+                            setInputValue('');
+                            onSearchButtonClick();
+                        }}
+                        {...renderParams}
+                    />
+                )}
+            </Stack>
+        </Box>
+    );
+};
+
+// export const CSearchBar = connect(null, {
+//     onSearch: (text) => actionGoodsFind({ text, limit: 5 }),
+//     onSearchButtonClick: () => actionPromiseClear("goodsFind"),
+// })(SearchBar);

+ 19 - 0
src/components/common/SearchBar/SearchCategoryResultItem.js

@@ -0,0 +1,19 @@
+import { backendURL } from "helpers";
+import { Link } from "react-router-dom";
+import defaultGoodImage from "images/defaultGoodImage.png";
+import { Stack, Typography } from "@mui/material";
+const SearchCategoryResultItem = ({ category, onClick, link = "" } = {}) => {
+    const { _id = null, name = "" } = category || {};
+
+    return (
+        <Link className="Link" to={`${link}${_id}/`}>
+            <Stack direction="row" className="SearchCategoryResultItem" onClick={() => onClick && onClick()}>
+                <Typography sx={{ flexGrow: 1 }}>{name.length > 30 ? `${name.substring(0, 30)}...` : name}</Typography>
+
+                <Typography>{_id}</Typography>
+            </Stack>
+        </Link>
+    );
+};
+
+export default SearchCategoryResultItem;

+ 29 - 0
src/components/common/SearchBar/SearchGoodResultItem.js

@@ -0,0 +1,29 @@
+import { backendURL } from 'helpers';
+import { Link } from 'react-router-dom';
+import defaultGoodImage from 'images/defaultGoodImage.png';
+
+import { Grid, Box, Stack, Typography } from '@mui/material';
+const SearchGoodResultItem = ({ good, onClick, link = '' } = {}) => {
+    const { _id = 0, images = [], name = '', description = '', price = '' } = good || {};
+
+    return (
+        <Link className="Link" to={`${link}${_id}/`}>
+            <Stack direction="row" className="SearchGoodResultItem" onClick={() => onClick && onClick()}>
+                <Box component="img" src={images[0]?.url ? `/${images ? images[0]?.url : ''}` : defaultGoodImage} />
+                <Box sx={{ p: 1, flexGrow: 1 }}>
+                    <Typography variant="body1" sx={{ flexGrow: 1 }}>
+                        {name.length > 30 ? `${name.substring(0, 30)}...` : name}
+                    </Typography>
+                    <Typography variant="body2">
+                        {description.length > 70 ? `${description.substring(0, 70)}...` : description}
+                    </Typography>
+                </Box>
+                <Typography variant="body1" sx={{ display: 'flex', alignItems: 'center' }}>
+                    {price}
+                </Typography>
+            </Stack>
+        </Link>
+    );
+};
+
+export default SearchGoodResultItem;

+ 52 - 0
src/components/common/SearchBar/SearchResults.js

@@ -0,0 +1,52 @@
+import { connect } from 'react-redux';
+import { useEffect } from 'react';
+import SearchGoodResultItem from './SearchGoodResultItem';
+import { HiOutlineEmojiSad } from 'react-icons/hi';
+import SearchCategoryResultItem from './SearchCategoryResultItem';
+import { Divider, Paper, Stack, Typography } from '@mui/material';
+import { Box } from '@mui/system';
+import { Error } from 'components/Error';
+
+export const SearchResults = ({ items, onItemClick, itemLink = '' }) => {
+    return (
+        <Paper className="SearchResults">
+            <Stack>
+                {!!items?.length ? (
+                    itemLink.match(/.+(good).+/) ? (
+                        items.map((good) => (
+                            <Box>
+                                <SearchGoodResultItem
+                                    link={itemLink}
+                                    good={good}
+                                    key={good._id}
+                                    onClick={() => onItemClick && onItemClick()}
+                                />
+                                <Divider sx={{ my: 1 }} />
+                            </Box>
+                        ))
+                    ) : itemLink.match(/.+(category || categories).+/) ? (
+                        items.map((cat) => (
+                            <Box>
+                                <SearchCategoryResultItem
+                                    link={itemLink}
+                                    category={cat}
+                                    key={cat._id}
+                                    onClick={() => onItemClick && onItemClick()}
+                                />
+                                <Divider sx={{ my: 1 }} />
+                            </Box>
+                        ))
+                    ) : (
+                        []
+                    )
+                ) : (
+                    <Error>Ничего не найдено</Error>
+                )}
+            </Stack>
+        </Paper>
+    );
+};
+
+const CSearchResults = connect((state) => ({ items: state.promise.goodsFind?.payload || [] }))(SearchResults);
+
+export default CSearchResults;

+ 7 - 0
src/components/common/SearchBar/index.js

@@ -0,0 +1,7 @@
+import { SearchBar } from './SearchBar';
+
+// import CSearchResults, { SearchResults } from './SearchResults';
+// import SearchGoodResultItem from './SearchGoodResultItem';
+
+// export { SearchBar, SearchResults, CSearchResults, SearchGoodResultItem };
+export { SearchBar };

+ 0 - 0
src/components/layout/Aside/CAdminCategories.js


+ 8 - 0
src/components/layout/Aside/CCategories.js

@@ -0,0 +1,8 @@
+import { connect } from 'react-redux';
+import { Categories } from '../../common/Categories';
+
+const CCategories = connect((state) => ({
+    categories: state.promise.rootCats?.payload || [],
+}))(Categories);
+
+export { CCategories };

+ 18 - 0
src/components/layout/Aside/index.js

@@ -0,0 +1,18 @@
+import { Box } from '@mui/material';
+import { Route, Routes } from 'react-router-dom';
+
+import { CCategories } from './CCategories';
+
+const Aside = ({ children }) => (
+    <Box className="Aside">
+        <Box className="body">
+            <Routes>
+                {/* <Route path="/admin/" component={CAdminCategories} /> */}
+                <Route path="/*" element={<CCategories />} />
+            </Routes>
+            {children}
+        </Box>
+    </Box>
+);
+
+export { Aside };

+ 5 - 0
src/components/layout/Content/index.js

@@ -0,0 +1,5 @@
+import { Paper, Box } from '@mui/material';
+
+const Content = ({ children }) => <Box className="Content">{children}</Box>;
+
+export default Content;

+ 94 - 0
src/components/layout/Footer/index.js

@@ -0,0 +1,94 @@
+import { Container, Grid, Table, TableBody, TableCell, TableRow, Typography } from '@mui/material';
+import { Box } from '@mui/system';
+
+const Footer = () => (
+    <Box className="Footer">
+        <Container>
+            <Grid container>
+                <Grid item xs={2}></Grid>
+                <Grid item xs={2}>
+                    <Typography variant="body1" textAlign="left">
+                        Графік роботи
+                    </Typography>
+                    <Table>
+                        <TableBody>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Пн-Пт:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        9.00-18.00
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Субота:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        9.00-15.00
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Неділя
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        вихідний
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                        </TableBody>
+                    </Table>
+                </Grid>
+                <Grid item xs={3}></Grid>
+                <Grid item xs={3}>
+                    <Typography variant="body1" textAlign="left">
+                        Контакти
+                    </Typography>
+                    <Table>
+                        <TableBody>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Тел:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        +380667213260
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                            <TableRow>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        Email:
+                                    </Typography>
+                                </TableCell>
+                                <TableCell className="TableCell">
+                                    <Typography variant="body2" textAlign="left">
+                                        illya.shyyan@hneu.net
+                                    </Typography>
+                                </TableCell>
+                            </TableRow>
+                        </TableBody>
+                    </Table>
+                </Grid>
+                <Grid item xs={2}></Grid>
+            </Grid>
+        </Container>
+    </Box>
+);
+
+export { Footer };

+ 42 - 0
src/components/layout/Header/index.js

@@ -0,0 +1,42 @@
+import { AppBar, Box, Button, IconButton, Stack, TextField, Toolbar, Typography } from '@mui/material';
+import { MdOutlineShoppingCart } from 'react-icons/md';
+import { ReactComponent as ShoppingLogo } from '../../../images/shopping-logo.svg';
+import { SearchBar } from '../../common/SearchBar';
+
+const Header = () => (
+    <Box className="Header">
+        <AppBar position="static" className="AppBar">
+            <Toolbar variant="dense" className="ToolBar">
+                <IconButton>
+                    <ShoppingLogo className="Logo" />
+                </IconButton>
+                <Stack direction="row" spacing={2}>
+                    <Button variant="text" color="inherit">
+                        <Typography variant="body1" component="div">
+                            Головна
+                        </Typography>
+                    </Button>
+                    <Button variant="text" color="inherit">
+                        <Typography variant="body1" component="div">
+                            Товари
+                        </Typography>
+                    </Button>
+                    <Button variant="text" color="inherit">
+                        <Typography variant="body1" component="div">
+                            Зворотній зв'язок
+                        </Typography>
+                    </Button>
+                </Stack>
+                <Box className="SearchBarWrapper">
+                    <SearchBar />
+                </Box>
+
+                <IconButton color="inherit" className="CartLogoButton">
+                    <MdOutlineShoppingCart className="CartLogo" />
+                </IconButton>
+            </Toolbar>
+        </AppBar>
+    </Box>
+);
+
+export { Header };

+ 17 - 0
src/helpers/getQuery.js

@@ -0,0 +1,17 @@
+export const getQuery =
+    () =>
+    (url, type = 'GET', body = {}) =>
+        fetch(url, {
+            method: type,
+            headers: {
+                'Content-Type': 'application/json',
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+            body: body,
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return Object.values(data.data);
+            });

+ 7 - 0
src/helpers/index.js

@@ -0,0 +1,7 @@
+import { jwtDecode } from './jwtDecode';
+import { getQuery } from './getQuery';
+import { mock } from './mock';
+
+export const query = getQuery('/');
+
+export { jwtDecode, getQuery, mock };

+ 9 - 0
src/helpers/jwtDecode.js

@@ -0,0 +1,9 @@
+export const jwtDecode = (token) => {
+    try {
+        let payload = JSON.parse(atob(token.split('.')[1]));
+
+        return payload;
+    } catch (e) {
+        console.log(e);
+    }
+};

+ 12 - 0
src/helpers/mock.js

@@ -0,0 +1,12 @@
+export const mock = (success, error, timeout) => {
+    return new Promise(() => {
+        setTimeout((resolve, reject) => {
+            if (Math.random() > 0.1) {
+                console.log(success());
+                resolve(success());
+            } else {
+                reject(error());
+            }
+        }, timeout || 1000);
+    });
+};

+ 3 - 0
src/images/shopping-logo-svgrepo-com 1.svg:Zone.Identifier

@@ -0,0 +1,3 @@
+[ZoneTransfer]
+ZoneId=3
+HostUrl=https://www.figma.com/

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 7 - 0
src/images/shopping-logo.svg


+ 0 - 13
src/index.css

@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}

+ 4 - 4
src/index.js

@@ -1,14 +1,14 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import './index.css';
+import './index.scss';
 import App from './App';
 import reportWebVitals from './reportWebVitals';
 
 const root = ReactDOM.createRoot(document.getElementById('root'));
 root.render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>
+    <React.StrictMode>
+        <App />
+    </React.StrictMode>
 );
 
 // If you want to start measuring performance in your app, pass a function

+ 80 - 0
src/index.scss

@@ -0,0 +1,80 @@
+*{
+  padding: 0;
+  margin:0;
+}
+
+
+.App{
+  & .Header{
+    margin-bottom: 30px;
+    & .AppBar{
+      background: white;
+      color:#6750A4;
+
+      & .ToolBar{
+        padding-left: 50px;
+        padding-right: 50px;
+        & .Logo{
+          width:50px;
+          height:50px;
+          margin-right: 10px;
+        }
+        & .SearchBarWrapper{
+          flex-grow: 1;
+          margin-left: 10px;
+          & .SearchBar{
+            & .SearchBarInput{
+              width: 100%;
+              max-width: 500px;
+            }
+          }
+        }
+        & .CartLogoButton{
+          & .CartLogo{
+            width:30px;
+            height:30px;
+          }
+        }
+      }
+
+    }
+
+  }
+  & .Aside{
+    margin-left: 50px;
+    width:100%;
+    max-width: 250px;
+
+    & .body{
+      padding: 10px 0px 100px 0px;
+      border-radius: 5px;
+      border:1px solid #C9C5CA ;
+      & .Categories{
+
+      }
+    }
+
+  }
+
+  & .Content{
+    margin-left: 25px;
+    margin-right: 50px;
+    min-height: 600px;
+    border-radius: 5px;
+    flex:1;
+    border:1px solid #C9C5CA ;
+  }
+
+  & .Footer{
+    margin-top:70px;
+    background-color:  #F4EFF4;
+    padding: 25px 0px;
+    & .TableCell{
+      border-bottom: none;
+      padding: 0;
+      padding-top: 5px;
+    }
+
+  }
+}
+

+ 34 - 0
src/reducers/authReducer.js

@@ -0,0 +1,34 @@
+import { jwtDecode } from '../helpers';
+
+export function authReducer(state, { type, token }) {
+    if (state === undefined) {
+        if (localStorage.authToken) {
+            token = localStorage.authToken;
+            type = 'AUTH_LOGIN';
+            state = {};
+        }
+    }
+
+    if (type === 'AUTH_LOGIN') {
+        if (!token || !jwtDecode(token)) return {};
+        localStorage.authToken = token;
+        return {
+            ...state,
+            token: token,
+            payload: jwtDecode(token),
+        };
+    }
+
+    if (type === 'AUTH_LOGOUT') {
+        localStorage.removeItem('authToken');
+        return {};
+    }
+    return state || {};
+}
+
+export const actionAuthLogin = (token) => ({
+    type: 'AUTH_LOGIN',
+    token: token,
+});
+
+export const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' });

+ 37 - 0
src/reducers/cartReducer.js

@@ -0,0 +1,37 @@
+export function cartReducer(state = {}, { type, good, count = 1 }) {
+    if (count <= 0) {
+        type = 'CART_DELETE';
+    }
+
+    if (type === 'CART_ADD') {
+        return {
+            ...state,
+            [good['_id']]: {
+                good,
+                count: good['_id'] in state ? state[good._id].count + count : count,
+            },
+        };
+    }
+    if (type === 'CART_CHANGE') {
+        return {
+            ...state,
+            [good['_id']]: {
+                good,
+                count: count,
+            },
+        };
+    }
+    if (type === 'CART_DELETE') {
+        let { [good._id]: toRemove, ...newState } = state;
+        return newState;
+    }
+    if (type === 'CART_CLEAR') {
+        return {};
+    }
+    return state;
+}
+
+export const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count: +count });
+export const actionCartChange = (good, count = 1) => ({ type: 'CART_CHANGE', good, count: +count });
+export const actionCartDelete = (good) => ({ type: 'CART_DELETE', good });
+export const actionCartClear = () => ({ type: 'CART_CLEAR' });

+ 55 - 0
src/reducers/feedReducer.js

@@ -0,0 +1,55 @@
+// import { actionPromise } from '.';
+// import { actionCatAll, actionGoodsFind } from '../actions';
+// import { actionCatsFind } from '../actions/actionCatsFind';
+// import { actionGoodsAll } from '../actions/actionGoodsAll';
+// import { gql } from '../helpers';
+
+// function feedReducer(state = { payload: [] }, { type, payload = [] }) {
+//     if (type === 'FEED_ADD') {
+//         return {
+//             ...state,
+//             payload: [...state['payload'], ...payload],
+//         };
+//     }
+
+//     if (type === 'FEED_CLEAR') {
+//         return { payload: [] };
+//     }
+//     return state || { payload: [] };
+// }
+
+// const actionFeedAdd = (payload) => ({ type: 'FEED_ADD', payload });
+// const actionFeedClear = () => ({ type: 'FEED_CLEAR' });
+// const actionFeedGoods =
+//     (skip = 0) =>
+//     async (dispatch, getState) => {
+//         await dispatch(actionGoodsAll({ skip, limit: 50, promiseName: 'feedGoodsAll' }));
+//     };
+
+// const actionFeedGoodsFind =
+//     ({ skip = 0, text = '' }) =>
+//     async (dispatch, getState) => {
+//         await dispatch(actionGoodsFind({ skip, limit: 50, promiseName: 'feedGoodsFind', text }));
+//     };
+
+// const actionFeedCatsFind =
+//     ({ skip = 0, text = '' }) =>
+//     async (dispatch, getState) => {
+//         await dispatch(actionCatsFind({ skip, promiseName: 'feedCatsFind', text, limit: 50 }));
+//     };
+
+// const actionFeedCats =
+//     (skip = 0) =>
+//     async (dispatch, getState) => {
+//         await dispatch(actionCatAll({ promiseName: 'feedCatAll', skip, limit: 50 }));
+//     };
+
+// export {
+//     actionFeedCats,
+//     actionFeedCatsFind,
+//     actionFeedGoods,
+//     actionFeedClear,
+//     actionFeedAdd,
+//     actionFeedGoodsFind,
+//     feedReducer,
+// };

+ 45 - 0
src/reducers/index.js

@@ -0,0 +1,45 @@
+import { createStore, combineReducers, applyMiddleware } from 'redux';
+import { composeWithDevTools } from 'redux-devtools-extension';
+import thunk from 'redux-thunk';
+import { authReducer, actionAuthLogin, actionAuthLogout } from './authReducer';
+import {
+    promiseReducer,
+    actionPending,
+    actionFulfilled,
+    actionRejected,
+    actionPromise,
+    actionPromiseClear,
+} from './promiseReducer';
+import { cartReducer, actionCartAdd, actionCartChange, actionCartDelete, actionCartClear } from './cartReducer';
+// import {
+//     actionFeedCats,
+//     actionFeedCatsFind,
+//     actionFeedGoods,
+//     actionFeedGoodsFind,
+//     actionFeedClear,
+//     actionFeedAdd,
+//     feedReducer,
+// } from './feedReducer';
+import { createStoreHook } from 'react-redux';
+
+export { cartReducer, actionCartAdd, actionCartChange, actionCartDelete, actionCartClear };
+export { authReducer, actionAuthLogin, actionAuthLogout };
+export { promiseReducer, actionPending, actionFulfilled, actionRejected, actionPromise, actionPromiseClear };
+// export {
+//     actionFeedCats,
+//     actionFeedCatsFind,
+//     actionFeedGoods,
+//     actionFeedGoodsFind,
+//     actionFeedClear,
+//     actionFeedAdd,
+//     feedReducer,
+// };
+export const store = createStore(
+    combineReducers({
+        auth: authReducer,
+        promise: promiseReducer,
+        cart: cartReducer,
+        // feed: feedReducer,
+    }),
+    composeWithDevTools(applyMiddleware(thunk))
+);

+ 28 - 0
src/reducers/promiseReducer.js

@@ -0,0 +1,28 @@
+export function promiseReducer(state = {}, { type, name, status, payload, error }) {
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: { status, payload: status === 'PROMISE' ? [name].payload : payload, error },
+        };
+    }
+    if (type === 'PROMISE_CLEAR') {
+        const { [name]: toRemove, ...newState } = state;
+        return newState;
+    }
+    return state;
+}
+
+export const actionPending = (name) => ({ type: 'PROMISE', name, status: 'PENDING' });
+export const actionFulfilled = (name, payload) => ({ type: 'PROMISE', name, status: 'FULFILLED', payload });
+export const actionRejected = (name, error) => ({ type: 'PROMISE', name, status: 'REJECTED', error });
+export const actionPromiseClear = (name) => ({ type: 'PROMISE_CLEAR', name });
+export const actionPromise = (name, promise) => async (dispatch) => {
+    dispatch(actionPending(name));
+    try {
+        let payload = await promise;
+        dispatch(actionFulfilled(name, payload));
+        return payload;
+    } catch (error) {
+        dispatch(actionRejected(name, JSON.parse(error.message)));
+    }
+};