Alex.ElRubio 1 year ago
commit
d88635eb12
75 changed files with 33094 additions and 0 deletions
  1. 31 0
      Js-HW1/Js-HW1.js
  2. 89 0
      Js-HW10/Js-HW10.js
  3. 111 0
      Js-HW11/Js-HW11.js
  4. 69 0
      Js-HW12/Js-HW12.js
  5. 88 0
      Js-HW13/Js-HW13.js
  6. 92 0
      Js-HW14/Js-HW14.js
  7. 78 0
      Js-HW15/Js-HW15.js
  8. 147 0
      Js-HW2/Js-HW2.js
  9. 249 0
      Js-HW3/Js-HW3.js
  10. 194 0
      Js-HW4/Js-HW4.js
  11. 121 0
      Js-HW5/Js-HW5.js
  12. 84 0
      Js-HW6/Js-HW6.js
  13. 52 0
      Js-HW7/Js-HW7.js
  14. 116 0
      Js-HW8/Js-HW8.js
  15. 62 0
      Js-HW9/Js-HW9.js
  16. 1 0
      Readme.md
  17. 150 0
      react-store-app/.gitignore
  18. 72 0
      react-store-app/README.md
  19. 7 0
      react-store-app/jsconfig.json
  20. 719 0
      react-store-app/old-code.js
  21. 27873 0
      react-store-app/package-lock.json
  22. 48 0
      react-store-app/package.json
  23. BIN
      react-store-app/public/favicon.ico
  24. 43 0
      react-store-app/public/index.html
  25. BIN
      react-store-app/public/logo192.png
  26. BIN
      react-store-app/public/logo512.png
  27. 25 0
      react-store-app/public/manifest.json
  28. 3 0
      react-store-app/public/robots.txt
  29. 46 0
      react-store-app/src/App.js
  30. 8 0
      react-store-app/src/App.test.js
  31. 31 0
      react-store-app/src/apollo/client.js
  32. 50 0
      react-store-app/src/apollo/queries.js
  33. 23 0
      react-store-app/src/components/AddToBasketBtn.js
  34. 37 0
      react-store-app/src/components/BasketItem.js
  35. 64 0
      react-store-app/src/components/BasketSidebar.js
  36. 37 0
      react-store-app/src/components/Card.js
  37. 17 0
      react-store-app/src/components/CategoryItem.js
  38. 15 0
      react-store-app/src/components/Footer.js
  39. 9 0
      react-store-app/src/components/GetIcon.js
  40. 52 0
      react-store-app/src/components/Header.js
  41. 17 0
      react-store-app/src/components/MobileCategories.js
  42. 27 0
      react-store-app/src/components/Quantity.js
  43. 5 0
      react-store-app/src/components/Title.js
  44. 3 0
      react-store-app/src/config.js
  45. 26 0
      react-store-app/src/hooks/useMakeRequest.js
  46. 29 0
      react-store-app/src/hooks/useMobileDetect.js
  47. 1 0
      react-store-app/src/images/empty_cart.svg
  48. BIN
      react-store-app/src/images/shopBG.jpg
  49. 21 0
      react-store-app/src/index.js
  50. 1 0
      react-store-app/src/logo.svg
  51. 132 0
      react-store-app/src/old-code/full-code-gql.html
  52. 719 0
      react-store-app/src/old-code/full-code-gql.js
  53. 51 0
      react-store-app/src/pages/Category.js
  54. 52 0
      react-store-app/src/pages/Detail.js
  55. 47 0
      react-store-app/src/pages/Home.js
  56. 13 0
      react-store-app/src/reportWebVitals.js
  57. 5 0
      react-store-app/src/setupTests.js
  58. 61 0
      react-store-app/src/stores/cartStore.js
  59. 33 0
      react-store-app/src/styles/AddToBasketBtn.module.scss
  60. 15 0
      react-store-app/src/styles/App.module.scss
  61. 68 0
      react-store-app/src/styles/BasketItem.module.scss
  62. 134 0
      react-store-app/src/styles/BasketSidebar.module.scss
  63. 123 0
      react-store-app/src/styles/Card.module.scss
  64. 23 0
      react-store-app/src/styles/Category.module.scss
  65. 46 0
      react-store-app/src/styles/CategoryItem.module.scss
  66. 139 0
      react-store-app/src/styles/Detail.module.scss
  67. 18 0
      react-store-app/src/styles/Footer.module.scss
  68. 118 0
      react-store-app/src/styles/Header.module.scss
  69. 23 0
      react-store-app/src/styles/Home.module.scss
  70. 86 0
      react-store-app/src/styles/MobileBasket.module.scss
  71. 53 0
      react-store-app/src/styles/MobileBottomNav.module.scss
  72. 14 0
      react-store-app/src/styles/MobileCategories.module.scss
  73. 35 0
      react-store-app/src/styles/Quantity.module.scss
  74. 9 0
      react-store-app/src/styles/_variables.scss
  75. 34 0
      react-store-app/src/styles/index.css

+ 31 - 0
Js-HW1/Js-HW1.js

@@ -0,0 +1,31 @@
+let cigarettesPerDay = +prompt('How many cigarettes do you smoke per day?');
+let onePackPrice = +prompt('How much does one pack cost?')
+
+let cigarettesPerMonth = cigarettesPerDay * 30;
+let packsPerMonth = cigarettesPerMonth / 20;
+
+let totalPricePerMonth = packsPerMonth * onePackPrice;
+alert(`You spend ${totalPricePerMonth} for cigarettes every month. Aren\'t you surprised?`)
+
+var credentials = {
+    login: 'admin',
+    password: 'qwerty',
+};
+
+let login = document.querySelector('#login');
+let password = document.querySelector('#password');
+let loginBtn = document.querySelector('#login-btn');
+let message = document.createElement('div');
+message.id = 'message'
+
+loginBtn.addEventListener('click', (e) => {
+    e.preventDefault()
+    if(login.value === credentials.login && password.value === credentials.password) {
+        message.innerHTML = 'Successful login'
+        message.className = 'success-message'
+    } else {
+        message.innerHTML = 'Data is wrong!'
+        message.className = 'fail-message'
+    }
+    document.body.append(message)
+});

+ 89 - 0
Js-HW10/Js-HW10.js

