6 Commits 80aa6ce8bd ... d88635eb12

Author SHA1 Message Date
  Alex.ElRubio d88635eb12 ... 1 year ago
  Alex.ElRubio 80aa6ce8bd project storage 1 year ago
  Alex.ElRubio c0a6f82787 remoove submodule 1 year ago
  Alex.ElRubio 663651e44d store project 1 year ago
  Alex-elrubio d2d50778c9 all my homeworks 1 year ago
  Alex-elrubio bc444f49a5 first commit 1 year ago
60 changed files with 31510 additions and 1 deletions
  1. 0 1
      react-store-app
  2. 150 0
      react-store-app/.gitignore
  3. 72 0
      react-store-app/README.md
  4. 7 0
      react-store-app/jsconfig.json
  5. 719 0
      react-store-app/old-code.js
  6. 27873 0
      react-store-app/package-lock.json
  7. 48 0
      react-store-app/package.json
  8. BIN
      react-store-app/public/favicon.ico
  9. 43 0
      react-store-app/public/index.html
  10. BIN
      react-store-app/public/logo192.png
  11. BIN
      react-store-app/public/logo512.png
  12. 25 0
      react-store-app/public/manifest.json
  13. 3 0
      react-store-app/public/robots.txt
  14. 46 0
      react-store-app/src/App.js
  15. 8 0
      react-store-app/src/App.test.js
  16. 31 0
      react-store-app/src/apollo/client.js
  17. 50 0
      react-store-app/src/apollo/queries.js
  18. 23 0
      react-store-app/src/components/AddToBasketBtn.js
  19. 37 0
      react-store-app/src/components/BasketItem.js
  20. 64 0
      react-store-app/src/components/BasketSidebar.js
  21. 37 0
      react-store-app/src/components/Card.js
  22. 17 0
      react-store-app/src/components/CategoryItem.js
  23. 15 0
      react-store-app/src/components/Footer.js
  24. 9 0
      react-store-app/src/components/GetIcon.js
  25. 52 0
      react-store-app/src/components/Header.js
  26. 17 0
      react-store-app/src/components/MobileCategories.js
  27. 27 0
      react-store-app/src/components/Quantity.js
  28. 5 0
      react-store-app/src/components/Title.js
  29. 3 0
      react-store-app/src/config.js
  30. 26 0
      react-store-app/src/hooks/useMakeRequest.js
  31. 29 0
      react-store-app/src/hooks/useMobileDetect.js
  32. 1 0
      react-store-app/src/images/empty_cart.svg
  33. BIN
      react-store-app/src/images/shopBG.jpg
  34. 21 0
      react-store-app/src/index.js
  35. 1 0
      react-store-app/src/logo.svg
  36. 132 0
      react-store-app/src/old-code/full-code-gql.html
  37. 719 0
      react-store-app/src/old-code/full-code-gql.js
  38. 51 0
      react-store-app/src/pages/Category.js
  39. 52 0
      react-store-app/src/pages/Detail.js
  40. 47 0
      react-store-app/src/pages/Home.js
  41. 13 0
      react-store-app/src/reportWebVitals.js
  42. 5 0
      react-store-app/src/setupTests.js
  43. 61 0
      react-store-app/src/stores/cartStore.js
  44. 33 0
      react-store-app/src/styles/AddToBasketBtn.module.scss
  45. 15 0
      react-store-app/src/styles/App.module.scss
  46. 68 0
      react-store-app/src/styles/BasketItem.module.scss
  47. 134 0
      react-store-app/src/styles/BasketSidebar.module.scss
  48. 123 0
      react-store-app/src/styles/Card.module.scss
  49. 23 0
      react-store-app/src/styles/Category.module.scss
  50. 46 0
      react-store-app/src/styles/CategoryItem.module.scss
  51. 139 0
      react-store-app/src/styles/Detail.module.scss
  52. 18 0
      react-store-app/src/styles/Footer.module.scss
  53. 118 0
      react-store-app/src/styles/Header.module.scss
  54. 23 0
      react-store-app/src/styles/Home.module.scss
  55. 86 0
      react-store-app/src/styles/MobileBasket.module.scss
  56. 53 0
      react-store-app/src/styles/MobileBottomNav.module.scss
  57. 14 0
      react-store-app/src/styles/MobileCategories.module.scss
  58. 35 0
      react-store-app/src/styles/Quantity.module.scss
  59. 9 0
      react-store-app/src/styles/_variables.scss
  60. 34 0
      react-store-app/src/styles/index.css

+ 0 - 1
react-store-app

@@ -1 +0,0 @@
-Subproject commit 615ac18f81d310fe45f7b1e18422589e72b41e3f

+ 150 - 0
react-store-app/.gitignore

@@ -0,0 +1,150 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

+ 72 - 0
react-store-app/README.md

@@ -0,0 +1,72 @@
+[![Netlify Status](https://api.netlify.com/api/v1/badges/f5d62f5e-e9bc-4243-87f2-0ad5196898e2/deploy-status)](https://app.netlify.com/sites/mt-react-fake-store-web-app/deploys)
+
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+
+The page will reload when you make changes.\
+You may also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
+
+### Code Splitting
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+
+### Analyzing the Bundle Size
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+
+### Making a Progressive Web App
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+
+### Advanced Configuration
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+
+### Deployment
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+
+### `npm run build` fails to minify
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

+ 7 - 0
react-store-app/jsconfig.json

@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "baseUrl": "src/"
+  },
+  "exclude": ["node_modules", "build"],
+  "include": ["src"]
+}

+ 719 - 0
react-store-app/old-code.js

