Vladimir 2 лет назад
Родитель
Сommit
ed671b4866

+ 34 - 0
HW Мой МАГАЗЫН/index.html

@@ -0,0 +1,34 @@
+<!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">
+    <link rel="stylesheet" href="style.css">
+    <title>Document</title>
+</head>
+<body class="body">
+    <header class="header">
+        <h1>Название магазина</h1>
+        <ul>
+            <li>
+                <button class="header__registration">Регистрация</button>
+            </li>
+            <li>
+                <button class="header__sign-in">Вход</button>
+            </li>
+            <li>
+                <button class="header__cart">Корзина</button>
+            </li>
+            <li>
+                <button class="header__dashboard">История заказов</button>
+            </li>
+        </ul>
+    </header>
+    <main class="main">
+        <aside class="aside"></aside>
+        <div class="main__main-content"></div>
+    </main>
+    <script src="main.js"></script>
+</body>
+</html>

+ 473 - 0
HW Мой МАГАЗЫН/main.js

@@ -0,0 +1,473 @@
+let aside              = document.querySelector(".aside");
+let content            = document.querySelector(".main__main-content");
+let registration = document.querySelector(".header__registration");
+let signIn       = document.querySelector(".header__sign-in");
+let cart         = document.querySelector(".imaginary-shopping-cart--content");
+let cartButton   = document.querySelector(".header__cart");
+let dasboard     = document.querySelector(".header__dashboard");
+
+registration.addEventListener("click", function() {
+    location.href = "#/register";
+});
+
+signIn.addEventListener("click", function() {
+    location.href = "#/login";
+});
+
+cartButton.addEventListener("click", function() {
+    location.href = "#/cart";
+});
+
+dasboard.addEventListener("click", function() {
+    location.href = "#/dasboard";
+});
+
+let createStore = function(reducer) {
+    let state = reducer(undefined, {});
+    let cbs = [];
+
+    let getState = () => state;
+    let subscribe = function(cb) {
+        cbs.push(cb);
+        return () => cbs = cbs.filter(c => c !== cb);
+    };
+    let dispatch = function(action) {
+        if(typeof(action) == "function") {
+            return action(dispatch, getState);
+        };
+
+        let newState = reducer(state, action);
+
+        if (newState !== state){
+            state = newState;
+
+            for (let cb of cbs) {
+                cb();
+            };
+        };
+    };
+
+    return {
+        getState,
+        subscribe,
+        dispatch
+    };
+};
+
+const promiseReducer = function(state={}, {type, name, status, payload, error}) {
+    if (type == 'PROMISE'){
+        return {
+            ...state,
+            [name]:{status, payload, error}
+        }
+    }
+
+    return state;
+};
+
+const actionPending             = name => ({type: "PROMISE", name, status: 'PENDING'});
+const actionFulfilled = (name,payload) => ({type: "PROMISE", name, status: 'FULFILLED', payload});
+const actionRejected  = (name,error)   => ({type: "PROMISE", name, status: 'REJECTED', error});
+const actionPromise = function(name, promise) {
+    return async dispatch => {
+        dispatch(actionPending(name));
+        try {
+            let payload = await promise
+            dispatch(actionFulfilled(name, payload))
+            return payload
+        }
+        catch(error){
+            dispatch(actionRejected(name, error))
+        };
+    };
+};
+
+let jwtDecode = function(token) {
+    let payloadInBase64;
+    let payloadInJson;
+    let payload;
+
+    try {
+        payloadInBase64 = token.split(".")[1];
+        payloadInJson = atob(payloadInBase64);
+        payload = JSON.parse(payloadInJson);
+
+        return payload;
+    }
+    catch(err) {
+
+    }
+};
+
+const authReducer = function(state = {}, {type, token}) {
+    let payload;
+
+    if (state == undefined) {
+        if(localStorage.authToken) {
+            type = "AUTH_LOGIN";
+            token = localStorage.authToken;
+        } else {
+            type = "AUTH_LOGOUT";
+        };
+    };
+    if (type == "AUTH_LOGIN") {
+        payload = jwtDecode(token);
+
+        if(payload) {
+            localStorage.authToken = token;
+        }
+
+        return {
+            token: token,
+            payload: payload
+        }
+    };
+    if (type == "AUTH_LOGOUT") {
+        localStorage.removeItem("authToken");
+
+        return {};
+    };
+
+    return state || {};
+};
+
+const actionAuthLogin  = token => ({type: "AUTH_LOGIN", token});
+const actionAuthLogout = () => ({type: "AUTH_LOGOUT"});
+
+let cartReducer = function(state={}, {type, good, count=1}) {
+    if(type == "CART_ADD") {
+        let newState = {...state};
+
+        if(good["_id"] in state) {
+            newState[good._id].count = newState[good._id].count + count;
+        }
+        else {
+            newState = {
+                ...state,
+                [good._id]: {count, good} 
+            };
+        };
+
+        return newState;
+    };
+    if(type == "CART_CHANGE") {
+        let newState = {...state,
+            [good._id]: {count, good}
+        };
+
+        return newState;
+    };
+    if(type == "CART_DELETE") {
+        let newState = {...state};
+
+        delete newState[good._id];
+
+        return newState;
+    };
+    if(type == "CART_CLEAR") {
+        return {};
+    };
+
+    return state;
+};
+
+const actionCartAdd    = (good, count=1) => ({type: 'CART_ADD', good, count})
+const actionCartChange = (good, count=1) => ({type: 'CART_CHANGE', good, count})
+const actionCartDelete = (good)          => ({type: 'CART_DELETE', good})
+const actionCartClear  = ()              => ({type: 'CART_CLEAR'})
+
+const combineReducers = function(reducers) {
+    return function (state={}, action) {
+        const newState = {};
+        
+        for(let [reducerName, reducer] of Object.entries(reducers)) {
+            let checkState = reducer(state[reducerName], action);
+
+            if(state[reducerName] != checkState) {
+                newState[reducerName] = checkState;
+            };
+        };
+
+        if(Object.keys(newState).length == 0) {
+            return state;
+        };
+
+        return {...Object.assign(state, newState)};
+    };
+};
+
+const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}));
+store.subscribe(() => console.log(store.getState()));
+
+const getGQL = function(url) {
+    return async function(query, variables) {
+        const res = await fetch(url, {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/json",
+                ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
+            },
+            body: JSON.stringify({ query, variables })
+        });
+        const data = await res.json();
+        if (data.data) {
+            return Object.values(data.data)[0];
+        }
+        else {
+            throw new Error(JSON.stringify(data.errors));
+        };
+    };
+};
+
+const backendURL = 'http://shop-roles.asmer.fs.a-level.com.ua';
+
+const gql = getGQL(backendURL + '/graphql');
+
+let actionRootCats = function() {
+    return actionPromise("rootCats", gql(`query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+      }
+    }`));
+};
+
+store.dispatch(actionRootCats());
+
+let actionCatById = function(_id) {
+    return actionPromise("catById", gql(`query catById($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            }
+        }
+    }`,
+    {q: JSON.stringify([{_id}])}
+    ));
+};
+
+let actionGoodById = function(_id) {
+    return actionPromise("goodById", gql(`query findGood($goodQuery: String) {
+        GoodFindOne(query:$goodQuery) {
+            _id name price images {
+                url
+            }
+        }
+    }`,
+    {goodQuery: JSON.stringify([{"_id": _id}])}
+    ));
+};
+
+let actionFullLogin = async function(login, password) {
+    let token = await gql("query userLogin($login: String, $password: String) {login(login: $login, password: $password)}", {"login": login, "password": password});
+
+    store.dispatch(actionAuthLogin(token));
+};
+
+let actionFullRegister = function(login, password, nick) {
+    return actionPromise("userRegister", gql(`mutation userRegister($login:String, $password:String, $nick:String) {
+        UserUpsert(user: {login:$login, password:$password, nick:$nick}) {
+          _id login nick
+        }
+      }`,
+      {
+        "login": login,
+        "password": password,
+        "nick": nick
+      }))
+};
+
+let actionOrders = async function() {
+    let order = await gql(`mutation makeOrder($order:OrderInput){
+        OrderUpsert(order: $order){
+          _id
+        }
+      }`, {
+          "order": {
+              orderGoods: Object.entries(store.getState().cart).map(([_id, count]) =>({"count": count.count, "good": {_id}}))
+            }
+      });
+
+    store.dispatch(actionCartClear());
+}
+
+let createCart = function() {
+    const [,route, _id] = location.hash.split('/');
+
+    if(route == "cart") {
+        let str = "<h2>Корзина</h2><table id='table'>";
+
+        for(let goodId of Object.entries(store.getState().cart)) {
+            str += `<tr>
+                        <td class="visually-hidden">${goodId[0]}</td>
+                        <td>${goodId[1].good.name}</td>
+                        <td><img src="${backendURL}/${goodId[1].good.images[0].url}"/></td>
+                        <td>Цена: <span id="price">${goodId[1].good.price}</span></td>
+                        <td>Кол-во: <input id="count" type="number" value="${goodId[1].count}"></td>
+                        <td><button id="clear">Удалить товар</button></td>
+                    </tr>`;
+        };
+        str += "</table><button id='order'>Сделать заказ</button>";
+
+        content.innerHTML = str;
+
+        table.addEventListener("click", function(evt) {
+            if(evt.target.tagName == "BUTTON") {
+                store.dispatch(actionCartDelete(store.getState().cart[evt.path[2].firstElementChild.textContent]?.good))
+            };
+        });
+
+        order.addEventListener("click", function() {
+            actionOrders();
+        });
+    }
+};
+
+let createDashbord = async function() {
+    const [,route, _id] = location.hash.split('/');
+
+    if(route == "dasboard") {
+        let orders = await gql(`query ordersFind($query:String) {
+            OrderFind(query: $query) {
+              createdAt orderGoods {
+                count good {
+                  name price images {
+                    url
+                  }
+                }
+              }
+            }
+          }`,
+            {
+            "query": JSON.stringify([{}])
+            });
+        let str = "<h2>История заказов</h2><table>";
+    
+        for(let order of orders) {
+            str += "<tr>";
+            str += `<td>${(new Date(+order.createdAt)).toLocaleString()}</td>`;
+            str += "<td>"
+            for(let orderGood of order.orderGoods) {
+                str += `<ul>
+                            <li>${orderGood.good.name}</li>
+                            <li><img src="${backendURL}/${orderGood.good.images[0].url}"/></li>
+                            <li>Цена: ${orderGood.good.price}</li>
+                        </ul>`;
+            }
+            str += "</td>"
+            str += "</tr>";
+        };
+
+        str += "</table>";
+
+        content.innerHTML = str;
+    }
+};
+
+window.onhashchange = () => {
+    const [, route, _id] = location.hash.split('/');
+
+    const routes = {
+        category(){
+            store.dispatch(actionCatById(_id));
+        },
+        good(){
+            store.dispatch(actionGoodById(_id));
+        },
+        login(){
+            content.innerHTML = `<h2>Вход на сайт</h2>
+                                 <input id="login" type="text" name="login" placeholder="Введите ваш логин">
+                                 <input id="password" type="password" name="password" placeholder="Введите ваш пароль">
+                                 <button id="sign_in">Войти</button>`;
+            sign_in.addEventListener("click", function() {
+                actionFullLogin(login.value, password.value);
+                // store.dispatch(actionFullLogin(login.value, password.value));
+            });
+        },
+        register(){
+            content.innerHTML = `<h2>Регистрация</h2>
+                                 <input id="login" type="text" name="reg-login" placeholder="Введите ваш будущий логин">
+                                 <input id="password" type="text" name="reg-password" placeholder="Введите ваш будущий пароль">
+                                 <input id="nick" type="text" name="reg-nick" placeholder="Введите ваш nick">
+                                 <button id="registr">Зарегистрироваться</button>`;
+            registr.addEventListener("click", function() {
+                store.dispatch(actionFullRegister(login.value, password.value, nick.value));
+            });
+        },
+        cart() {
+            createCart();        
+        },
+        dasboard() {
+            createDashbord();
+        }
+    };
+
+    if (route in routes) {
+        routes[route]();
+    };
+};
+
+window.onhashchange();
+
+store.subscribe(() => {
+    const {rootCats} = store.getState().promise;
+    if (rootCats?.payload){
+        let ul = document.createElement("ul");
+
+        aside.innerHTML = '';
+
+        for (const {_id, name} of rootCats?.payload){
+            let li = document.createElement("li");
+            let link = document.createElement('a');
+            link.href       = `#/category/${_id}`;
+            link.innerText  = name;
+
+            li.append(link);
+            ul.append(li);
+        }
+
+        aside.append(ul);
+    };
+});
+
+store.subscribe(() => {
+    const {catById} = store.getState().promise;
+    const [,route, _id] = location.hash.split('/')
+    if (catById?.payload && route === 'category'){
+        const {name} = catById.payload 
+        content.innerHTML = `<h1>${name}</h1> ТУТ ДОЛЖНЫ БЫТЬ ПОДКАТЕГОРИИ`
+        for (const {_id, name, price, images} of catById.payload.goods){
+            const card      = document.createElement('div')
+            card.innerHTML = `<h2>${name}</h2>
+                              <img src="${backendURL}/${images[0].url}" />
+                              <strong>${price}</strong>
+                              <a href="#/good/${_id}">ССЫЛКА НА СТРАНИЦУ ТОВАРА</a>`
+            content.append(card);
+        }
+    }
+});
+
+store.subscribe(() => {
+    const {goodById} = store.getState().promise;
+    const [,route, _id] = location.hash.split('/');
+
+    if (goodById?.payload && route == "good") {
+        const {name} = goodById.payload;
+
+        content.innerHTML = `<h1>${name}</h1>`;
+
+        const card = document.createElement('div');
+
+        card.innerHTML = `<h2>${goodById.payload.name}</h2>
+                          <img src="${backendURL}/${goodById.payload.images[0].url}" />
+                          <strong>${goodById.payload.price}</strong>
+                          <button id="goodAdd">Добавить в корзину</button>`;
+        content.append(card);
+        goodAdd.addEventListener("click", function() {
+            store.dispatch(actionCartAdd(goodById.payload));
+        });
+    };
+});
+
+store.subscribe(createCart);

+ 47 - 0
HW Мой МАГАЗЫН/style.css

@@ -0,0 +1,47 @@
+.body {
+    font-family: sans-serif;
+    font-size: 18px;
+    font-weight: 24px;
+}
+
+.header {
+    display: flex;
+    flex-wrap: nowrap;
+    justify-content: space-between;
+    align-items: center;
+}
+
+ul {
+    margin: 0;
+    padding: 0;
+    list-style: none;
+}
+
+.main {
+    display: flex;
+    flex-wrap: nowrap;
+}
+
+.main__main-content {
+    flex-grow: 1;
+}
+
+.main__main-content img {
+    width: 100px;
+}
+
+.visually-hidden:not(:focus):not(:active),
+input[type=checkbox].visually-hidden,
+input[type=radio].visually-hidden {
+    position:absolute;
+    width:1px;
+    height:1px;
+    margin:-1px;
+    border:0;
+    padding:0;
+    white-space:nowrap;
+    -webkit-clip-path:inset(100%);
+    clip-path:inset(100%);
+    clip:rect(0 0 0 0);
+    overflow:hidden
+}