@@ -0,0 +1,89 @@
+function Password(parent, open) {
+    let value = ''
+    let pass = document.createElement('input')
+    pass.placeholder = 'enter password'
+    let showPassBox = document.createElement('div')
+    let check = document.createElement('input')
+    check.type = 'checkbox'
+    check.name = 'password'
+    let label = document.createElement('label')
+    label.for = 'password'
+    label.innerText = 'Show password'
+    showPassBox.append(check, label)
+    parent.append(pass, showPassBox)
+    check.onchange = () => {
+        this.setOpen(check.checked)
+    }
+    pass.oninput = () => {
+        if (typeof this.onChange === "function") {
+            this.onChange(this.getValue());
+        }
+    };
+    this.getValue = function () {
+        return pass.value
+    }
+    this.setValue = function(value) {
+        pass.value = value
+    }
+    this.getOpen = function () {
+        return open
+    }
+    this.setOpen = function(newOpen) {
+        open = newOpen
+        pass.type = open ? 'text' : 'password'
+        check.checked = open
+        if(typeof this.onOpenChange === 'function') {
+            this.onOpenChange(open)
+        }
+    }
+    this.setOpen(open)
+    this.onChange = () => {
+        button.disabled = !(p.getValue() === p2.getValue())
+    }
+
+}
+
+
+
+// let p = new Password(document.body, false)
+//
+// p.onChange = data => console.log(data)
+// p.onOpenChange = open => console.log(open)
+//
+// p.setValue('qwerty')
+// console.log(p.getValue())
+
+// p.setOpen(false)
+// console.log(p.getOpen())
+
+function Login(parent) {
+    let input = document.createElement('input')
+    input.placeholder = 'enter login'
+    let button = document.createElement('button')
+    button.innerText = 'log in'
+    parent.appendChild(input)
+    let password = new Password(parent, false)
+    password.placeholder = 'enter password'
+    button.disabled = true
+    parent.appendChild(button)
+
+    parent.onchange = function () {
+        console.log(password.getValue())
+        button.disabled = !(input.value !== '' && password.getValue() !== '')
+    }
+}
+let newLogin = new Login(loginForm)
+console.log(newLogin)
+
+
+let p = new Password(document.body, true)
+let p2 = new Password(document.body, true)
+let button = document.createElement('button')
+button.innerText = 'log in'
+button.disabled = true
+
+
+
+
+
+document.body.append(button)

+ 111 - 0
Js-HW11/Js-HW11.js

@@ -0,0 +1,111 @@
+function createStore(reducer){
+    let state = reducer(undefined, {})
+    let callbacks = []
+    const getState = () => state
+    const dispatch = (action) => {
+        const newState = reducer(state, action)
+        if(newState !== state) {
+            state = newState
+        }
+        for(let cb of callbacks) {
+            cb()
+        }
+    }
+
+    const subscribe = (callback) => (callbacks.push(callback),
+        () => callbacks = callbacks.filter(c => c !== callback))
+    return {
+        getState,
+        dispatch,
+        subscribe
+    }
+}
+
+function reducer(state, {type, item, number, price}){ //объект action деструктуризируется на три переменных
+    if (!state){ //начальная уборка в ларьке:
+        return {
+            items :
+                [
+                     {
+                        name: 'beer',
+                        price: 35,
+                        number: 100
+                     },
+                    {
+                        name: 'chips',
+                        price: 50,
+                        number: 100
+                    },
+                    {
+                        name: 'cigarettes',
+                        price: 45,
+                        number: 100
+                    },
+                    {
+                        name: 'gum',
+                        price: 15,
+                        number: 100
+                    }
+                ],
+            money: 0
+        }
+    }
+    if (type === 'BUY'){ //если тип action - КУПИТЬ, то:
+        console.log(state.items)
+        return {
+            ...state, //берем все что было из ассортимента
+
+            items: state.items.filter(i => i.name === item), //и уменьшаем то, что покупается на количество
+            money: state.money += state.items.filter(i => i.price === price)
+        }
+    }
+    return state //если мы не поняли, что от нас просят в `action` - оставляем все как есть
+}
+
+const store = createStore(reducer)
+// console.log(store.getState())
+
+let $select = document.createElement('select')
+let $table = document.createElement('table');
+let $num = document.createElement('input')
+$num.type = 'number'
+let btn = document.createElement('button')
+$num.min = '1';
+console.log(store.getState().items)
+let money = document.createElement('div')
+for(let item of store.getState().items) {
+    let $tr = document.createElement('tr')
+    let $td = document.createElement('td')
+    let $td2 = document.createElement('td')
+    let $td3 = document.createElement('td')
+    $td.innerText = item.name
+    $td2.innerText = item.price
+    $td3.innerText = item.number
+    let $option = document.createElement('option')
+    $option.innerHTML = item.name
+    $select.append($option)
+    btn.innerHTML = 'buy'
+    $tr.append($td, $td2, $td3)
+    $table.append($tr)
+    btn.onclick = (e) => {
+        // console.log(store.getState().items, item)
+        // console.log(item.number - +$num.value >= 0 )
+            if(item.number - +$num.value >= 0 ) {
+                // console.log(+$num.value, item.number)
+                let [selected] = store.getState().items.filter(i => i.name === $select.value)
+                selected.number = item.number - +$num.value
+                // console.log(selected.number)
+                if(item.number > 0) {
+                    console.log(store.getState())
+                    store.dispatch({type: 'BUY', item: selected.name, number: item.number - +$num.value})
+                    console.log(store.getState())
+                }
+            }
+
+    }
+    console.log(store.getState())
+    store.subscribe(() => money.innerHTML = `Cashbox: ${+store.getState().money}`)
+    store.subscribe(() => $td3.innerHTML = `${item.number}`)
+    document.body.append($select, $num, btn, $table)
+}
+document.body.appendChild(money)

+ 69 - 0
Js-HW12/Js-HW12.js