@@ -0,0 +1,719 @@
+import React, { useState } from "react";
+import "./App.css";
+
+import thunk from "redux-thunk";
+import { createStore, combineReducers, applyMiddleware } from "redux";
+import { Provider, connect } from "react-redux";
+function createStore(reducer){
+    let state       = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
+    let cbs         = []                     //массив подписчиков
+
+    const getState  = () => state            //функция, возвращающая переменную из замыкания
+    const subscribe = cb => (cbs.push(cb),   //запоминаем подписчиков в массиве
+        () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
+
+    const dispatch  = action => {
+        if (typeof action === 'function'){ //если action - не объект, а функция
+            return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
+        }
+        const newState = reducer(state, action) //пробуем запустить редьюсер
+        if (newState !== state){ //проверяем, смог ли редьюсер обработать action
+            state = newState //если смог, то обновляем state
+            for (let cb of cbs)  cb() //и запускаем подписчиков
+        }
+    }
+
+    return {
+        getState, //добавление функции getState в результирующий объект
+        dispatch,
+        subscribe //добавление subscribe в объект
+    }
+}
+
+function combineReducers(reducers) {
+    return (state={}, action) => {
+        const newState = {}
+        // перебрать все редьюсеры
+        if (reducers) {
+            for (const [reducerName, reducer] of Object.entries(reducers)) {
+                const newSubState = reducer(state[reducerName], action)
+                if (newSubState !== state[reducerName]) {
+                    newState[reducerName] = newSubState
+                }
+            }
+            // если newState не пустой, то вернуть стейт в
+            if (Object.keys(newState).length !== 0) {
+                return {...state, ...newState}
+            } else {
+                return state
+            }
+        }
+
+    }
+}
+
+const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})
+const store = createStore(combinedReducer)
+
+store.subscribe(() => console.log(store.getState()))
+
+
+
+function jwtDecode(token){
+    try {
+        return JSON.parse(atob(token.split('.')[1]))
+    }
+    catch(e){
+    }
+}
+
+function authReducer(state, {type, token}) {
+    if (!state) {
+        if (localStorage.authToken) {
+            token = localStorage.authToken
+            type = 'AUTH_LOGIN'
+        } else {
+            return {}
+        }
+    }
+    if (type === 'AUTH_LOGIN') {
+        let payload = jwtDecode(token)
+        if (typeof payload === 'object') {
+            localStorage.authToken = token
+            return {
+                ...state,
+                token,
+                payload
+            }
+        } else {
+            return state
+        }
+    }
+    if (type === 'AUTH_LOGOUT') {
+        delete localStorage.authToken
+        location.reload()
+        return {}
+    }
+    return state
+}
+
+const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
+const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
+
+
+
+function cartReducer (state={}, {type, good={}, count=1}) {
+
+    if (Object.keys(state).length === 0 && localStorage.cart) {
+        let currCart = JSON.parse(localStorage.cart)
+        if (currCart && Object.keys(currCart).length !== 0) {
+            state = currCart
+        }
+    }
+
+    const {_id} = good
+
+    const types = {
+        CART_ADD() {
+            count = +count
+            if (!count) {
+                return state
+            }
+            let newState = {
+                ...state,
+                [_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : count + (state[_id]?.count || 0)}
+            }
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_CHANGE() {
+            count = +count
+            if (!count) {
+                return state
+            }
+            let newState = {
+                ...state,
+                [_id]: {good, count: count < 0 ? 0 : count}
+            }
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_REMOVE() {
+            let { [_id]: removed, ...newState }  = state
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_CLEAR() {
+            localStorage.cart = JSON.stringify({})
+            return {}
+        },
+    }
+    if (type in types) {
+        return types[type]()
+    }
+    return state
+}
+
+const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
+const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
+const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
+const actionCartClear = () => ({type: 'CART_CLEAR'})
+
+
+function promiseReducer(state={}, {type, status, payload, error, name}) {
+    if (!state) {
+        return {}
+    }
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: {
+                status: status,
+                payload : payload,
+                error: error,
+            }
+        }
+    }
+    return state
+}
+
+const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
+const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
+const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
+
+
+const actionPromise = (name, promise) => (
+    async (dispatch) => {
+        dispatch(actionPending(name))
+        try {
+            let data = await promise
+            dispatch(actionResolved(name, data))
+            return data
+        }
+        catch(error){
+            dispatch(actionRejected(name, error))
+        }
+    }
+)
+
+const getGQL = url => (
+    async (query, variables={}) => {
+        let obj = await fetch(url, {
+            method: 'POST',
+            headers: {
+                "Content-Type": "application/json",
+                ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
+            },
+            body: JSON.stringify({ query, variables })
+        })
+        let a = await obj.json()
+        if (!a.data && a.errors) {
+            throw new Error(JSON.stringify(a.errors))
+        } else {
+            return a.data[Object.keys(a.data)[0]]
+        }
+    }
+)
+
+const backURL = 'http://shop-roles.node.ed.asmer.org.ua/'
+const gql = getGQL(backURL + 'graphql');
+
+
+
+const actionOrder = () => (
+    async (dispatch, getState) => {
+        let {cart} = getState()
+
+        const orderGoods = Object.entries(cart)
+            .map(([_id, {good, count}]) => ({good: {_id}, count}))
+
+        let result = await dispatch(actionPromise('order', gql(`
+                  mutation newOrder($order:OrderInput){
+                    OrderUpsert(order:$order)
+                      { _id total}
+                  }
+          `, {order: {orderGoods}})))
+        if (result?._id) {
+            dispatch(actionCartClear())
+        }
+    })
+
+
+
+
+
+const actionLogin = (login, password) => (
+    actionPromise('login', gql(`query log($login: String, $password: String) {
+        login(login: $login, password: $password)
+    }`, {login, password}))
+)
+
+const actionFullLogin = (login, password) => (
+    async (dispatch) => {
+        let token = await dispatch(actionLogin(login, password))
+        if (token) {
+            dispatch(actionAuthLogin(token))
+            location.hash = '#/category'
+        } else {
+            showErrorMessage('please, enter correct login and password', main)
+        }
+    }
+)
+
+
+const actionRegister = (login, password) => (
+    actionPromise('register', gql(`mutation reg($user:UserInput) {
+        UserUpsert(user:$user) {
+        _id 
+        }
+    }
+    `, {user: {login, password}})
+    )
+)
+
+const actionFullRegister = (login, password) => (
+    async (dispatch) => {
+        let registerId = await dispatch(actionRegister(login, password))
+
+        if (registerId) {
+            dispatch(actionFullLogin(login, password))
+        }
+    }
+)
+
+
+
+const actionRootCats = () => (
+    actionPromise('rootCats', gql(`query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+        }
+    }`))
+)
+
+const actionCatById = (_id) => (
+    actionPromise('catById', gql(`query catById($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            }
+            subCategories {
+                _id name 
+            }
+        }
+    }`, {q: JSON.stringify([{_id}])}))
+)
+
+const actionGoodById = (_id) => (
+    actionPromise('goodById', gql(`query goodById($q: String) {
+        GoodFindOne(query: $q) {
+            _id name price description images {
+            url
+            }
+        }
+    }`, {q: JSON.stringify([{_id}])}))
+)
+
+const actionGoodsByUser = (_id) => (
+    actionPromise('goodByUser', gql(`query oUser($query: String) {
+        OrderFind(query:$query){
+        _id orderGoods{
+                price count total good{
+                    _id name categories{
+                    name
+                    }
+                    images {
+                        url
+                    }
+                }
+            } 
+            owner {
+            _id login
+            }
+        }
+    }`,
+        {query: JSON.stringify([{___owner: _id}])}))
+)
+
+store.subscribe(() => {
+    const {promise, auth} = store.getState()
+    const {rootCats} = promise
+
+    if (rootCats?.status === 'PENDING') {
+        aside.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (rootCats?.payload) {
+            aside.innerHTML = ''
+            authBox.innerHTML = ''
+            const regBtn = document.createElement('a')
+            regBtn.href = '#/register'
+            regBtn.innerText = 'Register'
+            const loginBtn = document.createElement('a')
+            loginBtn.className = 'loginBtn'
+            loginBtn.href = `#/login`
+            loginBtn.innerText = 'Login'
+            const logoutBtn = document.createElement('a')
+            logoutBtn.innerText = 'Logout'
+            auth.token ? authBox.append(logoutBtn) : authBox.append(regBtn, loginBtn)
+
+            logoutBtn.onclick = () => {
+                store.dispatch(actionAuthLogout())
+            }
+            for (const {_id, name} of rootCats?.payload) {
+                const link = document.createElement('a')
+                link.href = `#/category/${_id}`
+                link.innerText = name
+                aside.append(link)
+            }
+        }
+    }
+})
+
+store.dispatch(actionRootCats())
+
+
+function createForm(parent, type, callback) {
+let {auth} = store.getState()
+    let res = `<label for="login${type}">Nick</label>
+            <input id="login${type}" type="text"/>
+            <label for="pass${type}">Password</label>
+            <input id="pass${type}" type="password"/>
+      
+        <button id="btn${type}">${type}</button>
+    </div>`
+    parent.innerHTML = res
+    return () => window[`btn${type}`].onclick = () => {
+        store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
+    }
+}
+
+let message = document.createElement('p')
+function showErrorMessage(text, parent) {
+    message.innerHTML = text
+    parent.append(message)
+    }
+
+
+
+const createCartPage = (parent) => {
+    parent.innerHTML = ''
+    const {cart} = store.getState()
+
+    const clearBtn = document.createElement('button')
+    clearBtn.innerText = "clear all"
+    if(Object.keys(cart).length !== 0) {
+        parent.append(clearBtn)
+    }
+    clearBtn.onclick = () => {
+        store.dispatch(actionCartClear())
+    }
+
+    const cartPage  = document.createElement('div')
+    if(Object.keys(cart).length === 0) {
+        showErrorMessage('Hmm... Let`s add something into the cart!', cartPage)
+    }
+    main.append(cartPage)
+
+    let cartCounter = 0
+    for(const item in cart) {
+        const {good} = cart[item]
+        const {count, good: {_id: id, name: name, price: price, images: [{url}]}} = cart[item]
+
+        cartCounter += count*price
+
+        const card = document.createElement('div')
+        card.innerHTML = `
+                        <h4>${name}</h4>
+                        </div>
+                        <img src="${backURL}/${url}" />
+                        <p>amount: </p>                                
+                        `
+
+        const inputGr = document.createElement('div')
+        card.lastElementChild.append(inputGr)
+
+        const minusBtn = document.createElement('button')
+        minusBtn.innerText = '-'
+        inputGr.append(minusBtn)
+        minusBtn.onclick = () => {
+            store.dispatch(actionCartAdd(good, -1))
+        }
+
+        const changeCount = document.createElement('input')
+        changeCount.type = 'number'
+        changeCount.value = count
+        changeCount.setAttribute('min', '1')
+        inputGr.append(changeCount)
+        changeCount.oninput = () => {
+            store.dispatch(actionCartChange(good, changeCount.value))
+        }
+
+        const plusBtn = document.createElement('button')
+        plusBtn.innerText = '+'
+        inputGr.append(plusBtn)
+        plusBtn.onclick = () => {
+            store.dispatch(actionCartAdd(good))
+        }
+
+        const deleteGood = document.createElement('button')
+        deleteGood.innerText = 'remove item'
+        deleteGood.style.display = 'block'
+        card.lastElementChild.append(deleteGood)
+        deleteGood.onclick = () => {
+            store.dispatch(actionCartRemove(good))
+        }
+
+        cartPage.append(card)
+    }
+
+    const total  = document.createElement('h5')
+    total.innerText = `Total: ${cartCounter} UAH`
+
+    const sendOrder = document.createElement('button')
+    sendOrder.innerText = 'Make an order'
+    if(Object.keys(cart).length !== 0) {
+        parent.append(total)
+        parent.append(sendOrder)
+    }
+    const {auth} = store.getState()
+    sendOrder.disabled = !auth.token;
+    sendOrder.onclick = () => {
+        store.dispatch(actionOrder())
+    }
+}
+
+
+
+// location.hash
+window.onhashchange = () => {
+    const [,route, _id] = location.hash.split('/')
+
+    const routes = {
+        category(){
+            store.dispatch(actionCatById(_id))
+        },
+        good(){
+            store.dispatch(actionGoodById(_id))
+        },
+        register(){
+            const registerFunc = createForm(main, 'Register', actionFullRegister)
+            registerFunc()
+        },
+        login(){
+            const loginFunc = createForm(main, 'Login', actionFullLogin)
+            loginFunc()
+        },
+        orders(){
+            store.dispatch(actionGoodsByUser(_id))
+        },
+        cart(){
+            createCartPage(main)
+        }
+    }
+    if (route in routes) {
+        routes[route]()
+    }
+}
+
+
+store.subscribe(() => {
+    const [,route] = location.hash.split('/')
+    if (route === 'cart') {
+        createCartPage(main)
+    }
+})
+
+
+window.onhashchange()
+
+store.subscribe(() => {
+    const {promise} = store.getState()
+    const {catById} = promise
+    const [,route, _id] = location.hash.split('/')
+
+    if (catById?.status === 'PENDING') {
+        main.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (catById?.payload && route === 'category'){
+            main.innerHTML = ''
+            const catBody  = document.createElement('div')
+            main.append(catBody)
+
+            const {name} = catById.payload;
+            catBody.innerHTML = `<h1>${name}</h1>`
+
+            if (catById.payload.subCategories) {
+                const linkList  = document.createElement('div')
+                catBody.append(linkList)
+
+                for(const {_id, name} of catById.payload.subCategories) {
+                    const link = document.createElement('a')
+                    link.href = `#/category/${_id}`
+                    link.innerText  = name
+                    link.className = 'cat'
+                    catBody.append(link)
+                }
+            if(location.hash === '#/category/') {
+                for(const {_id, name} of catById.payload) {
+                    const link = document.createElement('a')
+                    link.href = `#/category/${_id}`
+                    link.innerText  = name
+                    link.className = 'cat'
+                    catBody.append(link)
+                }
+            }
+            }
+
+            if (catById.payload.goods) {
+                const cardBody  = document.createElement('div')
+                main.append(cardBody)
+                for (const good of catById.payload.goods){
+                    const {_id, name, price, images} = good
+                    const card      = document.createElement('div')
+                    card.className = 'card'
+                    card.innerHTML = `
+                                    <img src="${backURL}/${images[0].url}" />
+                                    <div>
+                                        <h4>${name}</h4>
+                                        <h5>${price} UAH</h5>                                    
+                                        <a href="#/good/${_id}" class="showMore">
+                                            Show more
+                                        </a>
+                                    </div>
+                                    `
+                    const btnCart = document.createElement('button')
+                    btnCart.innerText = 'To cart'
+                    btnCart.onclick = () => {
+                        store.dispatch(actionCartAdd(good))
+                    }
+                    card.lastElementChild.append(btnCart)
+                    cardBody.append(card)
+                }
+            }
+        }
+    }
+})
+
+store.subscribe(() => {
+        const {promise} = store.getState()
+        const {goodById} = promise
+        const [,route, _id] = location.hash.split('/');
+
+        if (goodById?.status === 'PENDING') {
+            main.innerHTML = `<img src="Loading_icon.gif">`
+        } else {
+            if (goodById?.payload && route === 'good') {
+                main.innerHTML = ''
+                const good = goodById.payload
+                const {_id, name, images, price, description} = good
+                const card = document.createElement('div')
+                card.innerHTML = `<h2>${name}</h2>
+                                <img src="${backURL}/${images[0].url}" />
+                                <div>                                    
+                                    <h6>${description}</h6>
+                                    <strong>Цена - ${price} грн</strong>
+                                </div>
+                                `
+                const btnCart = document.createElement('button')
+                btnCart.innerText  = 'Add to cart'
+                btnCart.onclick = () => {
+                    store.dispatch(actionCartAdd(good))
+                }
+                card.append(btnCart)
+                main.append(card);
+            }
+        }
+    }
+)
+
+
+store.subscribe(() => {
+    const {auth} = store.getState()
+    const name = document.createElement('div')
+    name.innerText = `Hello, stranger`
+    const {payload} = auth
+    if (payload?.sub) {
+        userBox.innerHTML = ''
+        const {id, login}  = payload.sub
+        name.innerText = `Hello, ${login}`
+        const myOrders = document.createElement('a')
+        myOrders.innerText = 'My orders'
+        myOrders.href = `#/orders/${id}`
+        userBox.append(myOrders)
+    } else {
+        userBox.innerHTML = ''
+    }
+    userBox.append(name)
+})
+
+
+
+store.subscribe(() => {
+    const {promise} = store.getState()
+    const {goodByUser} = promise
+    const [,route] = location.hash.split('/')
+
+    if (goodByUser?.status === 'PENDING') {
+        main.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (goodByUser?.payload && route === 'orders'){
+
+            main.innerHTML = ''
+            const cardBody  = document.createElement('div')
+            main.append(cardBody)
+
+            if (goodByUser.payload) {
+                let totalMoney = 0
+
+                for (const order of goodByUser.payload) {
+
+                    if (order.orderGoods) {
+                        for (const {price, count, total, good} of order.orderGoods) {
+                            if (price !== null && count !== null && total !== null && good !== null) {
+                                totalMoney += total
+                                const {_id, name, images} = good
+
+                                const card      = document.createElement('div')
+                                card.innerHTML = `
+                                <img src="${backURL}/${images[0].url}" />
+                                <div>
+                                    <h4>${name}</h4>
+                                    // <h6>
+                                    //     bought: ${count},  ${price} UAH 
+                                    // </h6>  
+                                    <h6>
+                                        Total: ${total} UAH
+                                    </h6>   
+                                    <a href="#/good/${_id}">
+                                        show more
+                                    </a>
+                                </div>
+                                `
+                                cardBody.append(card)
+
+                            }
+                        }
+                    }
+
+                }
+                const totalBlock = document.createElement('h3')
+                totalBlock.innerText = 'Total: ' + totalMoney + ' UAH'
+                main.append(totalBlock)
+            }
+        }
+    }
+})
+
+
+
+store.subscribe(() => {
+    const {cart} = store.getState()
+    let counter = 0;
+
+    for (const key in cart) {
+        counter += cart[key].count
+    }
+    cartCounter.innerText  = counter
+})

File diff suppressed because it is too large
+ 27873 - 0
react-store-app/package-lock.json


+ 48 - 0
react-store-app/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "react-store-app",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@apollo/client": "^3.7.0",
+    "@testing-library/jest-dom": "^5.16.4",
+    "@testing-library/react": "^13.1.1",
+    "@testing-library/user-event": "^13.5.0",
+    "clsx": "^1.1.1",
+    "graphql": "^16.6.0",
+    "lodash": "^4.17.21",
+    "mobx": "^6.6.2",
+    "mobx-react": "^7.5.3",
+    "react": "^17.0.0",
+    "react-dom": "^17.0.0",
+    "react-icons": "^4.3.1",
+    "react-router-dom": "^5.1",
+    "react-scripts": "5.0.1",
+    "sass": "^1.50.0",
+    "slugify": "^1.6.5",
+    "web-vitals": "^2.1.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"
+    ]
+  }
+}

