123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- 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 promiseReducer(state = {}, { type, name, status, payload, error }) {
- if (type === 'PROMISE') {
- return {
- ...state,
- [name]: { status, payload, 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 payload = await promise
- dispatch(actionResolved(name, payload))
- return payload
- }
- catch (error) {
- dispatch(actionRejected(name, error))
- }
- }
- const getGQL = url =>
- (query, variables = {}) =>
- fetch(url, {
- method: 'POST',
- headers: {
- "Content-Type": "application/json",
- ...(localStorage.authToken ? { "Authorization": "Bearer " + localStorage.authToken } : {})
- },
- body: JSON.stringify({ query, variables })
- })
- .then(res => res.json())
- .then(data => {
- if (data.errors && !data.data)
- throw new Error(JSON.stringify(data.errors))
- return data.data[Object.keys(data.data)[0]]
- })
- const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
- const gql = getGQL(`${backURL}/graphql`)
- const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
- function jwtDecode(token) {
- try {
- let decoded = token.split('.')
- decoded = decoded[1]
- decoded = atob(decoded)
- decoded = JSON.parse(decoded)
- return decoded
- } catch (e) {
- return;
- }
- }
- function authReducer(state, { type, token }) {
- if (!state) {
- if (!localStorage.authToken) {
- console.log('NO-TOKEN')
- return {}
- } else {
- type = 'AUTH_LOGIN'
- token = localStorage.authToken
- }
- }
- if (type === 'AUTH_LOGIN') {
- console.log('AUTH-LOGIN')
- let decoded = jwtDecode(token)
- if (decoded) {
- localStorage.authToken = token
- return { token, payload: decoded }
- }
- }
- if (type === 'AUTH_LOGOUT') {
- console.log('AUTH-LOGOUT')
- localStorage.removeItem('authToken')
- return {}
- }
- return state
- }
- function combineReducers(reducers) {
- return (state = {}, action) => {
- const newState = {}
- for (const [reducerName, reducer] of Object.entries(reducers)) {
- let newSubState = reducer(state[reducerName], action)
- if (newSubState !== state[reducerName]) {
- newState[reducerName] = newSubState
- }
- }
- if (Object.keys(newState).length !== 0) {
- return { ...state, ...newState }
- }
- return state
- }
- }
- const combinedReducer = combineReducers({ promise: promiseReducer, auth: authReducer, cart: cartReducer })
- const store = createStore(combinedReducer)
- const actionAuthLogin = token => ({ type: 'AUTH_LOGIN', token })
- const actionAuthLogout = () => ({ type: 'AUTH_LOGOUT' })
- console.log(store.getState()) //стартовое состояние может быть с токеном
- store.subscribe(() => console.log(store.getState()))
- 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))
- }
- }
- const actionGetOrders = () =>
- actionPromise('orders', gql(`
- query ord {
- OrderFind(query:"[{}]"){
- _id
- orderGoods {
- price
- count
- total
- good{
- name
- _id
- images {
- url
- }
- }
- }
- }
- }
- `))
- const actionCreateOrder = (count, id) =>
- actionPromise('createOrder', gql(`
- mutation createOrder($count:Int!, $id:ID) {
- OrderUpsert( order:{ orderGoods: [ {count: $count, good:{_id: $id}} ] } ) {
- _id
- total
- orderGoods {
- good { _id name }
- }
- }
- }
- `, {count, id}))
- const actionRegister = (login, password) =>
- actionPromise('registration', gql(`
- mutation register($login:String, $password:String) {
- UserUpsert(
- user: {
- login: $login,
- password: $password,
- }){
- login _id
- }
- }
- `, { login, password }))
- let logBtn = document.getElementById('logBtn')
- function cartReducer(state = {}, { type, good = {}, count = 1 }) {
- let { _id } = good
- console.log('_id from state',_id)
- const types = {
- CART_ADD() {
- return {
- ...state,
- [_id]: { good, count: (state[_id]?.count || 0) + count }
- }
- },
- CART_REMOVE() {
- let newState = {}
- for(let key in state) {
- if(key !== _id) {
- newState[key] = state[key]
- }
- }
- return newState
- },
- CART_CHANGE() {
- return {
- ...state,
- [_id]: { good, count }
- }
- },
- CART_CLEAR() {
- return {}
- },
- }
- if (type in types)
- return types[type]()
- return state
- }
- const actionCartAdd = (good, count = 1) => ({ type: 'CART_ADD', good, count })
- const actionCartDel = (good) => ({ type: 'CART_REMOVE', good})
- const actionCartClear = () => ({type: 'CART_CLEAR'})
- const actionCartChange = (good, count) => ({type: 'CART_CHANGE', good, count})
- 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
- subCategories {
- _id name
- }
- goods {
- _id name price images {
- url
- }
- }
- }
- }`, { q: JSON.stringify([{ _id }]) }))
- store.dispatch(actionRootCats())
- const actionGoodById = (_id) =>
- actionPromise('goodById', gql(`
- query goodById ($good:String) {
- GoodFindOne(query: $good) {
- name
- description
- price
- categories {
- name
- }
- images {
- url
- }
- }
- }`, { good: JSON.stringify([{ _id }]) }))
- store.subscribe(() => {
- const { rootCats } = store.getState().promise
- if (rootCats?.payload) {
- aside.innerHTML = ''
- for (const { _id, name } of rootCats?.payload) {
- const link = document.createElement('a')
- link.href = `#/category/${_id}`
- link.innerText = name
- aside.append(link)
- }
- }
- })
- window.onhashchange = () => {
- const [, route, _id] = location.hash.split('/')
- const routes = {
- category() {
- store.dispatch(actionCatById(_id))
- },
- good() {
- store.dispatch(actionGoodById(_id))
- },
- registration() {
- let navBar = document.getElementById('navBar')
- navBar.style.visibility = 'hidden'
- let pass, log
- aside.innerHTML = ''
- main.innerHTML = `
- <div style="border: 5px solid purple; padding: 10px; background-color: black;">
- <button id="returnBtnReg" style="font-size: small;">⇐ назад</button>
- <input id="logInp" style="font-size: small; display:block; margin:10px" type="text" placeholder="Логин"/>
- <input id="passInp" style="font-size: small; display:block; margin:10px" type="password" placeholder="Пароль"/>
- <small id="errBox" style="display: block; width:fit-content; color: red; margin: 0 auto;"></small>
- <button id="registerBtn" style="font-size: small; display:block; margin:0 auto">РЕГИСТРАЦИЯ</button>
- </div>
- `
- let returnBtn = document.getElementById('returnBtnReg')
- let logInp = document.getElementById('logInp')
- let passInp = document.getElementById('passInp')
- let registerBtn = document.getElementById('registerBtn')
- let errBox = document.getElementById('errBox')
- passInp.oninput = (e) => { pass = e.target.value }
- logInp.oninput = (e) => { log = e.target.value }
- returnBtn.onclick = () => {
- navBar.style.visibility = 'visible'
- history.back()
- }
- registerBtn.onclick = () => {
- console.log('login clicked')
- if (log && pass) {
- (async () => {
- await store.dispatch(actionRegister(log, pass))
- aside.innerHTML = ``
- if (store.getState().promise.registration.payload) {
- errBox.innerText = ''
- main.innerHTML = `
- <h2>Пользователь под логином '${store.getState().promise.registration.payload.login} успешно зарегистрирован!'<h2>
- <button id="returnBtn">⇐ Вернуться к просмотру</button>
- <span>  или  </span>
- <button id="returnBtn">
- <a href="#/login" style="text-decoration: none; color: black;">Авторизоваться</a>
- </button>
- `
- let retBtn = document.getElementById('returnBtn')
- retBtn.onclick = () => {
- navBar.style.visibility = 'visible'
- location.href = "#/category"
- }
- } else {
- errBox.innerText = 'Пользователь с таким логином уже существует'
- }
- })();
- }
- }
- },
- login() {
- let navBar = document.getElementById('navBar')
- navBar.style.visibility = 'hidden'
- let pass, log
- console.log('login')
- aside.innerHTML = ''
- main.innerHTML = `
- <div style="border: 5px solid purple; padding: 10px; background-color: black;">
- <button id="returnBtnLogin" style="font-size: small;">⇐ назад</button>
- <input id="logInp" style="font-size: small; display:block; margin:10px" type="text" placeholder="Логин"/>
- <input id="passInp" style="font-size: small; display:block; margin:10px" type="password" placeholder="Пароль"/>
- <small id="errBox" style="display: block; width:fit-content; color: red; margin: 0 auto;"></small>
- <button id="submitBtn" style="font-size: small; display:block; margin:0 auto">ВОЙТИ</button>
- </div>
- `
- let returnBtn = document.getElementById('returnBtnLogin')
- let logInp = document.getElementById('logInp')
- let passInp = document.getElementById('passInp')
- let submitBtn = document.getElementById('submitBtn')
- let errBox = document.getElementById('errBox')
- passInp.oninput = (e) => { pass = e.target.value }
- logInp.oninput = (e) => { log = e.target.value }
- returnBtn.onclick = () => {
- navBar.style.visibility = 'visible'
- history.back()
- }
- submitBtn.onclick = () => {
- if (log && pass) {
- (async () => {
- await store.dispatch(actionFullLogin(log, pass))
- aside.innerHTML = ``
- if (store.getState().auth.payload) {
- errBox.innerText = ''
- store.dispatch(actionGetOrders())
- } else {
- errBox.innerText = 'Неправильный логин или пароль'
- }
- })();
- }
- }
- },
- cart() {
- aside.innerHTML = ''
- main.innerHTML = `
- <button onclick="history.back()" style="font-size: smaller;">⇐ назад</button>
- <h2>Корзина</h2>
- <h4>Общая цена заказов: <span id="fullPrice"></span></h4>
- <button id="clearCartBtn" style="background-color: firebrick; color: white; font-size: smaller;">Очистить корзину</button>
- <button id="orderAll" style="font-size: small;">Заказать все</button>
- <br>
- <div id="cartContent"></div>
- `
- let cartContent = document.getElementById('cartContent')
- let orderAllBtn = document.getElementById('orderAll')
- if(localStorage.authToken) {
- orderAllBtn.disabled = false
- orderAllBtn.style.backgroundColor = "mediumseagreen"
- orderAllBtn.style.color = 'white'
- } else {
- orderAllBtn.disabled = true
- orderAllBtn.style.backgroundColor = "grey"
- orderAllBtn.style.color = 'whitesmoke'
- }
- orderAllBtn.onclick = () => {
- console.log('orderAll clicked');
- (async () => {
- let cart = store.getState().cart
- await store.dispatch(actionCartClear())
- for(let item in cart) {
- await store.dispatch(actionCreateOrder(cart[item].count, cart[item].good._id))
- drawCart()
- }
- await store.dispatch(actionGetOrders())
- aside.innerHTML = ''
- })()
- }
- let clearCartBtn = document.getElementById('clearCartBtn')
- clearCartBtn.onclick = () => {
- (async () => {
- await store.dispatch(actionCartClear())
- drawCart()
- })()
- }
- function drawCart() {
- let fullPrice = 0
- let cart = store.getState().cart
- cartContent.innerHTML = ``
- if(Object.keys(cart).length > 0) {
- for (let item in cart) {
- let orderPrice = cart[item].good.price * cart[item].count
- fullPrice += orderPrice
- let delOrderBtn = document.createElement('button')
- delOrderBtn.style.backgroundColor = "firebrick"
- delOrderBtn.style.color = "white"
- delOrderBtn.style.fontSize = "smaller"
- delOrderBtn.style.float = "right"
- delOrderBtn.innerText = "Удалить заказ [x]"
- delOrderBtn.onclick = () => {
- (async () => {
- await store.dispatch(actionCartDel(cart[item].good))
- drawCart()
- })()
- }
-
- let card = document.createElement('div')
- card.append(delOrderBtn)
-
- card.insertAdjacentHTML('beforeend', `
- <br>
- <h2>${cart[item].good.name}</h2>
- <div style="
- border:5px solid mediumvioletred;
- background-color: black;
- color: white;
- padding: 10px 5px;"
- >
- <h4>Кол-во: <button class="decBtn">-</button> ${cart[item].count} <button class="addBtn">+</button></h4>
- <h4>Стоимость заказа: <span style="color: gold">${orderPrice}</span></h4>
- <button class="makeOrderBtn" ${localStorage.authToken? 'style="background-color: mediumseagreen; color: white;"' : 'disabled'}>
- ${localStorage.authToken? 'Заказать' : 'Только авторизованные пользователи могут совершать заказы'}
- </button>
- </div>
- <br>
- <img style="border: 1px dashed grey;" src="${backURL}/${cart[item].good.images[0].url}" />
- <br>
- <strong>Цена за ед. товара: ${cart[item].good.price}</strong>
- <br>
- <a href="#/good/${cart[item].good._id}">На страницу товара</a>
- <br>
- `)
- card.style.backgroundColor = "whitesmoke"
- card.style.border = "1px solid black"
- card.style.margin = "10px"
- card.style.padding = "10px"
- card.querySelector('.decBtn').disabled = (cart[item].count <= 1)
- card.querySelector('.decBtn').onclick = () => {
- (async() => {
- await store.dispatch(actionCartChange(cart[item].good, cart[item].count-1))
- drawCart()
- })()
- }
- card.querySelector('.addBtn').onclick = () => {
- (async() => {
- await store.dispatch(actionCartChange(cart[item].good, cart[item].count+1))
- drawCart()
- })()
- }
- card.querySelector('.makeOrderBtn').onclick = () => {
- (async() => {
- await store.dispatch(actionCreateOrder(cart[item].count, cart[item].good._id))
- await store.dispatch(actionCartDel(cart[item].good))
- await store.dispatch(actionGetOrders())
- drawCart()
- })()
- }
- cartContent.append(card)
- }
- }
- aside.innerHTML = ``
-
- let fullPriceSpan = document.getElementById('fullPrice')
- fullPriceSpan.innerText = `${fullPrice}`
- }
- drawCart()
- },
- orders() {
- aside.innerHTML = ''
- main.innerHTML = `
- <button onclick="history.back()" style="font-size: smaller;">⇐ назад</button>
- <h2>Список заказов ${store.getState().auth.payload.sub.login}</h2>
- <br>
- `
- let container = document.createElement('div')
- main.append(container)
- if (store.getState().promise.orders) {
- const payload = store.getState().promise.orders.payload
- if (payload.length > 0) {
- for (let { orderGoods } of payload) {
- for (let item of orderGoods) {
- try {
- let card = document.createElement('div')
- card.insertAdjacentHTML('beforeend', `
- <br>
- <h2>${item.good.name}</h2>
- <div style="border:5px solid mediumvioletred; background-color: black; color: white; padding: 0px 5px;">
- <h4>Кол-во: ${item.count}</h4>
- <h4>Стоимость заказа: ${item.total}</h4>
- </div>
- <br>
- <img style="border: 1px dashed grey;" src="${backURL}/${item.good.images[0].url}" />
- <br>
- <strong>Цена за ед. товара: ${item.price}</strong>
- <br>
- <a href="#/good/${item.good._id}">На страницу товара</a>
- <br>
- `)
- card.style.backgroundColor = "whitesmoke"
- card.style.border = "1px solid black"
- card.style.margin = "10px"
- card.style.padding = "10px"
- container.prepend(card)
- } catch (e) {
- console.log('nulls are ignored')
- }
- }
- }
- }
- }
- }
- }
- if (route in routes)
- routes[route]()
- }
- window.onhashchange()
- //drawing goood list of cathegory
- store.subscribe(() => {
- const { catById } = store.getState().promise
- const [, route, _id] = location.hash.split('/')
- if (catById?.payload && route === 'category') {
- const { name, subCategories } = catById.payload
- main.innerHTML = `<h1>${name}</h1>`
- if (subCategories) {
- console.log('here', subCategories)
- let subCats = document.createElement('div')
- for (let item of subCategories) {
- let link = document.createElement('a')
- link.innerHTML = `${item.name} ⏎`
- link.href = `#/category/${item._id}`
- link.style.margin = '5px'
- link.style.display = 'inline-block'
- link.style.padding = '5px'
- link.style.backgroundColor = 'black'
- link.style.color = 'white'
- subCats.append(link)
- }
- main.append(subCats)
- }
- main.style.padding = "10px"
- if (catById.payload.goods) {
- for (const good of catById.payload.goods) {
- const card = document.createElement('div')
- card.innerHTML = `
- <h2>${good.name}</h2>
- <img style="border: 1px dashed grey;" src="${backURL}/${good.images[0].url}" />
- <br>
- <strong>Price: ${good.price}</strong>
- <br>
- <a href="#/good/${good._id}">На страницу товара</a>
- <br>
- `
- card.style.backgroundColor = "whitesmoke"
- card.style.border = "1px solid black"
- card.style.margin = "10px"
- card.style.padding = "10px"
- main.append(card)
- let cartButton = document.createElement('button')
- cartButton.innerText = 'Добавить в корзину'
- cartButton.style.fontSize = 'smaller'
- cartButton.style.marginTop = '10px'
- cartButton.style.backgroundColor = 'gold'
- card.append(cartButton)
- cartButton.onclick = () => {
- store.dispatch(actionCartAdd(good))
- }
- }
- }
- }
- })
- //item page
- store.subscribe(() => {
- const { goodById } = store.getState().promise
- const [, route, _id] = location.hash.split('/')
- if (goodById?.payload && route === 'good') {
- const { name, categories, images, price, description } = goodById.payload
- main.innerHTML = `
- <div class="card">
- <button onclick="history.back()" style="font-size: smaller;">⇐ назад</button>
- <h1>${name}</h1>
- <h4>${categories[0].name}<h4>
- <br>
- <img style="border: 1px dashed grey;" src="${backURL}/${images[0].url}" />
- <br>
- <strong>Price: ${price}</strong>
- <p>${description}</p>
- </div>
- `
- main.style.padding = "10px"
- }
- })
- //cntrol over login
- store.subscribe(() => {
- const { payload } = store.getState().auth
- const [, route, _id] = location.hash.split('/')
- let logBtn = document.getElementById('logBtn')
- let logLink = document.getElementById('logLink')
- let regLink = document.createElement('a')
- let nameField = document.getElementById('nameField')
- let navBar = document.getElementById('navBar')
- if (!payload) {
- nameField.innerText = ''
- logBtn.style.backgroundColor = 'mediumaquamarine'
- logLink.innerText = 'Войти'
- logBtn.onclick = () => {/*killing onclick login action*/ }
- if (!document.getElementById('regLink')) {
- regLink.className = 'linkDeco'
- regLink.innerText = 'Регистрация'
- regLink.href = `#/registration`
- regLink.style.backgroundColor = 'mediumvioletred'
- regLink.id = 'regLink'
- logBtn.after(regLink)
- }
- try {
- let orderBtn = document.getElementById('orderBtn')
- navBar.removeChild(orderBtn)
- } catch (e) { }
- } else if (payload && route === 'login') {
- aside.innerHTML = ``
- main.innerHTML = `
- <h2>Вы успешно вошли под логином пользователя '${payload.sub.login}'<h2>
- <button id="returnBtn">⇐ Вернуться к просмотру</button>
- `
- let retBtn = document.getElementById('returnBtn')
- retBtn.onclick = () => {
- navBar.style.visibility = 'visible'
- location.href = "#/category/5dc49f4d5df9d670df48cc64"
- }
- } else {
- nameField.innerText = payload.sub.login
- logBtn.style.backgroundColor = 'firebrick'
- logBtn.innerHTML = 'Выйти'
- try {
- let _regLink = document.getElementById('regLink')
- _regLink.parentElement.removeChild(_regLink)
- } catch (e) { }
- if (!document.getElementById('orderBtn')) {
- let orderBtn = document.createElement('a')
- orderBtn.href = '#/orders'
- orderBtn.id = 'orderBtn'
- orderBtn.innerText = 'список заказов'
- orderBtn.className = 'linkDeco'
- navBar.append(orderBtn)
- }
- logBtn.onclick = () => {
- nameField.innerText = ''
- logBtn.style.backgroundColor = 'mediumaquamarine'
- logBtn.innerHTML = `<a href="#/login" style="color: white; text-decoration: none;" id="logLink">Войти</a>`
- store.dispatch(actionAuthLogout())
- store.dispatch(actionGetOrders())
- }
- }
- })
- //count cart items
- store.subscribe(() => {
- const { cart } = store.getState()
- let items = 0
- for (let item in cart) {
- items += cart[item].count
- }
- let itemCount = document.getElementById('itemCount')
- itemCount.innerText = items
- })
- window.onload = () => {
- location.href = "#/category/5dc49f4d5df9d670df48cc64"
- store.dispatch(actionGetOrders())
- }
|