@@ -0,0 +1,69 @@
+fetch('https://swapi.dev/api/people/1/')
+    .then(res => res.json())
+    .then(luke => renderTable(document.body, luke))
+
+function renderTable(parent, obj) {
+    let $table = document.createElement('table')
+    for(let key in obj) {
+        let $tr = document.createElement('tr')
+        let $td = document.createElement('td')
+        let $td2 = document.createElement('td')
+        if(Array.isArray(obj[key])) {
+            for(let item of obj[key]) {
+                let newTable = document.createElement('table')
+                let newTr = document.createElement('tr')
+                let newTd = document.createElement('td')
+                linkChecker(item, newTd)
+                newTr.append(newTd)
+                newTable.append(newTr)
+                $td2.append(newTable)
+            }
+        } else {
+            linkChecker(obj[key], $td2)
+        }
+
+        $td.innerHTML = key
+
+        $tr.append($td, $td2)
+        $table.appendChild($tr)
+    }
+    parent.appendChild($table)
+}
+function linkChecker(str, container) {
+    if(typeof str === 'string') {
+        if(str.includes('https://swapi.dev/api/')) {
+            let btn = document.createElement('button')
+            btn.innerHTML = 'Show'
+            btn.onclick = () => {
+                fetch(str).then(r => r.json())
+                    .then(r => renderTable(container, r))
+                    // .then(r => console.log(r))
+            }
+            container.append(btn)
+        } else {
+            container.innerHTML = str
+        }
+    }
+}
+
+function myFetch(url){
+    return new Promise(function (resolve, reject){
+        const xhr = new XMLHttpRequest()
+        xhr.open('get', url)
+        xhr.onload = function () {
+            if (xhr.status >= 200 && xhr.status < 300) {
+                resolve(xhr.response);
+            } else {
+                reject(() => alert('something went wrong'));
+            }
+        xhr.send();
+    }});
+}
+myFetch('https://swapi.dev/api/peope/4/')
+    .then(res => console.log(res))
+
+function delay(ms) {
+    return setTimeout(() => console.log('delay worked'), ms)
+}
+Promise.race([myFetch('https://swapi.dev/api/peope/4/'), delay(300)]).then(val => console.log(val))
+Promise.race([myFetch('https://swapi.dev/api/peope/1/'), delay(200)]).then(val => console.log(val))

+ 88 - 0
Js-HW13/Js-HW13.js