BIN
react-store-app/public/favicon.ico


+ 43 - 0
react-store-app/public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

BIN
react-store-app/public/logo192.png


BIN
react-store-app/public/logo512.png


+ 25 - 0
react-store-app/public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
react-store-app/public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 46 - 0
react-store-app/src/App.js

@@ -0,0 +1,46 @@
+import styles from "styles/App.module.scss";
+import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
+import clsx from "clsx";
+
+// PAGES
+import Home from "pages/Home";
+import Detail from "pages/Detail";
+import Category from "pages/Category";
+
+// COMPONENTS
+import Header from "components/Header";
+import BasketSidebar from "components/BasketSidebar";
+import Footer from "components/Footer";
+
+// HOOKS
+import useMobileDetect from "hooks/useMobileDetect";
+
+
+const App = () => {
+  const device = useMobileDetect();
+
+  return (
+    <Router>
+      <div className={clsx(device.type === "mobile" && styles.paddingForMobile, styles.container)}>
+        <Header />
+        <main className={styles.main}>
+          <Switch>
+            <Route path="/" exact>
+              <Home />
+            </Route>
+            <Route path="/product/:_id">
+              <Detail />
+            </Route>
+            <Route path="/category/:_id">
+              <Category />
+            </Route>
+          </Switch>
+        </main>
+        <Footer />
+      </div>
+      <BasketSidebar />
+    </Router>
+  );
+};
+
+export default App;

+ 8 - 0
react-store-app/src/App.test.js

@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+  render(<App />);
+  const linkElement = screen.getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});

+ 31 - 0
react-store-app/src/apollo/client.js

@@ -0,0 +1,31 @@
+import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
+import { setContext } from '@apollo/client/link/context';
+import { onError } from '@apollo/client/link/error';
+import {GRAPHQL_URL} from '../config';
+
+const httpLink = new HttpLink({ uri: GRAPHQL_URL });
+const authLink = setContext(async (_, { headers }) => {
+  const token = localStorage.authToken;
+
+  return {
+    headers: {
+      ...headers,
+      authorization: token ? `Bearer ${token}` : '',
+    },
+  };
+});
+
+const errorLink = onError(({ operation, graphQLErrors }) => {
+  console.warn(
+    `Query: ${operation.operationName}, graphQLErrors: "${JSON.stringify(
+      graphQLErrors,
+    )}".`,
+  );
+});
+
+const client = new ApolloClient({
+  link: errorLink.concat(authLink).concat(httpLink),
+  cache: new InMemoryCache(),
+});
+
+export default client;

+ 50 - 0
react-store-app/src/apollo/queries.js

@@ -0,0 +1,50 @@
+import { gql } from '@apollo/client';
+
+export const CATEGORY_FIND = gql`
+  query CategoryFind($query: String) {
+  CategoryFind(query:$query) {
+    _id
+    name
+    image {
+      url
+    }
+  }
+}
+`;
+
+export const GOOD_FIND = gql`
+query GoodFind($query: String) {
+  GoodFind(query:$query) {
+    _id
+    name
+    images{url}
+    price
+  }
+}  
+`;
+
+export const GOOD_FIND_ONE = gql`
+query GoodFindOne($query: String) {
+  GoodFindOne(query:$query) {
+    _id
+    name
+    images{url}
+    price
+    description
+	}
+}
+`;
+export const CATEGORY_FIND_ONE = gql`
+query CategoryFindOne($query: String) {
+  CategoryFindOne(query:$query) {
+    _id
+    name
+    goods{  
+      _id
+      name
+      images{url}
+      price
+    }
+  }
+}`;
+

+ 23 - 0
react-store-app/src/components/AddToBasketBtn.js

@@ -0,0 +1,23 @@
+import styles from "styles/AddToBasketBtn.module.scss";
+import GetIcon from "components/GetIcon";
+import cartStore from "stores/cartStore";
+
+const AddToBasketBtn = ({ data: product }) => {
+  const addToBasket = (product) => {
+    cartStore.addProduct(product);
+  };
+
+  return (
+    <button
+      className={styles.addToBasket}
+      onClick={(e) => {
+        e.preventDefault();
+        addToBasket(product);
+      }}
+    >
+      <GetIcon icon="BsFillCartPlusFill" size={18} /> add to basket
+    </button>
+  );
+};
+
+export default AddToBasketBtn;

+ 37 - 0
react-store-app/src/components/BasketItem.js

@@ -0,0 +1,37 @@
+import styles from "styles/BasketItem.module.scss";
+import Title from "components/Title";
+import GetIcon from "components/GetIcon";
+import Quantity from "components/Quantity";
+import cartStore from 'stores/cartStore';
+import {observer} from 'mobx-react';
+import {IMAGES_URL} from 'config';
+
+const BasketItem = ({ data }) => {
+  const {product} = data;
+
+  return (
+    <div className={styles.item}>
+      <div className={styles.img}>
+        {product.images[0] ? <img src={IMAGES_URL + product.images[0].url} alt="" /> : null}
+      </div>
+      <div className={styles.detail}>
+        <div className={styles.title}>
+          <Title txt={product.name} size={16} />
+        </div>
+        <div className={styles.priceContainer}>
+          <small className={styles.singlePrice}>{product.price.toFixed(2)}</small>
+          <small className={styles.quantityN}>{data.quantity}</small>
+          <small className={styles.totalPrice}> {`${(product.price * data.quantity).toFixed(2)}`} UAH</small>
+        </div>
+        <Quantity data={data} />
+      </div>
+      <div className={styles.removeItem}>
+        <button type="button" onClick={() => cartStore.removeProduct(data.product)}>
+          <GetIcon icon="BsDash" size={17} />
+        </button>
+      </div>
+    </div>
+  );
+};
+
+export default observer(BasketItem);

+ 64 - 0
react-store-app/src/components/BasketSidebar.js

