Browse Source

Модуль JS done

Levshin95 1 year ago
parent
commit
1e02e7408d

BIN
Модуль_JS/Loading_icon.gif


BIN
Модуль_JS/cart.png


+ 111 - 0
Модуль_JS/index.html

@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>Modul JS</title>
+    <meta charset='utf8' />
+    <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">
+    <style>
+        #mainContainer {
+            display: flex;
+        }
+
+        #aside {
+            width: 30%;
+        }
+
+        #aside>a {
+            display: block;
+        }
+    </style>
+</head>
+
+<body>
+    <header class="header">
+        <div class="header-wrap">
+            <a id="back" href="javascript:history.back();" rel="external nofollow" >
+                <div class="headerTitle">
+                        <p class="a-level">
+                            <font size="10" color="red">A</font><font size="10">-</font><font size="10" color="green">L</font><font size="10" color="yellow">E</font><font size="10" color="blue">V</font><font size="10" color="yellow">E</font><font size="10" color="green">L</font>
+                        </p>
+                        <p class="a-shop">
+                            <font size="10" color="orange">S</font><font size="10" color="yellow">H</font><font size="10" color="green">O</font><font size="10" color="red">P</font>
+                        </p>
+                </div>
+            </a>
+            <div class="headerBtn">
+                <a class="enterLogin" id="enterLogin">Log in</a>
+                
+                <button id="enterLogout" hidden> Log out</button>
+
+                <div id='cartIcon'><a class="enterCart" id="enterCart"><img src="cart.png" class="imgCart" alt=""></a></div>
+            </div>
+        </div>
+        
+        
+
+    </header>
+    <main class="main">
+        <div id='mainContainer'>
+            <aside class='aside' id='aside'>
+            </aside>
+            <div id='categories'>
+
+            </div>
+            <div class='main-container' id='main'>
+
+                <div class="mainLoginContainer" id="mainLoginContainer">
+                    <div class="log" id="hideLogin">
+                        <div class="header-div">
+                            <h1>Вход</h1>
+                            <a id="outLogin"><img class="outLogin" src="https://cdn-icons-png.flaticon.com/512/67/67345.png" alt="image description"></a>
+                        </div>
+
+                        <div class="input-form">
+                            <input type="text" class="inputUserName" id="inputUserName" placeholder="Введите Логин">
+                            <input type="password" class="inputPassword" id="inputPassword" placeholder="Введите Пароль">
+
+                        </div>
+                        <div class="btn-form">
+                            <button class="loginBtn" id="loginBtn">Войти</button>
+                        </div>
+                        <div class="register-href">
+                            <a class="enterRegister" id="enterRegister">Зарегистрироваться</a>
+                        </div>
+                    </div>
+                    <div class="registerContainer", id="registerContainer">
+                        <div class="reg">
+                            <div class="header-register">
+                                <h1>Регистрация</h1>
+                                <a id="outRegister"><img class="outLogin" src="https://cdn-icons-png.flaticon.com/512/67/67345.png" alt="image description"></a>
+                            </div>
+                            <div class="inputRegisterForm">
+                                <input type="text" class="inputUserNameRegister" id="inputUserNameRegister" placeholder="Введите Логин">
+                                <input type="password" class="inputPasswordRegister" id="inputPasswordRegister" placeholder="Введите Пароль">
+    
+                            </div>
+                            <div class="btnRegisterForm">
+                                <button class="registerBtn" id="registerBtn">Зарегистрироваться</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="cartContainer" class="cartContainer" style=" border: 3px; display:none">
+                <div class="headerCart">
+                    <h1>Корзина</h1>
+                    <a id="closeCart" class="closeCart" >X</a>
+                </div>
+                <div id="cartProductsContainer">
+                
+                </div>
+                <!-- <button id="clearCartBtn">Очистить</button> -->
+            </div>
+        </div>
+    </main>
+    <script src='index.js'></script>
+</body>
+
+</html>

+ 504 - 0
Модуль_JS/index.js