@@ -0,0 +1,88 @@
+let $nickname = document.querySelector('#nick')
+let sendBtn = document.querySelector('#sendButton');
+let $msg = document.querySelector('#msg')
+let msgHolder = document.querySelector('#msgHolder')
+
+function jsonPost(url, data) {
+    return new Promise((resolve, reject) => {
+        var x = new XMLHttpRequest();
+        x.onerror = () => reject(new Error('jsonPost failed'))
+        x.open("POST", url, true);
+        x.send(JSON.stringify(data))
+
+        x.onreadystatechange = () => {
+            if (x.readyState === XMLHttpRequest.DONE && x.status === 200){
+                resolve(JSON.parse(x.responseText))
+            }
+            else if (x.status !== 200){
+                reject(new Error('status is not 200'))
+            }
+        }
+    })
+}
+
+// function messageDataCreator(message) {
+//     return {
+//         func: 'addMessage',
+//         nick: $nickname.value,
+//         message: message,
+//     }
+// }
+//
+function showMessagesList(arr) {
+    let $container = document.createElement('div')
+    for(let msg of arr) {
+        let $div = document.createElement('div')
+        $div.className = 'msg'
+        let $p1 = document.createElement('p')
+        let $p2 = document.createElement('p')
+        $p1.innerHTML = msg.nick + ':'
+        $p2.innerHTML = msg.message
+        $div.append($p1, $p2)
+        $container.append($div)
+    }
+    msgHolder.prepend($container)
+}
+
+    async function getMessages() {
+        let id = 0;
+        let resp = await jsonPost("http://students.a-level.com.ua:10012", {func: "getMessages", messageId: id})
+        let {data, nextMessageId} = await resp
+        id = nextMessageId
+        let list = data.slice(data.length - 50).reverse()
+        showMessagesList(list)
+        if(nextMessageId > id) {
+            id = nextMessageId
+            showMessagesList(data)
+        }
+    }
+
+    // let interval = setInterval(async () => {
+    //
+    // }, 3000)
+
+
+async function sendMessage(nick, message) {
+    console.log('test')
+        await jsonPost("http://students.a-level.com.ua:10012",
+            {
+                func: 'addMessage',
+                nick: nick,
+                message: message,
+            }
+        )
+    }
+
+sendBtn.onclick = sendAndCheck()
+    async function sendAndCheck() {
+        await getMessages()
+        await sendMessage($nickname.value, $msg.value)
+        $msg.value = ''
+    }
+
+function delay(ms) {
+    return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+
+

+ 92 - 0
Js-HW14/Js-HW14.js

@@ -0,0 +1,92 @@
+const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
+
+const red = document.querySelector('.red')
+const yellow = document.querySelector('.yellow')
+const green = document.querySelector('.green')
+let col = red
+
+// async function trafficLight(){
+//     while (true){
+//         changeLight(green)
+//         await delay(3000)
+//         changeLight(yellow)
+//         await delay(2000)
+//         changeLight(red)
+//         await delay(3000)
+//     }
+// }
+// trafficLight().then()
+
+function changeLight(color) {
+    if(col !== color) {
+        col.style.opacity = .2
+        color.style.opacity = 1
+        col = color
+    } else {
+        color.style.opacity = 1
+    }
+}
+
+let btn = document.querySelector('.btn')
+
+function domEventPromise(button, eventName) {
+    return new Promise(res => {
+        button.addEventListener(eventName, (e) => res(e))
+
+        button.removeEventListener(eventName, (e) => res(e))
+    });
+}
+domEventPromise(btn, 'click')
+    .then(e => console.log('event click happens', e))
+
+let stopLightIsActive = true
+async function pedestrianTrafficLight() {
+    while(stopLightIsActive){
+            changeLight(red)
+            await delay(100)
+    }
+}
+pedestrianTrafficLight().then()
+btn.addEventListener('click', async () => {
+    stopLightIsActive = !stopLightIsActive
+        await delay(3000)
+        changeLight(yellow)
+        await delay(1000)
+        changeLight(green)
+        await delay(4000)
+        changeLight(yellow)
+        await delay(1000)
+        changeLight(red)
+})
+
+async function speedtest(getPromise, count, parallel = 1) {
+    let time = performance.now();
+    let promisesArr = [];
+    let initParallel = parallel;
+    for (let i = 0; i < count; i++) {
+        promisesArr.push(getPromise);
+        parallel--;
+        await Promise.all(promisesArr);
+    }
+
+    time = performance.now() - time;
+
+    return {
+        duration: time,
+        querySpeed: count / time,
+        queryDuration: time / count,
+        parallelSpeed: (count / time) * initParallel,
+        parallelDuration: time / (count * initParallel),
+    };
+}
+
+speedtest(() => delay(1000), 10, 10).then((res) =>console.log(res));
+speedtest(
+    () =>
+        fetch("http://swapi.dev/api/people/1").then((res) =>
+            res.json()
+        ),
+    2,
+    2
+).then((v)=>console.log(v))
+

+ 78 - 0
Js-HW15/Js-HW15.js

@@ -0,0 +1,78 @@
+let gql = (url, query, variables={}) =>
+    fetch(url, {
+        method: 'POST',
+        headers:{
+            Accept: 'application/json',
+            "Content-Type": 'application/json'
+        },
+        body: JSON.stringify({query, variables})
+    }).then(res => res.json())
+
+// используя функцию gql, понаделать функций с параметрами для разных запросов: категории, категория по id с товарами, товар по id с картинками и описанием, логин, регистрация.
+//     например:
+let idd = 123456
+// `\\"_id\\":\\"${id}\\"`
+console.log(JSON.parse("[{\"_id\":\"123456\"}]"))
+const url = 'http://shop-roles.node.ed.asmer.org.ua/graphql'
+const gqlCategoriesList = () => {
+    return gql(url, `query cats{
+    CategoryFind(query: "[{}]"){
+    _id, name,
+  }
+}`, {}
+    )
+}
+
+const gqlRegister = (login, password) => {
+    return gql(url, `mutation register($login: String, $password: String){
+    UserUpsert(user: {login: $login, password: $password}){
+        _id, login, createdAt
+    }
+}`, {login, password})
+}
+
+const gqlLogin = (login, password) =>
+    gql(url,
+        `query login($login:String, $password: String){
+                login(login:$login, password: $password)
+        }`, {login, password})
+
+const gqlCategories = () => {
+    return gql(url,
+        ` query categories{
+                            CategoryFind(query:"[{}]"){
+                            _id name goods {
+                                _id
+                                name
+                                price
+                                description
+                                }
+                          }
+                     }`
+        );
+}
+const productSearchById = (id) => {
+    let jsn = JSON.stringify({_id: id})
+    return gql(
+        url,
+        `query searchById{
+        GoodFind(query: ${JSON.stringify(`[${jsn}]`)}){
+            name description images {
+                _id
+                createdAt
+                text
+                url
+                originalFileName
+            }
+        }
+    }`,
+    );
+};
+
+(async function callAll() {
+    console.log(await gqlCategoriesList());
+    console.log(await gqlRegister("Vlada Simonova", "123456"));
+    console.log(await gqlLogin("Vlada Simonova", "123456"));
+    console.log(await gqlCategories());
+    console.log(await productSearchById("62c9472cb74e1f5f2ec1a0d3"));
+})();

+ 147 - 0
Js-HW2/Js-HW2.js

@@ -0,0 +1,147 @@
+var a = 5;
+var b, c;
+
+b = (a * 5);
+b = (c = b/2);
+
+b = a *= 5;
+b = c = b / 2;
+
+console.log(b)
+
+for(let i = 0 i < 10 i++) {
+    console.log(i)
+}
+
+const num1 = 110 num2 = num1 + 54
+
+const obj = {}
+(function () { })()
+let x = (5; + 10)
+console.log(x)
+
+let age1 = prompt('how old are you?');
+alert(`you were born in ${2022 - age1}`)
+// (0 °C × 9/5) + 32 = 32 °F
+let celsius = prompt('Enter the temperature in ℃')
+alert(`${celsius * 1.8 + 32} fahrenheit`);
+
+let num1 = prompt('enter the number to divide')
+let num2 = prompt('enter another number')
+alert(Math.floor(num1 / num2));
+
+let number = +prompt('enter the number');
+
+if(!isNaN(number)) {
+    console.log('it\'s a number')
+    if(number % 2 === 0) {
+        console.log('the number is even')
+    } else {
+        console.log('the number is odd')
+    }
+} else {
+    console.log('It\'s not a number!')
+}
+
+let name = prompt('What\'s your name?')
+alert(`Hello, ${name}!`)
+
+let str = prompt('Enter message')
+
+if(str.includes('shit')) {
+    console.log('Hey, it\'s inappropriate')
+}
+
+let isSpeakingSpanish = confirm('Do you speak Spanish?')
+isSpeakingSpanish ? console.log('Hola, amigo!') : console.log('So let\'s learn it!')
+
+let isMan = confirm('Are you a man?')
+let isMarried = confirm('Are you married?')
+let hasChildren = confirm('Do you have children?')
+
+
+let question = confirm('are you a woman?');
+let isWoman = question;
+console.log(isWoman);
+if(isWoman) {
+ alert('you`re a woman')
+ } else {
+ alert('you`re a man');
+}
+
+let shoppingCart = ['avocado', 'milk', 'lemon', 'cheese', 'tea'];
+const answers = [isMan, isMarried, hasChildren, isWoman]
+
+let numbers = [1, 2, 5, 4, 5];
+
+numbers.splice(2, 1,numbers[0] + numbers[1])
+
+console.log(numbers)
+
+let array = ['he', 'l', 'lo']
+array.join('');
+
+let dog = {
+    gender: 'male',
+    breed: 'chihuahua',
+    name: 'Ronnie',
+    age: 6,
+    ['favorite food']: ['cheese', 'meat', 'tomatoes'],
+    hasChildren: false
+}
+
+dog["favorite food"].push('egg yolk')
+
+let age = +prompt("Сколько вам лет?","");
+
+if(age < 0) {
+    alert('возраст не может быть отрицательным')
+} else if (age < 18) {
+    alert("школьник");
+} else if (age > 18 && age < 30) {
+    alert("молодежь");
+} else if (age > 30 && age < 45) {
+    alert("зрелость");
+} else if (age > 45 && age < 60){
+    alert("закат");
+} else if (age > 60){
+    alert("как пенсия?");
+} else {
+    alert("то ли киборг, то ли KERNESS");
+}
+let size = +prompt('введите размер обуви дял конвертации в UK')
+switch (size) {
+    case 35:
+        alert(3.5);
+        break;
+    case 36:
+        alert(4);
+        break;
+    case 37:
+        alert(5);
+        break;
+    case 38:
+        alert(6);
+        break;
+    case 39:
+        alert(6.5);
+        break;
+    case 40:
+        alert(7);
+        break;
+    default:
+        alert('something went wrong')
+}
+isWoman ? alert('you`re a woman') : alert('you`re a man');
+
+
+let flatNumber = +prompt('Введите номер квартиры')
+let flatsPerFloor = +prompt('Сколько квартир на этаже?')
+let porchesPerHouse = +prompt('Сколько подъездов в доме?')
+let floorsPerHouse = +prompt('Сколько этажей в доме?')
+
+let flatsPerEntrance = flatsPerFloor * floorsPerHouse;
+let entrance = Math.floor((flatNumber - 1) / flatsPerEntrance + 1)
+let floor = Math.floor((flatNumber - 1) % flatsPerEntrance / flatsPerFloor + 1)
+
+alert(`Flat No.${flatNumber} is on floor ${floor}, entrance ${entrance}`)

+ 249 - 0
Js-HW3/Js-HW3.js

@@ -0,0 +1,249 @@
+let task = prompt('enter task name').toLowerCase();
+
+switch (task) {
+    case 'switch: sizes':
+        let size = +prompt('введите размер обуви дял конвертации в UK')
+        switch (size) {
+            case 35:
+                alert(3.5);
+                break;
+            case 36:
+                alert(4);
+                break;
+            case 37:
+                alert(5);
+                break;
+            case 38:
+                alert(6);
+                break;
+            case 39:
+                alert(6.5);
+                break;
+            case 40:
+                alert(7);
+                break;
+            default:
+                alert('something went wrong')
+        }
+        break;
+
+    case 'switch: if':
+        let color = prompt("Введите цвет","");
+        if(color === "red") {
+            document.write("<div style='background-color: red;'>красный</div>")
+        } else if(color === "black") {
+            document.write("<div style='background-color: black; color: white;'>черный</div>")
+        } else if(color === "blue") {
+            document.write("<div style='background-color: blue;'>синий</div>")
+        } else if(color === "green") {
+            document.write("<div style='background-color: green;'>зеленый</div>")
+        } else {
+            document.write("<div style='background-color: gray;'>Я не понял</div>")
+        }
+        break;
+
+    case 'prompt: or':
+        let age1 = prompt('how old are you?');
+        if(age1 === '' || age1 === null) {
+            alert('something is wrong...')
+        } else {
+            alert(`you were born in ${2022 - age1}`)
+        }
+        break;
+
+    case 'confirm: or this days':
+        let question1 = confirm('Шоппинг?') || alert(' Ты - бяка!');
+        break;
+
+    case 'confirm: if this days':
+        let question2 = confirm('Шоппинг?');
+        if(!question2) {
+            alert(' Ты - бяка!')
+        }
+        break;
+
+    case 'triple prompt':
+        let surname = prompt('Enter surname')
+        let name = prompt('Enter name')
+        let patronymic = prompt('Enter patronymic')
+
+        alert(`${surname} ${name} ${patronymic}`)
+        break;
+
+    case 'default: or':
+        let enteredName = prompt('enter your name');
+        let enteredSurname = prompt('enter your surname');
+
+        enteredName = enteredName || 'Ivan';
+        enteredSurname = enteredSurname || 'Ivanov';
+        break;
+
+    case 'default: if':
+        let enteredName2 = prompt('enter your name');
+        let enteredSurname2 = prompt('enter your surname');
+        if(!enteredSurname2) {
+            enteredSurname2 = 'Ivanov'
+        }
+        if(!enteredName2) {
+            enteredName2 = 'Ivan'
+        }
+        break;
+
+    case 'login and password':
+        const adminName = 'admin';
+        const adminPassword = 'qwerty';
+        let login = prompt('Login: ');
+
+        if(adminName === login) {
+            let password = prompt('Password: ');
+            if(adminPassword === password) {
+                alert('Success!')
+            } else {
+                alert('password is incorrect')
+            }
+        } else {
+            alert('login is incorrect')
+        }
+        break;
+
+    case 'currency calc':
+        let currency1 = prompt('USD or EUR?')
+        let rate1;
+        switch (currency1) {
+            case 'usd':
+                rate1 = 34.67
+                break;
+            case 'eur':
+                rate1 = 37.38
+                break;
+            default:
+                rate1 = 0;
+        }
+        let uah = +prompt('enter UAH to convert');
+        alert(`${(uah / rate1).toFixed(2)} ${currency1}`)
+        break;
+
+    case 'currency calc: improved':
+        let currency2 = prompt('USD or EUR?').toLowerCase();
+        let rate2;
+        switch (currency2) {
+            case 'usd':
+                rate2 = 34.67
+                break;
+            case 'eur':
+                rate2 = 37.38
+                break;
+            default:
+                rate2 = 0;
+        }
+        let uah1 = +prompt('enter UAH to convert');
+        alert(`${(uah1 / rate2).toFixed(2)} ${currency2}`)
+        break;
+
+    case 'currency calc: two rates':
+        let currency3 = prompt('Do you want to use USD or EUR?').toLowerCase()
+        let rate3;
+        let buyRate1 = confirm('Do you want to buy currency?')
+        switch (currency3) {
+            case 'usd':
+                buyRate1 ? rate3 = 36.28 : rate3 = 34.67
+                break;
+            case 'eur':
+                buyRate1 ? rate3 = 41.77 : rate3 = 37.38
+                break;
+            default:
+                alert('error');
+        }
+        let uah2 = +prompt('enter UAH to convert');
+        alert(`${(uah2 / rate3).toFixed(2)} ${currency3}`)
+        break;
+
+    case 'currency calc: if':
+        let currency4 = prompt('Do you want to use USD or EUR?').toLowerCase()
+        if(currency4 === 'usd' || currency4 === 'eur') {
+            let uah4 = +prompt('enter UAH to convert');
+            let rate4;
+            let buyRate2 = confirm('Do you want to buy currency?')
+            if (currency4 === 'usd') {
+                buyRate2 ? rate4 = 36.28 : rate4 = 34.67
+            } else if (currency4 === 'eur') {
+                buyRate2 ? rate4 = 41.77 : rate4 = 37.38
+            } else {
+                alert('error')
+            }
+            alert(`${(uah4 / rate4).toFixed(2)} ${currency4}`)
+        }
+        break;
+
+    case 'scissors':
+
+        let usersTurn = prompt('Rock, paper or scissors?').toLowerCase();
+        let turns = ['rock', 'paper', 'scissors'];
+        const randomIndex  = Math.floor(Math.random() * 3)
+        let computersTurn = turns[randomIndex]
+        console.log(computersTurn)
+
+        if(usersTurn === computersTurn) {
+            alert('It\'s a tie!')
+        } else if(usersTurn === 'paper' && computersTurn === 'scissors') {
+            alert('computer won');
+        } else if(usersTurn === 'scissors' && computersTurn === 'rock') {
+            alert('computer won');
+        } else if(usersTurn === 'rock' && computersTurn === 'paper') {
+            alert('computer won')
+        } else if(computersTurn === 'paper' && usersTurn === 'scissors') {
+            alert('you won');
+        } else if(computersTurn === 'scissors' && usersTurn === 'rock') {
+            alert('you won');
+        } else if(computersTurn === 'rock' && usersTurn === 'paper') {
+            alert('you won')
+        } else {
+            alert('error')
+        }
+        break;
+
+    case 'задание на синий пояс':
+        let ratios = {
+            usdToBuy: 36.28,
+            eurToBuy: 41.77,
+            usdToSell: 34.67,
+            eurToSell: 37.38
+        }
+        let currency5 = prompt('Do you want to use USD or EUR?').toLowerCase()
+        if(currency5 === 'usd' || currency5 === 'eur') {
+            let rate;
+            let uah = +prompt('enter UAH to convert');
+            let buyRate = confirm('Do you want to buy currency?')
+            if (currency5 === 'usd') {
+                rate = buyRate ? ratios["usdToBuy"] : ratios["usdToSell"]
+            } else if (currency5 === 'eur') {
+                rate = buyRate ? ratios["eurToBuy"] : ratios["eurToSell"]
+            } else {
+                alert('error')
+            }
+            alert(`${(uah / rate).toFixed(2)} ${currency5}`)
+        }
+        break;
+
+    case 'real data':
+        let currency = prompt('Enter the currency name to convert (e.g. USD)').toUpperCase()
+            let uah3 = +prompt('enter UAH to convert');
+            fetch('https://open.er-api.com/v6/latest/' + currency)
+                .then(res => res.json())
+                .then(data => {
+                    alert(`${(uah3 / data.rates.UAH).toFixed(2)} ${currency}`)
+                })
+        break;
+
+    case 'задание на черный пояс':
+        let usersTurn1 = prompt('Rock, paper or scissors?').toLowerCase();
+        let turns1 = ['rock', 'paper', 'scissors'];
+        const randomIndex1  = Math.floor(Math.random() * 3)
+        let computersTurn1 = turns1[randomIndex1]
+        console.log(computersTurn1)
+        let compWin = usersTurn1 === 'paper' && computersTurn1 === 'scissors' || usersTurn1 === 'scissors' && computersTurn1 === 'rock' || usersTurn1 === 'rock' && computersTurn1 === 'paper';
+        let userWin = computersTurn1 === 'paper' && usersTurn1 === 'scissors' || computersTurn1 === 'scissors' && usersTurn1 === 'rock' || computersTurn1 === 'rock' && usersTurn1 === 'paper';
+        let tie = usersTurn1 === computersTurn1 && alert('It\'s a tie!')
+        let victory = compWin && alert('computer won') || userWin && alert('you won');
+        break;
+};

+ 194 - 0
Js-HW4/Js-HW4.js

@@ -0,0 +1,194 @@
+let body = {
+    tagName: 'body',
+    children: [
+        {
+            tagName: 'div',
+            children: [
+                {
+                    tagName: 'span',
+                    innerText: 'Enter a data please:'
+                },
+                {
+                    tagName: 'br'
+                },
+                {
+                    tagName: 'input',
+                    type: 'text',
+                    id: 'name'
+                },
+                {
+                    tagName: 'input',
+                    type: 'text',
+                    id: 'surname'
+                }
+            ]
+        },
+        {
+            tagName: 'div',
+            children: [
+                {
+                    tagName: 'button',
+                    innerText: 'OK',
+                    id: 'ok'
+                },
+                {
+                    tagName: 'button',
+                    innerText: 'Cancel',
+                    id: 'cancel'
+                }
+            ]
+        }
+    ]
+};
+
+console.log(body.children[1].children[1].innerText)
+console.log(body.children[0].children[3].id)
+
+
+var person;
+var notebook = {
+    brand: prompt('Brand:'),
+    type:  prompt('Type:'),
+    model: prompt('Model:'),
+    ram: +prompt('Ram:'),
+    size: +prompt('Size:'),
+    weight: +prompt('Weight:'),
+    resolution: {
+        width: +prompt('Resolution width:'),
+        height: +prompt('Resolution height:'),
+    },
+    owner: person
+};
+
+var phone = {
+    brand: prompt('Brand:'),
+    model: prompt('Model:'),
+    ram: +prompt('Ram:'),
+    color: prompt('Color:'),
+    owner: person
+};
+
+person = {
+    name: prompt('Name:'),
+    surname: prompt('Surname:'),
+    married: confirm('Married?'),
+    smartphone: phone,
+    laptop: notebook
+};
+
+let array1 = []
+array1[0] = prompt('string 1');
+array1[1] = prompt('string 2');
+array1[2] = prompt('string 3');
+
+let confirm1;
+while(!confirm1) {
+    confirm1 = confirm('Are you hungry?');
+    if(confirm1) {
+        break;
+    }
+}
+
+let prompt1;
+let array2 = []
+while(prompt1 !== null) {
+    prompt1 = prompt('Enter something');
+    array2.push(prompt1)
+    if(!prompt1) {
+        break
+    }
+    console.log(array2)
+}
+
+
+let prompt2 = prompt('Enter something');
+let array3 = []
+do {
+    let i = 0;
+    prompt2 ? array3[i++] = prompt2 : null
+    console.log(array3)
+} while(prompt2 !== null)
+
+let i = 0;
+let n;
+while(n !== 5) {
+    i++
+    let n = Math.random() * 5;
+    console.log(n)
+    console.log(i)
+    if(n === 5) {
+        break
+    }
+}
+let end = +prompt('Введите крайнее число прогрессии')
+let sum = 0;
+for(let i = 1; i < 30; i+=3){
+    console.log(i)
+    sum += i
+}
+alert(`Сумма чисел в прогрессии - ${sum}`);
+
+let chessString = '';
+
+for(let i = 0; i < 10; i++) {
+    i % 2 === 0 ? chessString += ' ' : chessString += '#'
+}
+console.log(chessString);
+
+let numbers2 = '';
+
+for(let i = 0; i < 10; i++) {
+    numbers2 += '\n'
+    for(let j = 0; j < 10; j++) {
+        numbers2 += j
+    }
+}
+console.log(numbers2)
+
+let height = 8;
+let width = 10;
+let board = "";
+
+for (let i = 0; i < height; i++) {
+    if(i > 0) board += "\n";
+    for (let j = 0; j < width; j++) {
+        if ((j + i) % 2 === 0){
+            board += ".";
+        } else {
+            board += "#";
+        }
+    }
+}
+
+console.log(board);
+
+let numbers = [];
+for(let i = 0; i < 15; i++) {
+    numbers.push(Math.pow(i, 3));
+}
+
+console.log(numbers)
+
+let multiplyTable = [];
+for(let i = 0; i <= 9; i++) {
+    let nestedArr = []
+    multiplyTable.push(nestedArr)
+    for(let j = 0; j <= 9; j++) {
+        nestedArr.push([j * i])
+    }
+}
+console.log(multiplyTable[5][6])
+
+
+let table = document.createElement('table')
+for(let i = 1; i <= 9; i++) {
+    let tr = document.createElement('tr');
+    for(let j = 1; j <= 9; j++) {
+        let td = document.createElement('td')
+        td.innerHTML = String(j * i)
+        tr.appendChild(td)
+    }
+    table.appendChild(tr)
+}
+
+document.body.appendChild(table)

+ 121 - 0
Js-HW5/Js-HW5.js

@@ -0,0 +1,121 @@
+let a = {
+    name: 'Olga',
+    surname: 'Kosareva'
+}
+let b = {
+    name: 'Elena',
+    surname: 'Krasnik'
+}
+let c = {
+    name: 'Leonid',
+    surname: 'Onoprienko'
+}
+
+a.age = 42;
+b.fatherName = 'Igorevna'
+c.sex = 'male'
+
+
+let arrofPersons = [a, b, c, {name: 'Anatoly', surname: 'Petrenko', age: 30}]
+
+for(let i = 0; i < arrofPersons.length; i++) {
+    console.log(arrofPersons[i]);
+}
+
+for(let person of arrofPersons) {
+    console.log(person.surname + ' ' + person.name);
+}
+
+for(let i = 0; i < arrofPersons.length; i++) {
+   for (let key in arrofPersons[i]) {
+        console.log(arrofPersons[i][key]);
+    }
+}
+
+for(let obj of arrofPersons) {
+    if(obj.name && obj.surname) {
+        obj.fullName = obj.name + ' ' + obj.surname
+    }
+ }
+console.log(arrofPersons);
+
+JSON.stringify(arrofPersons);
+
+
+// let str = "<table border='1'>"
+// arrofPersons.map(person => {
+//     str += `<tr><td>${person.name}</td><td>${person.surname}</td></tr>`
+// })
+// str += '</table>'
+// document.write(str)
+
+
+let str = "<table border='1'>"
+arrofPersons.map(person => {
+    str += `<tr>`
+    Object.values(person).map(value => str += `<td>${value}</td>`)
+    str += `</tr>`
+})
+str += '</table>'
+document.write(str)
+
+var someTree = {
+    tagName: "table", //html tag
+    subTags: [ //вложенные тэги
+        {
+                    tagName: "tr",
+                    subTags: [
+                        {
+                            tagName: "td",
+                            text: "some text",
+                        },
+                        {
+                            tagName: "td",
+                            text: "some text 2",
+                        }
+                    ]
+        }
+    ],
+    attrs:
+    {
+        border: 1,
+    },
+}
+
+let table = `<${someTree.tagName} ${Object.keys(someTree.attrs)[0]}=${Object.values(someTree.attrs)}>`
+
+for(let subTag of someTree.subTags) {
+    table += `<${subTag.tagName}>`
+    for(let innerTag of subTag.subTags) {
+        table += `<${innerTag.tagName}>${innerTag.text}</${innerTag.tagName}>`
+    }
+}
+table += `</${someTree.tagName}>`
+document.write(table);
+
+let arr = [1,2,3,4,5, "a", "b", "c"]
+
+let [odd1, even1, odd2, even2, odd3, ...letters] = arr
+
+let arr2 = [1, "abc"]
+
+let [number, [s1, s2, s3]] = arr2
+
+let obj = {
+    name: 'Ivan',
+    surname: 'Petrov',
+    children: [
+        {name: 'Maria'},
+        {name: 'Nikolay'}
+    ]
+}
+
+let {
+    children:
+        [{name: name1}, {name: name2}]} = obj
+
+
+let arr3 = [1,2,3,4, 5,6,7,10];
+
+let [first, second, {length = arr3.length}] = arr3
+

+ 84 - 0
Js-HW6/Js-HW6.js

@@ -0,0 +1,84 @@
+function a(text) {
+    alert(text)
+}
+
+function cube(n) {
+    return Math.pow(n, 3)
+}
+
+function avg2(a, b) {
+    return (a + b) / 2
+}
+
+function sum3(a, b, ...c) {
+    return a + b + +c
+}
+function intRandom(min = 0, max = 10) {
+    return Math.round(Math.random() * (max - min - 1) + min)
+}
+
+// function greetAll(...arguments) {
+//     for(let arg of arguments) {
+//         alert(`Hello, ${arg}!`)
+//     }
+// }
+
+function greetAll(...arguments) {
+    arguments = arguments.map(item => ' ' + item)
+    alert(`Hello, ${arguments}!`)
+
+}
+function sum(...arguments) {
+    let result = 0
+    for(let num of arguments) {
+        result += num
+    }
+    return result
+}
+
+var sample = prompt("Введите название задания")
+
+switch (sample.toLowerCase()){
+    case "a": a('Test prompt')
+        break
+    case "cube": cube(5)
+        break
+    case 'avg2': avg2(55, 17)
+        break
+    case 'sum3': sum3(5, 82, 47)
+        break
+    case 'intRandom': intRandom(1, 105)
+        break
+    case 'greetAll': greetAll('Sonya', 'Kerry', 'Eve')
+        break
+    case 'sum': sum(45, 741, 32, 14)
+        break
+    default:
+        a()
+}
+
+let sampleObj = {
+        a() {
+            a('Test prompt')
+        },
+        cube() {
+            cube(50)
+        },
+        avg2() {
+            avg2(55, 17)
+        },
+        sum3() {
+            sum3(5, 82, 47)
+        },
+        intRandom() {
+            intRandom(1, 50)
+        },
+        greetAll() {
+            greetAll('Sonya', 'Kerry', 'Eve')
+        },
+        sum() {
+            sum(45, 741, 32, 14)
+        },
+    };
+
+console.log(sampleObj[prompt("Введите название задания")]());

+ 52 - 0
Js-HW7/Js-HW7.js

@@ -0,0 +1,52 @@
+let $table = document.createElement('table');
+
+function rerenderColor(e, color) {
+    Array.from($table.children).map(item => {
+        Array.from(item.children).filter(item => item.cellIndex === e.target.cellIndex).map(item => item.style.backgroundColor = color)
+    })
+}
+
+for(let i = 1; i < 10; i++) {
+    let $tr = document.createElement('tr')
+
+    for(let j = 1; j < 10; j++) {
+        // $td.addEventListener('mouseover', (e) => rerenderColor(e, 'blue'))
+        // $td.addEventListener('mouseout', (e) => rerenderColor(e, 'transparent'))
+        let $td = document.createElement('td')
+        $td.innerText = String(j * i)
+
+        $td.addEventListener('mouseover', (e) => {
+            rerenderColor(e, 'red')
+
+            $td.style.backgroundColor = 'green'
+        })
+        $td.addEventListener('mouseout', (e) => rerenderColor(e, 'transparent'))
+        $tr.appendChild($td)
+    }
+    $table.appendChild($tr)
+}
+document.body.appendChild($table)
+
+const $n1 = document.querySelector('#n1')
+const $n2 = document.querySelector('#n2')
+const $calcBtn = document.querySelector('#calcBtn')
+const $result = document.querySelector('#result')
+
+const calcSum = () => {
+    return $result.innerText = String(+$n1.value + +$n2.value)
+}
+
+$calcBtn.addEventListener( 'click',  () => {
+    calcSum()
+})
+$n1.onchange = calcSum()
+$n2.onchange = calcSum()
+$calcBtn.onclick = calcSum()
+
+
+
+
+
+
+
+

+ 116 - 0
Js-HW8/Js-HW8.js

@@ -0,0 +1,116 @@
+var persons = [
+    {name: "Иван", age: 17},
+    {name: "Мария", age: 35},
+    {name: "Алексей", age: 73},
+    {name: "Яков", age: 12},
+]
+
+function sort(arr, type, bool = true) {
+    return arr.sort((a, b) => {
+        let res;
+        if (bool) {
+            console.log('true')
+            res = a[type] > b[type] ? 1 : -1;
+        } else {
+            console.log('false')
+            res = a[type] < b[[type]] ? 1 : -1;
+        }
+        return res
+    })
+}
+console.log(sort(persons, 'name', false))
+
+
+//ARRAY MAP
+let someArr = ["1", {}, null, undefined, "500", 700];
+
+let newArr = someArr.map(item => typeof item === 'string' ? +item : item)
+console.log(newArr)
+
+
+//ARRAY REDUCE
+let reduceArr = ["0", 5, 3, "string", null];
+let reducedArr = reduceArr.filter(item => typeof item === 'number').reduce((acc, curr) => acc *= curr)
+console.log(reducedArr)
+
+
+//OBJECT FILTER
+var phone = {
+    brand: "meizu",
+    model: "m2",
+    ram: 2,
+    color: "black",
+};
+
+function filter(obj, callback) {
+    let filteredObj = {}
+    for(let key in obj) {
+        if (callback(key, obj[key])) {
+            filteredObj = {...filteredObj, [key]: obj[key]}
+        }
+      }
+        return filteredObj
+}
+
+console.log(filter(phone,(key,value) => key === "color" || value === 2))
+
+
+//OBJECT MAP
+function map(obj, callback) {
+    let res = {};
+    for(let key in obj) {
+        res = { ...res, ...callback(key, obj[key]) }
+    }
+    return res
+}
+
+let test = map({name: "Иван", age: 17},function(key,value){
+    var result = {};
+    result[key+"_"] = value + "$";
+    return result;
+})
+console.log(test)
+
+
+//SUM
+function geomProgressionCalc(min, max, step){
+    if (max <= 0) return 0;
+    return geomProgressionCalc(min + step,max -1, step) + min
+}
+
+console.log(geomProgressionCalc(10, 20, 2))
+
+//HTML TREE RECURSION
+let $table = document.createElement('table');
+
+function rerenderColor(e, color) {
+    Array.from($table.children).map(item => {
+        Array.from(item.children).filter(item => item.cellIndex === e.target.cellIndex).map(item => item.style.backgroundColor = color)
+    })
+};
+
+let i = 10;
+function createRow() {
+    i--
+    let $tr = document.createElement('tr')
+
+    let j = 1;
+    function createCell() {
+        j++
+        let $td = document.createElement('td');
+        $td.innerText = String(j * i)
+        $td.addEventListener('mouseover', (e) => {
+            rerenderColor(e, 'red')
+            $td.style.backgroundColor = 'green'
+        })
+        $td.addEventListener('mouseout', (e) => rerenderColor(e, 'transparent'))
+        $tr.appendChild($td)
+        if(j < 9) createCell()
+    }
+    $table.appendChild($tr)
+    createCell()
+    if(i > 1) createRow();
+}
+
+createRow()
+document.body.appendChild($table);

+ 62 - 0
Js-HW9/Js-HW9.js

@@ -0,0 +1,62 @@
+function makeProfileTimer() {
+    let start = performance.now();
+    return function() {
+        let end = performance.now();
+        return end - start
+    };
+}
+
+let timer = makeProfileTimer();
+
+alert("Замеряем время работы этого alert");
+alert(`This code took ${timer()}ms`);
+
+function makeSaver(data){
+    let promptShowed = false;
+    let newData;
+    return () => {
+        if(!promptShowed) {
+            promptShowed = true;
+            return newData = data()
+        }
+    }
+}
+var saver = makeSaver(Math.random)
+var value1 = saver()
+var value2 = saver()
+console.log(value1, value2)
+console.log(value1 === value2 )
+var saver2 = makeSaver(() => console.log('saved function called') || [null, undefined, false, '', 0, Math.random()][Math.ceil(Math.random()*6)])
+var value3 = saver2()
+var value4 = saver2()
+console.log(value3 === value4)
+
+let namePrompt = prompt.bind(window, 'Как тебя зовут?')
+let nameSaver = makeSaver(namePrompt)
+
+alert(`Привет! Prompt еще не было!`)
+alert(`Привет ${nameSaver()}. Только что запустился prompt, первый и последний раз`)
+alert(`Слушай, ${nameSaver()}, го пить пиво. Ведь prompt был только один раз`)
+
+let count = 5;
+function timer2() {
+    console.log(count);
+    const timeout = setTimeout(() => {
+        timer();
+    }, 1000);
+    if (count > 0) {
+        return (() => {
+            return count--;
+        })();
+    }
+    clearTimeout(timeout);
+    if (count === 0) {
+        console.log("поехали!");
+    }
+}
+timer2();
+
+
+
+
+

+ 1 - 0
Readme.md

@@ -0,0 +1 @@
+Readme

+ 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;
+}