@@ -0,0 +1,64 @@
+import styles from "styles/BasketSidebar.module.scss";
+import emptyCardImg from "images/empty_cart.svg";
+import GetIcon from "components/GetIcon";
+import {observer} from 'mobx-react';
+import Title from "components/Title";
+import clsx from "clsx";
+import BasketItem from "components/BasketItem";
+import { useRef } from "react";
+import cartStore from 'stores/cartStore';
+
+const BasketSidebar = () => {
+  const container = useRef();
+
+  return (
+    <div
+      className={clsx(styles.sidebarContainer, cartStore.isOpen ? styles.show : styles.hide)}
+      ref={container}
+      onClick={(event) => event.target === container.current && cartStore.setIsOpen(false)}
+    >
+      <div className={styles.sidebar}>
+        <div className={styles.header}>
+          <div className={styles.title}>
+            <Title txt="your basket" size={20} transform="uppercase" />
+            {<small>your basket has got {cartStore.count} items</small>}
+          </div>
+          <button className={styles.close} onClick={() => cartStore.setIsOpen(false)}>
+            <GetIcon icon="BsX" size={30} />
+          </button>
+        </div>
+        {cartStore.count > 0 ? (
+          <>
+            <div className={styles.items}>
+              {cartStore.items?.map((item, key) => (
+                <BasketItem data={item} key={key} />
+              ))}
+            </div>
+            <div className={styles.basketTotal}>
+              <div className={styles.total}>
+                <Title txt="basket summary" size={23} transform="uppercase" />
+                <GetIcon icon="BsFillCartCheckFill" size={25} />
+              </div>
+              <div className={styles.totalPrice}>
+                <small>total try</small>
+                <div className={styles.price}>
+                  <span>{cartStore.total.toFixed(2)}</span>
+                </div>
+              </div>
+              <button type="button" className={styles.confirmBtn}>
+                Confirm the basket
+              </button>
+            </div>
+          </>
+        ) : (
+          <div className={styles.emptyBasket}>
+            <img src={emptyCardImg} alt="" />
+            <Title txt="your basket is empty" size={23} transform="uppercase" />
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default observer(BasketSidebar);

+ 37 - 0
react-store-app/src/components/Card.js

@@ -0,0 +1,37 @@
+import styles from "styles/Card.module.scss";
+import { Link } from "react-router-dom";
+import _ from 'lodash';
+import {IMAGES_URL} from '../config';
+
+import AddToBasketBtn from "components/AddToBasketBtn";
+
+const Card = ({ product }) => {
+  return (
+    <div className={styles.card}>
+      <Link to={`/product/${product._id}`} className={styles.content}>
+        { !_.isEmpty(product.images) ?
+          <div className={styles.img}>
+          <img src={IMAGES_URL + product.images[0].url } alt="" />
+        </div>
+        : null}
+        <div className={styles.info}>
+          <div className={styles.title}>
+            <h3>{product.name}</h3>
+          </div>
+          <div className={styles.footer}>
+            {product.price ?
+            (<div className={styles.price}>
+              {product.price.toFixed(2)} <small>UAH</small>
+            </div>)
+            : null }
+            <div className={styles.btn}>
+              <AddToBasketBtn data={product} />
+            </div>
+          </div>
+        </div>
+      </Link>
+    </div>
+  );
+};
+
+export default Card;

+ 17 - 0
react-store-app/src/components/CategoryItem.js

@@ -0,0 +1,17 @@
+import styles from "styles/CategoryItem.module.scss";
+import { Link } from "react-router-dom";
+import linkBG from "images/shopBG.jpg";
+import {IMAGES_URL} from '../config';
+
+const CategoryItem = ({ data, setNavIsOpen = () => null}) => {
+  return (
+    <li className={styles.item}>
+      <Link to={`/category/${data._id}`} className={styles.sub_a} onClick={() => setNavIsOpen(false)}>
+        <img src={data.image? IMAGES_URL + data.image.url : linkBG} alt="" />
+        <h3>{data.name}</h3>
+      </Link>
+    </li>
+  );
+};
+
+export default CategoryItem;

+ 15 - 0
react-store-app/src/components/Footer.js

@@ -0,0 +1,15 @@
+import styles from "styles/Footer.module.scss";
+import GetIcon from "components/GetIcon";
+
+const Footer = () => {
+  return (
+    <footer className={styles.footer}>
+      <p>
+        <GetIcon icon="BsFillHeartFill" size={22} color="#da0037" /> <a href="http://gitlab.a-level.com.ua/Alex.ElRubio">Larychev Oleksandr</a>
+      </p>
+    </footer>
+  );
+};
+
+export default Footer;
+  

+ 9 - 0
react-store-app/src/components/GetIcon.js

@@ -0,0 +1,9 @@
+import * as Icons from "react-icons/bs";
+
+const GetIcon = ({ icon, size, color }) => {
+  const Icon = Icons[icon];
+
+  return <Icon size={size} color={color} />;
+};
+
+export default GetIcon;

+ 52 - 0
react-store-app/src/components/Header.js

@@ -0,0 +1,52 @@
+import styles from "styles/Header.module.scss";
+import { Link } from "react-router-dom";
+import GetIcon from "components/GetIcon";
+import clsx from "clsx";
+import CategoryItem from "./CategoryItem";
+import {CATEGORY_FIND} from 'apollo/queries';
+import {useQuery} from "@apollo/client";
+import cartStore from "stores/cartStore";
+import { observer } from 'mobx-react';
+import _ from 'lodash';
+
+const Header = () => {
+  const {data} = useQuery(CATEGORY_FIND, {variables: {query: "[{\"parent\":null}]"}});
+
+
+  return (
+    <header className={styles.header}>
+      <div className={styles.logo}>
+        <Link to="/">
+          <h2>react store</h2>
+        </Link>
+      </div>
+      <div className={styles.navContainer}>
+        <nav className={styles.nav}>
+          <ul>
+            <li>
+              <Link to="/" onClick={(e) => e.preventDefault()} className={styles.a}>
+                Categories
+              </Link>
+              <ul className={styles.subMenu}>{_.get(data, 'CategoryFind', []).map((cat, index) => <CategoryItem data={cat} key={index} />)}</ul>
+            </li>
+            <li>
+              <Link
+                to="/"
+                className={clsx(styles.basketBtn, styles.a)}
+                onClick={(e) => {
+                  e.preventDefault();
+                  cartStore.setIsOpen((oldState) => !oldState);
+                }}
+              >
+                <GetIcon icon="BsCart4" size={25} color="#ffffff" />
+                {cartStore.count > 0 && <span className={styles.basketLength}> {cartStore.count} </span>}
+              </Link>
+            </li>
+          </ul>
+        </nav>
+      </div>
+    </header>
+  );
+};
+
+export default observer(Header);

+ 17 - 0
react-store-app/src/components/MobileCategories.js

@@ -0,0 +1,17 @@
+import styles from "styles/MobileCategories.module.scss";
+import CategoryItem from "components/CategoryItem";
+import useMakeRequest from "hooks/useMakeRequest";
+
+const MobileCategories = ({ setNavIsOpen }) => {
+  const result = useMakeRequest("https://fakestoreapi.com/products/categories");
+
+  return (
+    <div className={styles.mobileCategories}>
+      <ul className={styles.mobileCategoriesMenu}>
+        {result.data ? result.data.map((cat, index) => <CategoryItem data={cat} key={index} setNavIsOpen={setNavIsOpen} />) : <div>{result.error}</div>}
+      </ul>
+    </div>
+  );
+};
+
+export default MobileCategories;

+ 27 - 0
react-store-app/src/components/Quantity.js

@@ -0,0 +1,27 @@
+import styles from "styles/Quantity.module.scss";
+import GetIcon from "components/GetIcon";
+import { useRef, useEffect } from "react";
+import cartStore from 'stores/cartStore';
+import {observer} from 'mobx-react';
+
+const Quantity = ({ data }) => {
+  const inp = useRef("inp");
+
+  useEffect(() => {
+    inp.current.value = data.quantity || 1;
+  }, [data.quantity]);
+
+  return (
+    <div className={styles.quantity}>
+      <button type="button" className={styles.quantityBtn} onClick={() => cartStore.substractProduct(data.product)}>
+        <GetIcon icon="BsDash" size={20} />
+      </button>
+      <input type="number" min="1" max="10" defaultValue={1} ref={inp} />
+      <button type="button" className={styles.quantityBtn} onClick={() => cartStore.addProduct(data.product)}>
+        <GetIcon icon="BsPlus" size={20} />
+      </button>
+    </div>
+  );
+};
+
+export default observer(Quantity);

+ 5 - 0
react-store-app/src/components/Title.js

@@ -0,0 +1,5 @@
+const Title = ({ txt, size, color, transform }) => {
+  return <h2 style={{ fontSize: size, color: color, textTransform: transform }}>{txt}</h2>;
+};
+
+export default Title;

+ 3 - 0
react-store-app/src/config.js

@@ -0,0 +1,3 @@
+export const GRAPHQL_URL = 'http://shop-roles.node.ed.asmer.org.ua/graphql';
+
+export const IMAGES_URL = 'http://shop-roles.node.ed.asmer.org.ua/'

+ 26 - 0
react-store-app/src/hooks/useMakeRequest.js

@@ -0,0 +1,26 @@
+import { useState, useEffect } from "react";
+
+const useMakeRequest = (endpoint) => {
+  const [result, setResult] = useState({
+    data: null,
+    error: null,
+  });
+
+  useEffect(() => {
+    const asyncFunc = async () => {
+      try {
+        const response = await fetch(endpoint);
+        const json = await response.json();
+        setResult((old) => ({ ...old, data: json }));
+      } catch (error) {
+        setResult((old) => ({ ...old, error: new Error(error).message }));
+      }
+    };
+
+    asyncFunc();
+  }, [endpoint]);
+
+  return result;
+};
+
+export default useMakeRequest;

+ 29 - 0
react-store-app/src/hooks/useMobileDetect.js

@@ -0,0 +1,29 @@
+import { useEffect, useState } from "react";
+
+const useMobileDetect = () => {
+  const [device, setDevice] = useState({
+    type: "desktop",
+  });
+
+  useEffect(() => {
+    window.addEventListener("resize", () => {
+      if (window.innerWidth <= 768) {
+        setDevice((old) => ({ ...old, type: "mobile" }));
+      } else {
+        setDevice((old) => ({ ...old, type: "desktop" }));
+      }
+    });
+
+    window.addEventListener("load", () => {
+      if (window.innerWidth <= 768) {
+        setDevice((old) => ({ ...old, type: "mobile" }));
+      } else {
+        setDevice((old) => ({ ...old, type: "desktop" }));
+      }
+    });
+  }, []);
+
+  return device;
+};
+
+export default useMobileDetect;

File diff suppressed because it is too large
+ 1 - 0
react-store-app/src/images/empty_cart.svg


BIN
react-store-app/src/images/shopBG.jpg


+ 21 - 0
react-store-app/src/index.js

@@ -0,0 +1,21 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import { ApolloProvider } from "@apollo/client";
+import "styles/index.css";
+import App from "./App";
+import reportWebVitals from "./reportWebVitals";
+import apolloClient from './apollo/client';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <ApolloProvider client={apolloClient}>
+      <App />
+    </ApolloProvider>
+  </React.StrictMode>,
+  document.getElementById("root")
+);
+
+// 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();

File diff suppressed because it is too large
+ 1 - 0
react-store-app/src/logo.svg


+ 132 - 0
react-store-app/src/old-code/full-code-gql.html

@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Shop</title>
+  <style>
+    body {
+      font-family: system-ui;
+      padding: 0;
+    }
+    header {
+      height: 50px;
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+    }
+    header a {
+      margin: 5px 10px;
+      border: 1px solid grey;
+      padding: 5px 10px;
+    }
+    a {
+      text-decoration: none;
+      color: black;
+      padding: 5px 10px;
+      transition-property: transform, border-radius;
+      transition-duration: .5s;
+    }
+    .cat {
+      color: grey;
+    }
+    img {
+      max-width: 150px;
+    }
+    #userBox {
+      display: flex;
+      align-items: baseline;
+      flex-direction: row-reverse;
+    }
+
+    #authBox {
+      display: flex;
+      min-width: 150px;
+      justify-content: space-between;
+    }
+    aside {
+      display: flex;
+      flex-direction: column;
+      margin-right: 150px;
+    }
+    aside > a {
+      margin-bottom: 15px;
+    }
+
+    #mainContainer {
+      display: flex;
+    }
+    button, .loginBtn  {
+      margin: 5px 10px;
+      border: 1px solid #FB8F1D;
+      padding: 5px 10px;
+      background-color: sandybrown;
+      color: white;
+      font-size: 16px;
+      font-family: system-ui;
+    }
+    .showMore, #userBox > a {
+      margin: 5px 10px;
+      border: 1px solid #FB8F1D;
+      padding: 5px 10px;
+    }
+    a:hover {
+      transform: translateY(5px);
+      box-shadow: 0 0 11px sandybrown;
+      border-radius: 8px;
+      animation-delay: .5s;
+    }
+    .card {
+      padding: 15px 20px;
+      margin: 35px;
+      border: 1px solid lightgrey;
+      border-radius: 20px;
+      display: flex;
+      transition-property: transform;
+      transition-duration: .5s;
+      width: 400px;
+      justify-content: space-between;
+    }
+    .card:hover {
+      transform: translateX(10px);
+    }
+    button:hover {
+      color: #FB8F1D;
+      background-color: white;
+      box-shadow: 0 0 11px sandybrown;
+    }
+
+  </style>
+</head>
+
+<body>
+
+  <header>
+
+    <a href="">Main</a>
+      <a id="cartIcon" href="#/cart">
+        Cart
+        <span id="cartCounter"></span>
+      </a>
+      <div id="userBox"></div>
+    <div id="authBox"></div>
+    </div>
+  </header>
+
+
+  <div id='mainContainer'>
+    <aside id='aside'>
+    </aside>
+
+    <main id='main'>
+      <h3>Welcome to our shop! Let's find something for you :)</h3>
+    </main>
+
+  </div>
+
+</div>
+  <script src='index.js'></script>
+</body>
+
+</html>