@@ -0,0 +1,504 @@
+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))
+    )
+
+    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 jwtDecode(token) {
+    try {
+        return JSON.parse(atob(token.split('.')[1]))
+    }
+    catch (e) {
+
+    }
+}
+
+function authReducer(state = {}, { type, token }) {
+    if (type === "AUTH_LOGIN") {
+        const payload = jwtDecode(token)
+        if (payload) {
+            return {
+                token, payload
+            }
+        }
+    }
+    if (type === "AUTH_LOGOUT") {
+        return {}
+    }
+    return state
+}
+
+const actionAuthLogin = (token) =>
+    (dispatch, getState) => {
+        const oldState = getState()
+        dispatch({ type: "AUTH_LOGIN", token })
+        const newState = getState()
+        if (oldState !== newState) {
+            localStorage.setItem('authToken', token);
+        }
+    }
+
+const actionAuthLogout = () =>
+    dispatch => {
+        dispatch({ type: "AUTH_LOGOUT" })
+        localStorage.removeItem('authToken')
+    }
+
+function promiseReducer(state = {}, { type, name, status, payload, error }) {
+    ////?????
+    //ОДИН ПРОМИС:
+    //состояние: PENDING/FULFILLED/REJECTED
+    //результат
+    //ошибка:
+    //{status, payload, error}
+    //{
+    //       name1:{status, payload, error}   
+    //       name2:{status, payload, error}   
+    //       name3:{status, payload, error}   
+    //}
+    if (type === 'PROMISE') {
+        //if (name == 'login' && status == 'FULFILLED') {
+        //    store.dispatch(actionAuthLogin(payload.data.login))
+        //    state = store.getState();
+        //}
+        return {
+            ...state,
+            [name]: { status, payload, error }
+        }
+    }
+
+    return state;
+}
+
+const actionPending = (name) => ({ type: 'PROMISE', status: 'PENDING', name })
+const actionFulfilled = (name, payload) => ({ type: 'PROMISE', status: 'FULFILLED', name, payload })
+const actionRejected = (name, error) => ({ type: 'PROMISE', status: 'REJECTED', name, error })
+
+const actionPromise = (name, promise) =>
+    async (dispatch) => {
+        try {
+            dispatch(actionPending(name))
+            let payload = await promise
+            dispatch(actionFulfilled(name, payload))
+            if (name === 'login') {
+                store.dispatch(actionAuthLogin(payload.data.login))
+            }
+            return payload
+        }
+        catch (e) {
+            console.log(e);
+            dispatch(actionRejected(name, e))
+        }
+    }
+
+function cartReducer(state = {}, { type, count = 1, good }) {
+    // type CART_ADD CART_REMOVE CAT_CLEAR CART_DEC
+    // {
+    //  id1: {count: 1, good{name, price, images, id}, total: price}
+    // }
+    if (type === "CART_ADD") {
+        return {
+            ...state,
+            [good._id]: { count: count + (state[good._id]?.count || 0), good }
+
+        }
+    }
+    if (type === "CART_CLEAR") {
+        return {}
+    }
+    if (type === "CART_REMOVE") {
+        let newState = { ...state }
+        delete newState[good._id]
+        return newState
+    }
+    if (type === "CART_DELETE") {
+        if (state[good._id].count > 1) {
+            return {
+                ...state, [good._id]: { count: -count + (state[good._id]?.count || 0), good },
+            };
+        }
+    }
+
+    return state
+}
+
+const cartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
+const cartClear = () => ({ type: 'CART_CLEAR' })
+const cartRemove = (good) => ({ type: 'CART_REMOVE', good })
+const cartDelete = (good) => ({ type: 'CART_DELETE', good })
+
+/* store.dispatch({type: "CART_ADD", good: {_id: "чипсы"}})  ПРОБНЫЙ СТОР!!! */
+
+const delay = (ms) => new Promise((ok) => setTimeout(() => ok(ms), ms))
+
+function combineReducers(reducers) {
+    function combinedReducer(combinedState = {}, action) {
+        const newCombinedState = {}
+        for (const [reducerName, reducer] of Object.entries(reducers)) {
+            const newSubState = reducer(combinedState[reducerName], action)
+            if (newSubState !== combinedState[reducerName]) {
+                newCombinedState[reducerName] = newSubState
+            }
+        }
+        if (Object.keys(newCombinedState).length === 0) {
+            return combinedState
+        }
+        return { ...combinedState, ...newCombinedState }
+    }
+
+    return combinedReducer
+}
+
+const store = createStore(
+    combineReducers({ auth: authReducer, promise: promiseReducer, cart: cartReducer })
+) //не забудьте combineReducers если он у вас уже есть
+
+
+const enterLogin = document.getElementById('enterLogin')
+const outLogin = document.getElementById('outLogin')
+const enterLogout = document.getElementById('enterLogout')
+const loginContainer = document.getElementById('mainLoginContainer')
+const enterCart = document.getElementById('enterCart')
+const cartContainer = document.getElementById('cartContainer')
+const leaveCart = document.getElementById('closeCart')
+const cartProductsContainer = document.getElementById('cartProductsContainer')
+const enterRegister = document.getElementById('enterRegister')
+const registerContainer = document.getElementById('registerContainer')
+const hideLogin = document.getElementById('hideLogin')
+const outRegister = document.getElementById('outRegister')
+
+
+if (localStorage.authToken) {
+    store.dispatch(actionAuthLogin(localStorage.authToken))
+    enterLogin.style.display = 'none';
+    enterCart.style.display = 'block'
+    enterLogout.style.display = 'block';
+}
+//const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
+store.subscribe(() => console.log(store.getState()))
+
+
+
+const gql = (url, query, variables) => fetch(url, {
+    method: 'POST',
+    headers: {
+        "Content-Type": "application/json",
+        Accept: "application/json",
+    },
+    body: JSON.stringify({ query, variables })
+}).then(res => res.json())
+
+const backendURL = 'http://shop-roles.node.ed.asmer.org.ua'
+const graphQLBackendUrl = `${backendURL}/graphql` 
+
+
+const actionRootCats = () =>
+    actionPromise('rootCats', gql(graphQLBackendUrl, `query {
+        CategoryFind(query: "[{\\"parent\\":null}]"){
+            _id name
+        }
+    }`))
+
+const actionCatById = (_id) =>  //добавить подкатегории
+    actionPromise('catById', gql(graphQLBackendUrl, `query  categoryById($q: String){
+        CategoryFindOne(query: $q){
+            _id name goods {
+                _id name price images {
+                    url
+                }
+            }
+        }
+    }`, { q: JSON.stringify([{ _id }]) }))
+
+
+const actionGoodById = (_id) =>
+    actionPromise('goodById', gql(graphQLBackendUrl, `query goodById($q: String){
+        GoodFindOne(query: $q){
+            _id name description price
+        }
+    }`, { q: JSON.stringify([{ _id }]) }))
+
+
+const actionLogin = (login, password) =>
+    actionPromise('login', gql(graphQLBackendUrl,
+        `query log($login: String, $password: String){
+            login(login:$login, password: $password)
+        }`
+        , { login, password }))
+
+
+ /*        actionPromise(
+            "register",
+            gql(
+                `mutation register($login: String, $password: String) {
+                UserUpsert(user: {login: $login, password: $password}) {
+                   _id
+                   login
+                 }
+               }`,
+                { login: login, password: password }
+            )
+        ) */
+
+
+store.dispatch(actionRootCats())
+
+/* const actionEnterLogin = () */
+
+enterLogin.onclick = () => {
+    return loginContainer.style.display = 'block'
+}
+
+outLogin.onclick = () => {
+    return loginContainer.style.display = 'none'
+}
+
+enterLogout.onclick = () => {
+    store.dispatch(actionAuthLogout())
+}
+
+enterRegister.onclick = () => {
+    return hideLogin.style.display = 'none', registerContainer.style.display = 'block'
+}
+
+outRegister.onclick = () => {
+    return hideLogin.style.display = 'block', registerContainer.style.display = 'none'
+}
+
+const updateCartInfo = () => {
+    cartProductsContainer.innerHTML = '';
+    cartContainer.style.display = 'block';
+    const cart = store.getState().cart;
+    const ul = document.createElement('ul')
+    cartProductsContainer.append(ul)
+
+    const clearFromCartButton = document.createElement('button')
+        clearFromCartButton.innerHTML = "Очистить"
+        clearFromCartButton.onclick = () => { store.dispatch(cartClear())}
+        cartProductsContainer.append(clearFromCartButton)
+
+    const buyGoods = document.createElement('button')
+        buyGoods.innerHTML = "Купить"
+        buyGoods.onclick = () => {store.dispatch(cartClear()), alert('Спасибо за покупку!')}
+        cartProductsContainer.append(buyGoods)
+
+    for (const [key, value] of Object.entries(cart)) {
+        const li = document.createElement('li')
+        const a = document.createElement('a')
+        const removeFromCartButton = document.createElement('button')
+        removeFromCartButton.innerText = "-"
+        removeFromCartButton.onclick = () => { store.dispatch(cartRemove(value.good)) }
+       
+        a.innerHTML = value.good.name;
+        ul.append(li)
+        li.append(a)
+        if (value.good.images) {
+            for (let i = 0; i < value.good.images.length; i++) {
+                const imgElement = document.createElement('img')
+                imgElement.src = `${backendURL}/${value.good.images[i].url}`;
+                imgElement.style.height = '100px';
+                imgElement.style.width = '200px';
+                li.append(imgElement)
+            }
+        }
+
+        ul.append(removeFromCartButton)
+        if (value.count > 1) {
+
+            const deleteFromCartButton = document.createElement('button')
+            deleteFromCartButton.innerHTML = "-1"
+            deleteFromCartButton.onclick = () => { store.dispatch(cartDelete(value.good))}
+            ul.append(deleteFromCartButton)
+        } 
+        const allCountElement = document.createElement('p');
+        allCountElement.innerHTML = `Total: ${value.count}`;
+        ul.append(allCountElement)
+    }
+    
+};
+
+enterCart.onclick = () => {
+    updateCartInfo();
+}
+
+leaveCart.onclick = () => {
+    cartProductsContainer.innerHTML = '';
+    cartContainer.style.display = 'none';
+}
+
+
+const loginBtn = document.getElementById('loginBtn')
+loginBtn.onclick = handleLogin
+
+function handleLogin() {
+    let userName = document.getElementById('inputUserName')
+    let userPassword = document.getElementById('inputPassword')
+    store.dispatch(actionLogin(userName.value, userPassword.value));
+    outLogin.click();
+}
+
+
+/* store.dispatch(actionLogin('levshin95', '123123')) */
+
+
+store.subscribe(() => {
+    if (cartContainer.style.display === 'block' && store.getState().cart) {
+        updateCartInfo();
+    }
+})
+
+store.subscribe(() => {
+    const rootCats = store.getState().promise.rootCats?.payload?.data.CategoryFind
+    if (!rootCats) {
+        aside.innerHTML = '<img src="Loading_icon.gif">'
+    } else {
+        aside.innerHTML = ''
+        const ul = document.createElement('ul')
+        aside.append(ul)
+        for (let { _id, name } of rootCats) {
+            const li = document.createElement('li')
+            const a = document.createElement('a')
+            a.href = "#/category/" + _id
+            a.innerHTML = name
+            ul.append(li)
+            li.append(a)
+        }
+    }
+})
+
+const displayCategory = function () {
+    const catById = store.getState().promise.catById?.payload?.data.CategoryFindOne
+    const [, route] = location.hash.split('/')
+    if (catById && route === 'category') {
+        const { name, goods, _id } = catById
+        categories.innerHTML = `<h1>${name}</h1>`
+        const ul = document.createElement('ul')
+        categories.append(ul)
+        for (let good of goods) {
+            const li = document.createElement('li')
+            const a = document.createElement('a')
+            a.href = "#/good/" + good._id
+            a.innerHTML = good.name
+            ul.append(li)
+            li.append(a)
+            if (good.images) {
+                for (let i = 0; i < good.images.length; i++) {
+                    const imgElement = document.createElement('img')
+                    imgElement.src = `${backendURL}/${good.images[i].url}`;
+                    imgElement.style.height = '100px';
+                    imgElement.style.width = '200px';
+                    li.append(imgElement)
+                }
+            }
+
+            const authToken = store.getState().auth?.token;
+
+            if (authToken) {
+                const addToCartButton = document.createElement('button')
+                addToCartButton.innerText = "+"
+                addToCartButton.onclick = () => { store.dispatch(cartAdd(good, 1)) }
+                ul.append(addToCartButton)
+            }
+
+
+        }
+    }
+}
+
+store.subscribe(() => {
+    const authToken = store.getState().auth?.token;
+    if (!authToken) {
+        enterLogout.style.display = 'none';
+        enterCart.style.display = 'none';
+        enterLogin.style.display = 'block';
+        displayCategory();
+    } else {
+        debugger
+        enterLogin.style.display = 'none';
+        enterCart.style.display = 'block';
+        enterLogout.style.display = 'block';
+        displayCategory();
+        /* alert(`Hello, ${store.getState().auth?.payload?.sub?.login}`); */
+    }
+})
+
+
+
+store.subscribe(() => {
+    displayCategory();
+})
+
+store.subscribe(() => {
+
+    const goodById = store.getState().promise.goodById?.payload?.data.GoodFindOne
+    const [, route] = location.hash.split('/')
+    if (goodById && route === 'good') {
+        const { name, description, _id, price, images} = goodById
+        categories.innerHTML = `<h1>${name}</h1>`
+
+        const strongPrice = document.createElement('b')
+        const div = document.createElement('div')
+
+        categories.appendChild(div)
+        categories.appendChild(strongPrice)
+
+        categories.appendChild(strongPrice)
+
+        div.innerText = description
+        strongPrice.innerHTML = price
+    }
+})
+
+store.subscribe(() => { })
+
+
+
+/* store.subscribe(() => {
+    const goodById = store.getState().promise.goodById?.payload?.data.CategoryFindOne
+    const [,route] = location.hash.split('/')
+    if (catById && route === 'good') {
+        const {name, goods, _id} = catById
+        categories.innerHTML = `<h1>${name}</h1>`
+        // нарисовать картинки, описание, цену и т.д.
+    }
+}) */
+
+window.onhashchange = () => {
+    const [, route, _id] = location.hash.split('/')
+    console.log(route, _id)
+    const routes = {
+        category() {
+            store.dispatch(actionCatById(_id))
+        },
+        good() {
+            store.dispatch(actionGoodById(_id))
+        }
+    }
+    if (route in routes) {
+        routes[route]()
+    }
+}
+
+window.onhashchange()