+ 719 - 0
react-store-app/src/old-code/full-code-gql.js

@@ -0,0 +1,719 @@
+import React, { useState } from "react";
+import "./App.css";
+
+import thunk from "redux-thunk";
+import { createStore, combineReducers, applyMiddleware } from "redux";
+import { Provider, connect } from "react-redux";
+function createStore(reducer){
+    let state       = reducer(undefined, {}) //стартовая инициализация состояния, запуск редьюсера со state === undefined
+    let cbs         = []                     //массив подписчиков
+
+    const getState  = () => state            //функция, возвращающая переменную из замыкания
+    const subscribe = cb => (cbs.push(cb),   //запоминаем подписчиков в массиве
+        () => cbs = cbs.filter(c => c !== cb)) //возвращаем функцию unsubscribe, которая удаляет подписчика из списка
+
+    const dispatch  = action => {
+        if (typeof action === 'function'){ //если action - не объект, а функция
+            return action(dispatch, getState) //запускаем эту функцию и даем ей dispatch и getState для работы
+        }
+        const newState = reducer(state, action) //пробуем запустить редьюсер
+        if (newState !== state){ //проверяем, смог ли редьюсер обработать action
+            state = newState //если смог, то обновляем state
+            for (let cb of cbs)  cb() //и запускаем подписчиков
+        }
+    }
+
+    return {
+        getState, //добавление функции getState в результирующий объект
+        dispatch,
+        subscribe //добавление subscribe в объект
+    }
+}
+
+function combineReducers(reducers) {
+    return (state={}, action) => {
+        const newState = {}
+        // перебрать все редьюсеры
+        if (reducers) {
+            for (const [reducerName, reducer] of Object.entries(reducers)) {
+                const newSubState = reducer(state[reducerName], action)
+                if (newSubState !== state[reducerName]) {
+                    newState[reducerName] = newSubState
+                }
+            }
+            // если newState не пустой, то вернуть стейт в
+            if (Object.keys(newState).length !== 0) {
+                return {...state, ...newState}
+            } else {
+                return state
+            }
+        }
+
+    }
+}
+
+const combinedReducer = combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer})
+const store = createStore(combinedReducer)
+
+store.subscribe(() => console.log(store.getState()))
+
+
+
+function jwtDecode(token){
+    try {
+        return JSON.parse(atob(token.split('.')[1]))
+    }
+    catch(e){
+    }
+}
+
+function authReducer(state, {type, token}) {
+    if (!state) {
+        if (localStorage.authToken) {
+            token = localStorage.authToken
+            type = 'AUTH_LOGIN'
+        } else {
+            return {}
+        }
+    }
+    if (type === 'AUTH_LOGIN') {
+        let payload = jwtDecode(token)
+        if (typeof payload === 'object') {
+            localStorage.authToken = token
+            return {
+                ...state,
+                token,
+                payload
+            }
+        } else {
+            return state
+        }
+    }
+    if (type === 'AUTH_LOGOUT') {
+        delete localStorage.authToken
+        location.reload()
+        return {}
+    }
+    return state
+}
+
+const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token})
+const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'})
+
+
+
+function cartReducer (state={}, {type, good={}, count=1}) {
+
+    if (Object.keys(state).length === 0 && localStorage.cart) {
+        let currCart = JSON.parse(localStorage.cart)
+        if (currCart && Object.keys(currCart).length !== 0) {
+            state = currCart
+        }
+    }
+
+    const {_id} = good
+
+    const types = {
+        CART_ADD() {
+            count = +count
+            if (!count) {
+                return state
+            }
+            let newState = {
+                ...state,
+                [_id]: {good, count: (count + (state[_id]?.count || 0)) < 1 ? 1 : count + (state[_id]?.count || 0)}
+            }
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_CHANGE() {
+            count = +count
+            if (!count) {
+                return state
+            }
+            let newState = {
+                ...state,
+                [_id]: {good, count: count < 0 ? 0 : count}
+            }
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_REMOVE() {
+            let { [_id]: removed, ...newState }  = state
+            localStorage.cart = JSON.stringify(newState)
+            return newState
+        },
+        CART_CLEAR() {
+            localStorage.cart = JSON.stringify({})
+            return {}
+        },
+    }
+    if (type in types) {
+        return types[type]()
+    }
+    return state
+}
+
+const actionCartAdd = (good, count) => ({type: 'CART_ADD', good, count})
+const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
+const actionCartRemove = (good) => ({type: 'CART_REMOVE', good})
+const actionCartClear = () => ({type: 'CART_CLEAR'})
+
+
+function promiseReducer(state={}, {type, status, payload, error, name}) {
+    if (!state) {
+        return {}
+    }
+    if (type === 'PROMISE') {
+        return {
+            ...state,
+            [name]: {
+                status: status,
+                payload : payload,
+                error: error,
+            }
+        }
+    }
+    return state
+}
+
+const actionPending = (name) => ({type: 'PROMISE', status: 'PENDING', name})
+const actionResolved = (name, payload) => ({type: 'PROMISE', status: 'RESOLVED', name, payload})
+const actionRejected = (name, error) => ({type: 'PROMISE', status: 'REJECTED', name, error})
+
+
+const actionPromise = (name, promise) => (
+    async (dispatch) => {
+        dispatch(actionPending(name))
+        try {
+            let data = await promise
+            dispatch(actionResolved(name, data))
+            return data
+        }
+        catch(error){
+            dispatch(actionRejected(name, error))
+        }
+    }
+)
+
+const getGQL = url => (
+    async (query, variables={}) => {
+        let obj = await fetch(url, {
+            method: 'POST',
+            headers: {
+                "Content-Type": "application/json",
+                ...(localStorage.authToken ? {Authorization: "Bearer " + localStorage.authToken} : {})
+            },
+            body: JSON.stringify({ query, variables })
+        })
+        let a = await obj.json()
+        if (!a.data && a.errors) {
+            throw new Error(JSON.stringify(a.errors))
+        } else {
+            return a.data[Object.keys(a.data)[0]]
+        }
+    }
+)
+
+const backURL = 'http://shop-roles.node.ed.asmer.org.ua/'
+const gql = getGQL(backURL + 'graphql');
+
+
+
+const actionOrder = () => (
+    async (dispatch, getState) => {
+        let {cart} = getState()
+
+        const orderGoods = Object.entries(cart)
+            .map(([_id, {good, count}]) => ({good: {_id}, count}))
+
+        let result = await dispatch(actionPromise('order', gql(`
+                  mutation newOrder($order:OrderInput){
+                    OrderUpsert(order:$order)
+                      { _id total}
+                  }
+          `, {order: {orderGoods}})))
+        if (result?._id) {
+            dispatch(actionCartClear())
+        }
+    })
+
+
+
+
+
+const actionLogin = (login, password) => (
+    actionPromise('login', gql(`query log($login: String, $password: String) {
+        login(login: $login, password: $password)
+    }`, {login, password}))
+)
+
+const actionFullLogin = (login, password) => (
+    async (dispatch) => {
+        let token = await dispatch(actionLogin(login, password))
+        if (token) {
+            dispatch(actionAuthLogin(token))
+            location.hash = '#/category'
+        } else {
+            showErrorMessage('please, enter correct login and password', main)
+        }
+    }
+)
+
+
+const actionRegister = (login, password) => (
+    actionPromise('register', gql(`mutation reg($user:UserInput) {
+        UserUpsert(user:$user) {
+        _id 
+        }
+    }
+    `, {user: {login, password}})
+    )
+)
+
+const actionFullRegister = (login, password) => (
+    async (dispatch) => {
+        let registerId = await dispatch(actionRegister(login, password))
+
+        if (registerId) {
+            dispatch(actionFullLogin(login, password))
+        }
+    }
+)
+
+
+
+const actionRootCats = () => (
+    actionPromise('rootCats', gql(`query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+        }
+    }`))
+)
+
+const actionCatById = (_id) => (
+    actionPromise('catById', gql(`query catById($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            }
+            subCategories {
+                _id name 
+            }
+        }
+    }`, {q: JSON.stringify([{_id}])}))
+)
+
+const actionGoodById = (_id) => (
+    actionPromise('goodById', gql(`query goodById($q: String) {
+        GoodFindOne(query: $q) {
+            _id name price description images {
+            url
+            }
+        }
+    }`, {q: JSON.stringify([{_id}])}))
+)
+
+const actionGoodsByUser = (_id) => (
+    actionPromise('goodByUser', gql(`query oUser($query: String) {
+        OrderFind(query:$query){
+        _id orderGoods{
+                price count total good{
+                    _id name categories{
+                    name
+                    }
+                    images {
+                        url
+                    }
+                }
+            } 
+            owner {
+            _id login
+            }
+        }
+    }`,
+        {query: JSON.stringify([{___owner: _id}])}))
+)
+
+store.subscribe(() => {
+    const {promise, auth} = store.getState()
+    const {rootCats} = promise
+
+    if (rootCats?.status === 'PENDING') {
+        aside.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (rootCats?.payload) {
+            aside.innerHTML = ''
+            authBox.innerHTML = ''
+            const regBtn = document.createElement('a')
+            regBtn.href = '#/register'
+            regBtn.innerText = 'Register'
+            const loginBtn = document.createElement('a')
+            loginBtn.className = 'loginBtn'
+            loginBtn.href = `#/login`
+            loginBtn.innerText = 'Login'
+            const logoutBtn = document.createElement('a')
+            logoutBtn.innerText = 'Logout'
+            auth.token ? authBox.append(logoutBtn) : authBox.append(regBtn, loginBtn)
+
+            logoutBtn.onclick = () => {
+                store.dispatch(actionAuthLogout())
+            }
+            for (const {_id, name} of rootCats?.payload) {
+                const link = document.createElement('a')
+                link.href = `#/category/${_id}`
+                link.innerText = name
+                aside.append(link)
+            }
+        }
+    }
+})
+
+store.dispatch(actionRootCats())
+
+
+function createForm(parent, type, callback) {
+let {auth} = store.getState()
+    let res = `<label for="login${type}">Nick</label>
+            <input id="login${type}" type="text"/>
+            <label for="pass${type}">Password</label>
+            <input id="pass${type}" type="password"/>
+      
+        <button id="btn${type}">${type}</button>
+    </div>`
+    parent.innerHTML = res
+    return () => window[`btn${type}`].onclick = () => {
+        store.dispatch(callback(window[`login${type}`].value, window[`pass${type}`].value))
+    }
+}
+
+let message = document.createElement('p')
+function showErrorMessage(text, parent) {
+    message.innerHTML = text
+    parent.append(message)
+    }
+
+
+
+const createCartPage = (parent) => {
+    parent.innerHTML = ''
+    const {cart} = store.getState()
+
+    const clearBtn = document.createElement('button')
+    clearBtn.innerText = "clear all"
+    if(Object.keys(cart).length !== 0) {
+        parent.append(clearBtn)
+    }
+    clearBtn.onclick = () => {
+        store.dispatch(actionCartClear())
+    }
+
+    const cartPage  = document.createElement('div')
+    if(Object.keys(cart).length === 0) {
+        showErrorMessage('Hmm... Let`s add something into the cart!', cartPage)
+    }
+    main.append(cartPage)
+
+    let cartCounter = 0
+    for(const item in cart) {
+        const {good} = cart[item]
+        const {count, good: {_id: id, name: name, price: price, images: [{url}]}} = cart[item]
+
+        cartCounter += count*price
+
+        const card = document.createElement('div')
+        card.innerHTML = `
+                        <h4>${name}</h4>
+                        </div>
+                        <img src="${backURL}/${url}" />
+                        <p>amount: </p>                                
+                        `
+
+        const inputGr = document.createElement('div')
+        card.lastElementChild.append(inputGr)
+
+        const minusBtn = document.createElement('button')
+        minusBtn.innerText = '-'
+        inputGr.append(minusBtn)
+        minusBtn.onclick = () => {
+            store.dispatch(actionCartAdd(good, -1))
+        }
+
+        const changeCount = document.createElement('input')
+        changeCount.type = 'number'
+        changeCount.value = count
+        changeCount.setAttribute('min', '1')
+        inputGr.append(changeCount)
+        changeCount.oninput = () => {
+            store.dispatch(actionCartChange(good, changeCount.value))
+        }
+
+        const plusBtn = document.createElement('button')
+        plusBtn.innerText = '+'
+        inputGr.append(plusBtn)
+        plusBtn.onclick = () => {
+            store.dispatch(actionCartAdd(good))
+        }
+
+        const deleteGood = document.createElement('button')
+        deleteGood.innerText = 'remove item'
+        deleteGood.style.display = 'block'
+        card.lastElementChild.append(deleteGood)
+        deleteGood.onclick = () => {
+            store.dispatch(actionCartRemove(good))
+        }
+
+        cartPage.append(card)
+    }
+
+    const total  = document.createElement('h5')
+    total.innerText = `Total: ${cartCounter} UAH`
+
+    const sendOrder = document.createElement('button')
+    sendOrder.innerText = 'Make an order'
+    if(Object.keys(cart).length !== 0) {
+        parent.append(total)
+        parent.append(sendOrder)
+    }
+    const {auth} = store.getState()
+    sendOrder.disabled = !auth.token;
+    sendOrder.onclick = () => {
+        store.dispatch(actionOrder())
+    }
+}
+
+
+
+// location.hash
+window.onhashchange = () => {
+    const [,route, _id] = location.hash.split('/')
+
+    const routes = {
+        category(){
+            store.dispatch(actionCatById(_id))
+        },
+        good(){
+            store.dispatch(actionGoodById(_id))
+        },
+        register(){
+            const registerFunc = createForm(main, 'Register', actionFullRegister)
+            registerFunc()
+        },
+        login(){
+            const loginFunc = createForm(main, 'Login', actionFullLogin)
+            loginFunc()
+        },
+        orders(){
+            store.dispatch(actionGoodsByUser(_id))
+        },
+        cart(){
+            createCartPage(main)
+        }
+    }
+    if (route in routes) {
+        routes[route]()
+    }
+}
+
+
+store.subscribe(() => {
+    const [,route] = location.hash.split('/')
+    if (route === 'cart') {
+        createCartPage(main)
+    }
+})
+
+
+window.onhashchange()
+
+store.subscribe(() => {
+    const {promise} = store.getState()
+    const {catById} = promise
+    const [,route, _id] = location.hash.split('/')
+
+    if (catById?.status === 'PENDING') {
+        main.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (catById?.payload && route === 'category'){
+            main.innerHTML = ''
+            const catBody  = document.createElement('div')
+            main.append(catBody)
+
+            const {name} = catById.payload;
+            catBody.innerHTML = `<h1>${name}</h1>`
+
+            if (catById.payload.subCategories) {
+                const linkList  = document.createElement('div')
+                catBody.append(linkList)
+
+                for(const {_id, name} of catById.payload.subCategories) {
+                    const link = document.createElement('a')
+                    link.href = `#/category/${_id}`
+                    link.innerText  = name
+                    link.className = 'cat'
+                    catBody.append(link)
+                }
+            if(location.hash === '#/category/') {
+                for(const {_id, name} of catById.payload) {
+                    const link = document.createElement('a')
+                    link.href = `#/category/${_id}`
+                    link.innerText  = name
+                    link.className = 'cat'
+                    catBody.append(link)
+                }
+            }
+            }
+
+            if (catById.payload.goods) {
+                const cardBody  = document.createElement('div')
+                main.append(cardBody)
+                for (const good of catById.payload.goods){
+                    const {_id, name, price, images} = good
+                    const card      = document.createElement('div')
+                    card.className = 'card'
+                    card.innerHTML = `
+                                    <img src="${backURL}/${images[0].url}" />
+                                    <div>
+                                        <h4>${name}</h4>
+                                        <h5>${price} UAH</h5>                                    
+                                        <a href="#/good/${_id}" class="showMore">
+                                            Show more
+                                        </a>
+                                    </div>
+                                    `
+                    const btnCart = document.createElement('button')
+                    btnCart.innerText = 'To cart'
+                    btnCart.onclick = () => {
+                        store.dispatch(actionCartAdd(good))
+                    }
+                    card.lastElementChild.append(btnCart)
+                    cardBody.append(card)
+                }
+            }
+        }
+    }
+})
+
+store.subscribe(() => {
+        const {promise} = store.getState()
+        const {goodById} = promise
+        const [,route, _id] = location.hash.split('/');
+
+        if (goodById?.status === 'PENDING') {
+            main.innerHTML = `<img src="Loading_icon.gif">`
+        } else {
+            if (goodById?.payload && route === 'good') {
+                main.innerHTML = ''
+                const good = goodById.payload
+                const {_id, name, images, price, description} = good
+                const card = document.createElement('div')
+                card.innerHTML = `<h2>${name}</h2>
+                                <img src="${backURL}/${images[0].url}" />
+                                <div>                                    
+                                    <h6>${description}</h6>
+                                    <strong>Цена - ${price} грн</strong>
+                                </div>
+                                `
+                const btnCart = document.createElement('button')
+                btnCart.innerText  = 'Add to cart'
+                btnCart.onclick = () => {
+                    store.dispatch(actionCartAdd(good))
+                }
+                card.append(btnCart)
+                main.append(card);
+            }
+        }
+    }
+)
+
+
+store.subscribe(() => {
+    const {auth} = store.getState()
+    const name = document.createElement('div')
+    name.innerText = `Hello, stranger`
+    const {payload} = auth
+    if (payload?.sub) {
+        userBox.innerHTML = ''
+        const {id, login}  = payload.sub
+        name.innerText = `Hello, ${login}`
+        const myOrders = document.createElement('a')
+        myOrders.innerText = 'My orders'
+        myOrders.href = `#/orders/${id}`
+        userBox.append(myOrders)
+    } else {
+        userBox.innerHTML = ''
+    }
+    userBox.append(name)
+})
+
+
+
+store.subscribe(() => {
+    const {promise} = store.getState()
+    const {goodByUser} = promise
+    const [,route] = location.hash.split('/')
+
+    if (goodByUser?.status === 'PENDING') {
+        main.innerHTML = `<img src="Loading_icon.gif">`
+    } else {
+        if (goodByUser?.payload && route === 'orders'){
+
+            main.innerHTML = ''
+            const cardBody  = document.createElement('div')
+            main.append(cardBody)
+
+            if (goodByUser.payload) {
+                let totalMoney = 0
+
+                for (const order of goodByUser.payload) {
+
+                    if (order.orderGoods) {
+                        for (const {price, count, total, good} of order.orderGoods) {
+                            if (price !== null && count !== null && total !== null && good !== null) {
+                                totalMoney += total
+                                const {_id, name, images} = good
+
+                                const card      = document.createElement('div')
+                                card.innerHTML = `
+                                <img src="${backURL}/${images[0].url}" />
+                                <div>
+                                    <h4>${name}</h4>
+                                    // <h6>
+                                    //     bought: ${count},  ${price} UAH 
+                                    // </h6>  
+                                    <h6>
+                                        Total: ${total} UAH
+                                    </h6>   
+                                    <a href="#/good/${_id}">
+                                        show more
+                                    </a>
+                                </div>
+                                `
+                                cardBody.append(card)
+
+                            }
+                        }
+                    }
+
+                }
+                const totalBlock = document.createElement('h3')
+                totalBlock.innerText = 'Total: ' + totalMoney + ' UAH'
+                main.append(totalBlock)
+            }
+        }
+    }
+})
+
+
+
+store.subscribe(() => {
+    const {cart} = store.getState()
+    let counter = 0;
+
+    for (const key in cart) {
+        counter += cart[key].count
+    }
+    cartCounter.innerText  = counter
+})

+ 51 - 0
react-store-app/src/pages/Category.js

@@ -0,0 +1,51 @@
+import styles from "styles/Category.module.scss";
+import Card from "components/Card";
+import Title from "components/Title";
+import useMakeRequest from "hooks/useMakeRequest";
+import { useParams } from "react-router-dom";
+import _ from 'lodash';
+import { CATEGORY_FIND_ONE, GOOD_FIND_ONE } from "apollo/queries";
+import { useQuery } from "@apollo/client";
+
+const Category = () => {
+  const { _id } = useParams();
+  const {data, loading, error} = useQuery(CATEGORY_FIND_ONE, {variables: {query: `[{\"_id\" : \"${_id}\"}]`}});
+
+  const category = _.get(data,'CategoryFindOne', null);
+  const product = _.get(category,'goods', null);
+  const categoryName = _.get(category,'name', null);
+
+
+  if (!product) {
+    return (
+      <div style={{ width: "100%", display: "flex", justifyContent: "center", marginTop: "30px" }}>
+        <Title txt="Loading..." size={25} transform="uppercase" />
+      </div>
+    );
+  } else {
+    return (
+      <section className={styles.category}>
+        <div className={styles.container}>
+          <div className={styles.row}>
+            {product && (
+              <div className={styles.title}>
+                <Title txt={categoryName} color="#171717" size={22} transform="uppercase" />  
+              </div>
+            )}
+          </div>
+          <div className={styles.row}>
+            {product ? (
+              product.map((product, key) => <Card product={product} key={key} />)
+            ) : (
+              <div style={{ width: "100%", display: "flex", justifyContent: "center" }}>
+                <Title txt={product.error} size={25} transform="uppercase" />
+              </div>
+            )}
+          </div>
+        </div>
+      </section>
+    );
+  }
+};
+
+export default Category;

+ 52 - 0
react-store-app/src/pages/Detail.js

@@ -0,0 +1,52 @@
+import Title from "components/Title";
+import { useParams } from "react-router-dom";
+import styles from "styles/Detail.module.scss";
+import { useQuery } from "@apollo/client";
+import { GOOD_FIND_ONE } from "apollo/queries";
+import _ from 'lodash';
+import {IMAGES_URL} from '../config';
+
+const Detail = () => {
+  const {_id} = useParams();
+
+  const {data} = useQuery(GOOD_FIND_ONE, {variables: {query: `[{"_id" : "${_id}"}]`}});
+  const product = _.get(data,'GoodFindOne', null);
+
+  return (
+    <section className={styles.detail}>
+      {!product ? (
+        <div style={{ width: "100%", display: "flex", justifyContent: "center" }}>
+          <Title txt="Loading..." size={25} transform="uppercase" />
+        </div>
+      ) : (
+        <div className={styles.content}>
+          <div className={styles.top}>
+              { !_.isEmpty(product.images) ?
+                <div className={styles.img}>
+                  <img src={IMAGES_URL + product.images[0].url } alt="" />
+                </div>
+              : null}
+            <div className={styles.info}>
+              <div className={styles.title}>
+                <Title txt={product.name} transform="uppercase" size={20} />
+              </div>
+                {product.price ?
+                  (<div className={styles.price}>
+                    <p>
+                      {product.price.toFixed(2)} <small>UAH</small>
+                    </p>
+                  </div>)
+                : null }
+            </div>
+          </div>
+          <div className={styles.bottom}>
+            <Title txt="Description" size={20} transform="capitalize" />
+            <p className={styles.desc}>{product.description}</p>
+          </div>
+        </div>
+      )}
+    </section>
+  );
+};
+
+export default Detail;

+ 47 - 0
react-store-app/src/pages/Home.js

@@ -0,0 +1,47 @@
+import styles from "styles/Home.module.scss";
+import Card from "components/Card";
+import Title from "components/Title";
+import { GOOD_FIND } from "apollo/queries";
+import { useQuery } from "@apollo/client";
+import _ from 'lodash';
+
+const Home = () => {
+
+  const {data, loading, error} = useQuery(GOOD_FIND, {variables: {query: "[{}]"}});
+    console.log(data);
+
+  if (!data) {
+    if (error) {
+      return (
+        <div style={{ width: "100%", display: "flex", justifyContent: "center", marginTop: "30px" }}>
+          <Title txt={error} size={25} transform="uppercase" />
+        </div>
+      );
+    } else if (loading) {
+      return (
+        <div style={{ width: "100%", display: "flex", justifyContent: "center", marginTop: "30px" }}>
+          <Title txt="Loading..." size={25} transform="uppercase" />
+        </div>
+      );
+    }
+  } else {
+    return (
+      <section className={styles.home}>
+        <div className={styles.container}>
+          <div className={styles.row}>
+              <div className={styles.title}>
+                <Title txt="all products" color="#171717" size={22} transform="uppercase" />
+              </div>
+          </div>
+          <div className={styles.row}>
+            {
+              _.get(data, 'GoodFind').filter(({name}) => name).map((product, key) => <Card product={product} key={key} />)
+            }
+          </div>
+        </div>
+      </section>
+    );
+  }
+};
+
+export default Home;

+ 13 - 0
react-store-app/src/reportWebVitals.js

@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  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;

+ 5 - 0
react-store-app/src/setupTests.js

@@ -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';

+ 61 - 0
react-store-app/src/stores/cartStore.js

@@ -0,0 +1,61 @@
+import { action, makeObservable, observable, computed } from 'mobx';
+
+class CartStore {
+  constructor() {
+    makeObservable(this, {
+      items: observable,
+      isOpen: observable,
+      addProduct: action,
+      substractProduct: action,
+      removeProduct: action,
+      setIsOpen: action,
+      count: computed,
+      total: computed,
+    });
+  }
+
+  items = [];
+  isOpen = false;
+
+  get count() {
+    return this.items.reduce((accumulator, item) => accumulator + item.quantity, 0);
+  }
+
+  get total() {
+    return this.items.reduce((accumulator, item) => accumulator + (item.quantity * item.product.price), 0);
+  }
+
+  addProduct(product) {
+    const index = this.items.findIndex(item => product._id === item.product._id);
+
+    if (index !== -1) {
+      this.items[index].quantity ++;
+    } else {
+      this.items = this.items.concat({product, quantity: 1});
+    }
+  }
+
+  substractProduct(product) {
+    const index = this.items.findIndex(item => product._id === item.product._id);
+
+    if (index === -1) {
+      return;
+    }
+
+    if (this.items[index].quantity > 1) {
+      this.items[index].quantity--;
+    } else {
+      this.removeProduct(product);
+    }
+  }
+
+  removeProduct(product) {
+    this.items = this.items.filter(item => product._id !== item.product._id);
+  }
+
+  setIsOpen(state) {
+    this.isOpen = state;
+  }
+}
+
+export default new CartStore();

+ 33 - 0
react-store-app/src/styles/AddToBasketBtn.module.scss

@@ -0,0 +1,33 @@
+@import "styles/_variables.scss";
+
+.addToBasket {
+  flex: 1;
+  padding: 0 10px;
+  height: 37px;
+  border-radius: $border-radius;
+  text-transform: uppercase;
+  font-family: inherit;
+  font-weight: 600;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  transition: all 0.5s;
+  color: $white;
+  background-color: rgba($color: $red, $alpha: 0.8);
+}
+
+@media (max-width: 768px) {
+  .card {
+    .content {
+      .footer {
+        .addToBasket {
+          min-height: 40px;
+          width: 100%;
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+}

+ 15 - 0
react-store-app/src/styles/App.module.scss

@@ -0,0 +1,15 @@
+@import "styles/_variables.scss";
+
+.container {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+
+  .main {
+    flex: 1;
+  }
+
+  &.paddingForMobile {
+    padding-bottom: 50px;
+  }
+}

+ 68 - 0
react-store-app/src/styles/BasketItem.module.scss

@@ -0,0 +1,68 @@
+@import "styles/_variables.scss";
+
+.item {
+  border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.15);
+  display: flex;
+  padding: 1rem;
+
+  .img {
+    width: 100px;
+    height: 100px;
+    border-radius: $border-radius;
+    border: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+    padding: 5px;
+
+    > img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+
+  .detail {
+    flex: 1;
+    padding-left: 10px;
+
+    .priceContainer {
+      display: flex;
+      margin-top: 10px;
+
+      > small {
+        font-weight: 600;
+        padding: 5px 0;
+
+        &:first-of-type {
+          &::after {
+            content: "X";
+            margin-left: 5px;
+            margin-right: 5px;
+          }
+        }
+
+        &:last-of-type {
+          margin-left: auto;
+          font-weight: 600;
+
+          &::before {
+            content: "=";
+            margin-right: 5px;
+          }
+        }
+      }
+    }
+  }
+}
+
+.removeItem {
+  > button {
+    background-color: $red;
+    color: $white;
+    width: 20px;
+    height: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: $border-radius;
+    cursor: pointer;
+  }
+}

+ 134 - 0
react-store-app/src/styles/BasketSidebar.module.scss

@@ -0,0 +1,134 @@
+@import "styles/_variables.scss";
+
+.sidebarContainer {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  background-color: rgba($color: $black, $alpha: 0.75);
+  z-index: 1;
+
+  .sidebar {
+    width: 400px;
+    height: 100%;
+    background-color: $white;
+    margin-left: auto;
+    position: relative;
+
+    .header {
+      display: flex;
+      align-items: center;
+      padding: 1rem;
+
+      small {
+        font-weight: 500;
+        text-transform: capitalize;
+      }
+
+      .close {
+        margin-left: auto;
+        background-color: $red;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: $white;
+        border-radius: $border-radius;
+        cursor: pointer;
+      }
+    }
+
+    .items {
+      display: flex;
+      flex-direction: column;
+      overflow-y: auto;
+      max-height: 60%;
+    }
+
+    .basketTotal {
+      position: absolute;
+      width: 100%;
+      height: 150px;
+      bottom: 0;
+      padding: 1rem;
+      border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.15);
+
+      .total {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+      }
+
+      .totalPrice {
+        margin-top: 20px;
+        display: flex;
+        align-items: baseline;
+        justify-content: space-between;
+
+        > small {
+          text-transform: uppercase;
+          font-weight: 700;
+        }
+
+        .price {
+          display: flex;
+          align-items: center;
+          font-weight: 700;
+          font-size: 1.25rem;
+        }
+      }
+
+      .confirmBtn {
+        width: 100%;
+        margin-top: 10px;
+        background-color: #019267;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: $white;
+        font-family: inherit;
+        font-size: 1.15rem;
+        padding: 0 10px;
+        height: 37px;
+        font-weight: 500;
+        border-radius: $border-radius;
+        cursor: pointer;
+      }
+    }
+
+    .emptyBasket {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      padding: 0 1rem;
+
+      > img {
+        width: 250px;
+        height: 250px;
+        object-fit: contain;
+        object-position: center;
+      }
+
+      > button {
+        width: 100%;
+        height: 37px;
+        border-radius: $border-radius;
+        background-color: $red;
+        color: $white;
+        font-family: inherit;
+        font-weight: 500;
+        font-size: 1rem;
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+.hide {
+  display: none;
+}
+
+.show {
+  display: block;
+}

+ 123 - 0
react-store-app/src/styles/Card.module.scss

@@ -0,0 +1,123 @@
+@import "styles/_variables.scss";
+
+.card {
+  width: calc(100% / 6);
+  padding: 0.5rem;
+
+  .content {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    width: 100%;
+    height: 100%;
+    border: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+    border-radius: $border-radius;
+    transition: all 0.5s;
+
+    .img {
+      width: 100%;
+      height: 155px;
+      padding: 10px;
+
+      > img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        object-position: center;
+        transition: all 0.5s;
+      }
+    }
+
+    .info {
+      .title {
+        padding: 10px;
+        border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+
+        h3 {
+          font-weight: 500;
+          font-size: 0.9rem;
+          display: -webkit-box;
+          -webkit-box-orient: vertical;
+          -webkit-line-clamp: 1;
+          overflow: hidden;
+        }
+      }
+
+      .footer {
+        display: flex;
+        align-items: center;
+        padding: 10px;
+
+        .price {
+          flex: 1;
+          display: flex;
+          flex-direction: row;
+          align-items: baseline;
+          font-weight: 500;
+
+          small {
+            margin-left: 5px;
+          }
+        }
+
+        .btn {
+          width: auto;
+        }
+      }
+    }
+
+    &:hover {
+      > .img {
+        > img {
+          transition: all 0.5s;
+          transform: scale(1.05);
+        }
+      }
+      transition: all 0.5s;
+      border-color: $dark-gray;
+    }
+  }
+}
+
+@media (max-width: 1366px) {
+  .card {
+    width: calc(100% / 4);
+  }
+}
+
+@media (max-width: 1200px) {
+  .card {
+    width: calc(100% / 3);
+  }
+}
+
+@media (max-width: 900px) {
+  .card {
+    width: calc(100% / 2);
+  }
+}
+
+@media (max-width: 768px) {
+  .card {
+    .content {
+      .footer {
+        flex-direction: column;
+
+        .btn {
+          min-width: 100%;
+          margin-top: 10px;
+
+          & > * {
+            width: 100%;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 450px) {
+  .card {
+    width: 100%;
+  }
+}

+ 23 - 0
react-store-app/src/styles/Category.module.scss

@@ -0,0 +1,23 @@
+@import "styles/_variables.scss";
+
+.category {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .container {
+    width: 100%;
+    max-width: 1920px;
+    margin-top: 1rem;
+    padding: 0.5rem;
+
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+
+      .title {
+        padding: 0.5rem;
+      }
+    }
+  }
+}

+ 46 - 0
react-store-app/src/styles/CategoryItem.module.scss

@@ -0,0 +1,46 @@
+@import "styles/_variables.scss";
+
+.item {
+  width: 50%;
+  height: 100%;
+  display: flex;
+  flex-wrap: wrap;
+  padding: 3px;
+
+  .sub_a {
+    width: 100%;
+    height: 100px;
+    position: relative;
+    border-radius: $border-radius;
+    overflow: hidden;
+    box-shadow: rgba(0, 0, 0, 0.15) 0px 5px 15px 0px;
+
+    > img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      object-position: bottom;
+      filter: grayscale(85%) blur(3px);
+      transition: all 0.5s;
+    }
+
+    > h3 {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: $white;
+      text-transform: uppercase;
+      font-weight: 500;
+      letter-spacing: 5px;
+      text-align: center;
+    }
+
+    &:hover {
+      > img {
+        transition: all 0.5s;
+        filter: grayscale(50%);
+      }
+    }
+  }
+}

+ 139 - 0
react-store-app/src/styles/Detail.module.scss

@@ -0,0 +1,139 @@
+@import "styles/_variables.scss";
+
+.detail {
+  padding: 1rem;
+  display: flex;
+  justify-content: center;
+
+  .content {
+    width: 768px;
+    margin-top: 1rem;
+
+    .top {
+      display: flex;
+      width: 100%;
+
+      .img {
+        min-width: 300px;
+        max-width: 300px;
+        height: 300px;
+        padding: 10px;
+        border: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+        border-radius: $border-radius;
+
+        img {
+          width: 100%;
+          height: 100%;
+          object-fit: contain;
+        }
+      }
+
+      .info {
+        padding-left: 10px;
+        width: 100%;
+
+        .title {
+          color: $dark-gray;
+        }
+
+        .category {
+          margin-top: 10px;
+          font-weight: 500;
+        }
+
+        .rating {
+          margin-top: 10px;
+          display: flex;
+          align-items: center;
+          justify-content: flex-end;
+
+          .stars {
+            display: flex;
+            gap: 2px;
+          }
+        }
+
+        .price {
+          display: flex;
+          font-weight: 500;
+          font-size: 1.45rem;
+
+          p {
+            width: 100%;
+            border-radius: $border-radius;
+            color: $black;
+          }
+        }
+
+        .addToBasketAndQuantity {
+          display: inline-flex;
+          align-items: center;
+          margin-top: 10px;
+
+          .addToBasket {
+            padding: 0 10px;
+            flex: 1;
+            height: 37px;
+            border-radius: $border-radius;
+            text-transform: uppercase;
+            font-family: inherit;
+            font-weight: 600;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 5px;
+            transition: all 0.5s;
+            color: $white;
+            background-color: rgba($color: $red, $alpha: 0.8);
+          }
+
+          .quantityBox {
+            margin-right: 10px;
+          }
+        }
+      }
+    }
+
+    .bottom {
+      margin-top: 30px;
+
+      .desc {
+        font-size: 0.95rem;
+        font-weight: 400;
+        border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+        padding-top: 10px;
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .detail {
+    .content {
+      width: 100%;
+    }
+  }
+}
+
+@media (max-width: 600px) {
+  .detail {
+    .content {
+      .top {
+        flex-direction: column;
+
+        .img {
+          min-width: 100%;
+        }
+
+        .addToBasketAndQuantity {
+          width: 100%;
+        }
+
+        .info {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+}

+ 18 - 0
react-store-app/src/styles/Footer.module.scss

@@ -0,0 +1,18 @@
+@import "styles/_variables.scss";
+
+.footer {
+  padding: 0 1rem;
+  background-color: $silver;
+  height: 50px;
+  display: flex;
+  align-items: center;
+
+  p {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-weight: 500;
+    gap: 10px;
+    text-transform: uppercase;
+  }
+}

+ 118 - 0
react-store-app/src/styles/Header.module.scss

@@ -0,0 +1,118 @@
+@import "styles/_variables.scss";
+
+.header {
+  background-color: $silver;
+  display: flex;
+  padding: 0 1rem;
+  position: sticky;
+  top: 0;
+  z-index: 1;
+
+  .logo {
+    flex: 1;
+    display: flex;
+    align-items: center;
+
+    h2 {
+      text-transform: uppercase;
+      font-weight: 500;
+    }
+  }
+
+  .navContainer {
+    nav {
+      height: 75px;
+      position: relative;
+
+      > ul {
+        height: 100%;
+        display: flex;
+
+        > li {
+          height: 100%;
+
+          .subMenu {
+            position: absolute;
+            width: 600px;
+            top: calc(5px + 100%);
+            left: auto;
+            right: 0;
+            display: flex;
+            flex-wrap: wrap;
+            padding: 5px;
+            border: 1px solid rgba($color: $dark-gray, $alpha: 0.25);
+            border-radius: $border-radius;
+            margin-top: -20px;
+            transition: all 0.1s;
+            background-color: $white;
+            visibility: hidden;
+            opacity: 0;
+
+            &::before {
+              content: "";
+              background-color: transparent;
+              position: absolute;
+              width: 100%;
+              height: 5px;
+              top: -5px;
+              left: 0;
+            }
+          }
+
+          .a {
+            height: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-weight: 500;
+            text-transform: capitalize;
+            color: $dark-gray;
+            padding: 0 12px;
+          }
+
+          &:not(:last-of-type) {
+            margin-right: 5px;
+          }
+
+          .basketBtn {
+            background-color: $red;
+            position: relative;
+
+            .basketLength {
+              position: absolute;
+              background-color: $white;
+              top: 15px;
+              right: 5px;
+              width: 18px;
+              height: 18px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              border-radius: 100%;
+              color: $dark-gray;
+              font-size: 13px;
+            }
+          }
+
+          &:hover {
+            & > .subMenu {
+              transition: all 0.1s;
+              visibility: visible;
+              opacity: 1;
+              margin-top: 0;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .header {
+    padding: 1rem;
+    .navContainer {
+      display: none;
+    }
+  }
+}

+ 23 - 0
react-store-app/src/styles/Home.module.scss

@@ -0,0 +1,23 @@
+@import "styles/_variables.scss";
+
+.home {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+
+  .container {
+    width: 100%;
+    max-width: 1920px;
+    margin-top: 1rem;
+    padding: 0.5rem;
+
+    .row {
+      display: flex;
+      flex-wrap: wrap;
+
+      .title {
+        padding: 0.5rem;
+      }
+    }
+  }
+}

+ 86 - 0
react-store-app/src/styles/MobileBasket.module.scss

@@ -0,0 +1,86 @@
+@import "styles/_variables.scss";
+
+.mobileBasket {
+  height: calc(100% - 50px);
+  overflow-y: scroll;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+
+  .basketTotal {
+    width: 100%;
+    margin-top: auto;
+    height: 150px;
+    padding: 1rem;
+    border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.15);
+
+    .total {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+
+    .totalPrice {
+      margin-top: 20px;
+      display: flex;
+      align-items: baseline;
+      justify-content: space-between;
+
+      > small {
+        text-transform: uppercase;
+        font-weight: 700;
+      }
+
+      .price {
+        display: flex;
+        align-items: center;
+        font-weight: 700;
+        font-size: 1.25rem;
+      }
+    }
+
+    .confirmBtn {
+      width: 100%;
+      margin-top: 10px;
+      background-color: #019267;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: $white;
+      font-family: inherit;
+      font-size: 1.15rem;
+      padding: 0 10px;
+      height: 37px;
+      font-weight: 500;
+      border-radius: $border-radius;
+      cursor: pointer;
+    }
+  }
+
+  .emptyBasket {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 0 1rem;
+
+    > img {
+      width: 250px;
+      height: 250px;
+      object-fit: contain;
+      object-position: center;
+    }
+
+    > button {
+      width: 100%;
+      height: 37px;
+      border-radius: $border-radius;
+      background-color: $red;
+      color: $white;
+      font-family: inherit;
+      font-weight: 500;
+      font-size: 1rem;
+      cursor: pointer;
+    }
+  }
+}

+ 53 - 0
react-store-app/src/styles/MobileBottomNav.module.scss

@@ -0,0 +1,53 @@
+@import "styles/_variables.scss";
+
+.bottomNav {
+  background-color: $silver;
+  position: fixed;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  height: 50px;
+  z-index: 1;
+
+  .content {
+    overflow: hidden;
+    height: 100%;
+    overflow-y: auto;
+  }
+
+  .navContainer {
+    padding: 1rem;
+    border-top: 1px solid rgba($color: $dark-gray, $alpha: 0.1);
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    height: 50px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    background-color: inherit;
+    gap: 10px;
+
+    .navItem {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      text-transform: uppercase;
+      flex: 1;
+      color: $black;
+      background-color: rgba($color: $black, $alpha: 0.09);
+      padding: 7px;
+      border-radius: $border-radius;
+      cursor: pointer;
+    }
+  }
+}
+
+.fullHeight {
+  height: 100%;
+}
+
+.removeHeight {
+  height: unset;
+  height: 50px;
+}

+ 14 - 0
react-store-app/src/styles/MobileCategories.module.scss

@@ -0,0 +1,14 @@
+@import "styles/_variables.scss";
+
+.mobileCategories {
+  padding: 1rem;
+
+  .mobileCategoriesMenu {
+    display: flex;
+    flex-direction: column;
+
+    > li {
+      width: 100%;
+    }
+  }
+}

+ 35 - 0
react-store-app/src/styles/Quantity.module.scss

@@ -0,0 +1,35 @@
+@import "styles/_variables.scss";
+
+.quantity {
+  display: inline-flex;
+  align-items: center;
+  border: 1px solid rgba($color: $black, $alpha: 0.25);
+  border-radius: 20px;
+  overflow: hidden;
+
+  .quantityBtn {
+    background-color: transparent;
+    width: 25px;
+    height: 25px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    color: $dark-gray;
+  }
+
+  & > input {
+    text-align: center;
+    outline: none;
+  }
+
+  & > input::-webkit-outer-spin-button,
+  & > input::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+  }
+
+  & > input[type="number"] {
+    -moz-appearance: textfield;
+  }
+}

+ 9 - 0
react-store-app/src/styles/_variables.scss

@@ -0,0 +1,9 @@
+/* COLORS */
+$white: #ffffff;
+$silver: #fafafa;
+$red: #da0037;
+$dark-gray: #444444;
+$black: #171717;
+
+/* UNITS */
+$border-radius: 4px;

+ 34 - 0
react-store-app/src/styles/index.css

@@ -0,0 +1,34 @@
+@import url("https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600&display=swap");
+
+*,
+*::before,
+*::after {
+  padding: 0;
+  margin: 0;
+  box-sizing: border-box;
+  border: none;
+}
+
+html {
+  font-family: "Quicksand", sans-serif;
+  font-size: 17px;
+  min-height: 100%;
+}
+
+body {
+  width: 100%;
+  height: calc(100%);
+  font-family: inherit;
+  font-size: inherit;
+  line-height: 1.15;
+}
+
+ul {
+  list-style-type: none;
+  list-style: none;
+}
+
+a {
+  text-decoration: none;
+  color: inherit;
+}