+ 175 - 0
Модуль_JS/style.css

@@ -0,0 +1,175 @@
+.header-wrap {
+    display: flex;
+    align-items: center;
+    min-height: 100px;
+    background-color: rgb(179, 177, 180);
+    justify-content: space-between;
+    padding-right: 40px;
+}
+
+.headerBtn {
+    display: flex;
+    justify-content: flex-end;
+}
+
+.headerTitle {
+    display: flex;
+    padding-left: 40px;
+}
+
+.a-shop {
+    padding-left: 15px;
+}
+
+.enterLogin{
+    padding-right: 20px;
+}
+
+a{
+    text-decoration: none;
+    color:#000000;
+}
+
+a:hover{
+    color: #d80e0e;
+}
+
+li {
+    list-style-type: none;
+    padding: 5px;
+}
+
+.aside{
+    padding-top: 20px;
+    height: 100vh;
+}
+
+.main-container{
+    padding-left: 50px;
+    width: 100vh;
+    height: 100vh;    
+}
+
+.mainLoginContainer {
+    display: none;
+    border: 2px solid #000000;
+    margin-top: 50px;
+    max-width: 400px;
+}
+
+.header-div{
+    display: flex;
+    justify-content: space-between;
+    max-width: 350px;
+}
+
+.outLogin{
+    max-width: 20px;
+    max-height: 20px;
+    padding-top: 20px;
+}
+
+.log{
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+}
+
+.input-form{
+    padding-bottom: 20px;
+}
+
+.inputUserName, .inputPassword{
+    min-height: 40px;
+}
+
+.loginBtn{
+    display: inline-block;
+    border: 1px solid #34547a;
+    background: #34547a;
+    color: white;
+    text-transform: uppercase;
+    padding: 23px 155px;
+    line-height: 1;
+    text-decoration: none;
+    min-height: 27px;
+}
+
+.enterCart{
+    display: none;
+    
+}
+
+.imgCart {
+    padding-top: 3px;
+    padding-left: 10px;
+    max-width: 40px;
+    max-height: 20px;
+}
+
+.headerCart {
+    display: flex;
+    justify-content: space-between;
+}
+
+.closeCart{
+    height: 17px;
+    width: 12px;
+    border: 2px solid black;
+}
+
+.enterRegister{
+    color: rgb(0, 132, 255);
+}
+
+.register-href{
+    padding-top: 20px;
+    display: flex;
+    justify-content: center;
+}
+
+.registerContainer{
+    display: none;
+}
+
+.inputRegisterForm{
+    display: flex;
+    flex-direction: column;
+    padding: 20px;
+}
+
+.inputUserNameRegister{
+    min-height: 30px;
+}
+
+.inputPasswordRegister{
+    margin-top: 20px;
+    min-height: 30px;
+}
+
+.registerBtn{
+    display: inline-block;
+    border: 1px solid #34547a;
+    background: #34547a;
+    color: white;
+    text-transform: uppercase;
+    padding: 25px 100px;
+    line-height: 1;
+    text-decoration: none;
+    min-height: 65px;
+}
+
+.header-register{
+    display: flex;
+    justify-content: space-between;
+    max-width: 350px;
+}
+
+.header-register h1{
+    /* padding-left: 20px; */
+    margin-top: 10px;
+}
+
+.reg{
+    padding: 20px;
+}