123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655 |
- // createstore
- 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,
- dispatch,
- subscribe
- }
- }
- // ----------------------------------------------------------
- // decode token
- function jwtDecode(token) {
- try {
- return JSON.parse(atob(token.split('.')[1]))
- }
- catch (e) {
- }
- }
- // ----------------------------------------------------------
- // reducers
- function authReducer(state = {}, { type, token }) { //authorise reducer
- if (state === undefined) {
- if (localStorage.authToken) {
- type = "AUTH_LOGIN";
- token = localStorage.authToken;
- }
- }
- if (type === 'AUTH_LOGIN') { //то мы логинимся
- const payload = jwtDecode(token)
- if (payload) {
- return {
- token,
- payload
- }
- }
- }
- if (type === 'AUTH_LOGOUT') { //мы разлогиниваемся
- return {}
- }
- return state
- }
- function promiseReducer(state = {}, { type, name, status, payload, error }) { //promise reducer
- if (type === 'PROMISE') {
- return {
- ...state,
- [name]: { status, payload, error }
- }
- }
- return state
- }
- function cartReducer(state = {}, { type, good, count = 1 }) {
- if (type === 'CART_ADD') {
- return {
- ...state,
- [good._id]: { count: (state[good._id]?.count || 0) + count, good: good }
- }
- }
- if (type === 'CART_CHANGE') {
- return {
- ...state,
- [good._id]: { count, good }
- }
- }
- if (type === 'CART_DELETE') {
- delete state[good._id]
- return {
- ...state,
- }
- }
- 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' })
- 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({ promise: promiseReducer, auth: authReducer, cart: cartReducer }))
- // store.subscribe(() => console.log(store.getState()))
- // ----------------------------------------------------------
- // GQL
- 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.data) {
- return Object.values(data.data)[0]
- }
- else throw new Error(JSON.stringify(data.errors))
- })
- const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/'
- const gql = getGQL(backendURL + 'graphql')
- // ----------------------------------------------------------
- // promises
- const actionPromise = (name, promise) =>
- async dispatch => {
- dispatch(actionPending(name))
- try {
- let payload = await promise
- dispatch(actionFulfilled(name, payload))
- return payload
- }
- catch (e) {
- dispatch(actionRejected(name, e))
- }
- }
- 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 })
- // ----------------------------------------------------------
- // actions
- const actionFullRegister = (login, password) =>
- actionPromise('fullRegister', gql(`mutation UserUpsert($login: String, $password: String){UserUpsert(user: {login:$login,password:$password}){_id}}`, { login: login, password: password }))
- const actionAuthLogin = (token) =>
- (dispatch, getState) => {
- const oldState = getState()
- dispatch({ type: 'AUTH_LOGIN', token })
- const newState = getState()
- if (oldState !== newState)
- localStorage.authToken = token
- }
- const actionAuthLogout = () =>
- dispatch => {
- dispatch({ type: 'AUTH_LOGOUT' })
- localStorage.removeItem('authToken')
- }
- const actionFullLogin = (login, password) =>
- actionPromise('fullLogin', gql(`query login($login:String,$password:String){login(login:$login,password:$password)}`, { login: login, password: password }))
- const orderFind = () =>
- actionPromise('orderFind', gql(`query orderFind{
- OrderFind(query: "[{}]"){
- _id createdAt total orderGoods {_id price count good{name price images{url}}}
- }
- }`, { q: JSON.stringify([{}]) }))
- const actionAddOrder = (cart) =>
- actionPromise('actionAddOrder', gql(`mutation newOrder($cart: [OrderGoodInput])
- {OrderUpsert(order: {orderGoods: $cart})
- {_id total}}`, { cart: cart }))
- 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 }]) }))
- store.dispatch(actionRootCats())
- // ----------------------------------------------------------
- // subscribe
- store.subscribe(() => {
- const { rootCats } = (store.getState()).promise
- if (rootCats?.payload) {
- cat.innerHTML = `<li class="list-group-item"><b>Categories</b></li>`
- for (const { _id, name } of rootCats?.payload) {
- const categories = document.createElement('li')
- categories.innerHTML = `<a href='#/category/${_id}'>${name}</a>`
- categories.style = ' padding-left: 30px ; '
- cat.append(categories)
- }
- }
- else if (!rootCats) {
- cat.innerHTML = '<img src = img/preloader.gif>'
- }
- })
- store.subscribe(() => {
- const { catById } = (store.getState()).promise
- const [, route, _id] = location.hash.split('/')
- if (catById?.payload && route === 'category') {
- main.innerHTML = ``
- const { name } = catById.payload
- const card = document.createElement('div')
- card.id = 'sub-card'
- card.innerHTML = `<h2 id="cat-name"><b>${name}</b></h2><br>`
- const backPart = document.createElement('div')
- backPart.id = 'back'
- const backBtn = document.createElement('button')
- backBtn.setAttribute('type', 'button');
- backBtn.addEventListener('click', () => {
- history.back();
- });
- backBtn.innerText = '⬅'
- backPart.appendChild(backBtn)
- if (catById.payload.subCategories) {
- for (const { _id, name } of catById.payload?.subCategories) {
- card.innerHTML += `<a href='#/category/${_id}' class='subcategories'>${name}</a>`
- }
- }
- // card.append(backPart)
- main.append(card, backPart)
- for (const { _id, name, price, images } of catById.payload?.goods) {
- const card = document.createElement('div')
- card.id = 'card'
- card.innerHTML = `<h5><b>${name}</b></h5>
- <div class='card-img'><img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}"/></div><br>
- <h2 style='color: green; text-align:center'>Price: $${price}</h2><br><br>
- <a class="" style="width: 100%;" href='#/good/${_id}'>More details -> </a><br><br>`
- let button = document.createElement('button')
- button.innerText = 'BUY'
- button.className = 'buy-btn'
- button.setAttribute('type', 'button');
- button.onclick = async () => {
- await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images }))
- console.log('hi')
- }
- card.append(button)
- main.append(card)
- }
- }
- })
- store.subscribe(() => {
- const { goodById } = (store.getState()).promise
- const [, route, _id] = location.hash.split('/')
- if (goodById?.payload && route === 'good') {
- const { _id, name, description, images, price } = goodById.payload
- main.innerHTML = `<h1>${name}</h1>`
- const card = document.createElement('div')
- card.id = 'desc-card'
- const backPart = document.createElement('div')
- backPart.id = 'back'
- const backBtn = document.createElement('button')
- backBtn.setAttribute('type', 'button');
- backBtn.addEventListener('click', () => {
- history.back();
- });
- backBtn.innerText = '⬅'
- backPart.appendChild(backBtn)
- let block = document.createElement('div')
- block.id = 'price'
- block.innerHTML = `<h2>Price: <b class='price'>$${price}</b></h2>`
- let button = document.createElement('button')
- button.innerText = 'BUY'
- button.className = 'buy-btn'
- button.setAttribute('type', 'button');
- button.style = 'height:80px'
- button.onclick = async () => {
- await store.dispatch(actionCartAdd({ _id: _id, name: name, price: price, images: images }))
- console.log('hi')
- }
- card.innerHTML = `<img src="http://shop-roles.node.ed.asmer.org.ua/${images[0].url}" /><br><br>`
- card.append(block)
- card.innerHTML += `<p><b>Description:</b> ${description}</p>`
- main.append(backPart, card, button)
- }
- })
- store.subscribe(() => {
- const { orderFind } = (store.getState()).promise
- const [, route, _id] = location.hash.split('/')
- if (orderFind?.payload && route === 'orderFind') {
- main.innerHTML = '<h1>ORDER HISTORY</h1>'
- for (const { _id, createdAt, total, orderGoods } of orderFind.payload.reverse()) {
- const card = document.createElement('div')
- card.className = 'order-card'
- card.innerHTML = `<h3>Order: ${createdAt}</h3>`
- for (const { count, good } of orderGoods) {
- const divGood = document.createElement('div')
- divGood.style = "display:flex;margin-bottom: 20px;"
- divGood.innerHTML += `<div><b>${good.name}</b><br> Price: <b>$${good.price}</b><br> Amount: <b>${count} pt</b></b></div><img style="max-width: 80px;margin-right: 20px;display: block;margin-left: auto;" src="http://shop-roles.node.ed.asmer.org.ua/${good.images[0].url}"/><br><br>`
- card.append(divGood)
- }
- card.innerHTML += 'Date: <b>' + new Date(+createdAt).toLocaleString().replace(/\//g, '.') + '</b>'
- card.innerHTML += `<br><h2>Total: <b style="color:green;">$${total}</b></h2>`
- main.append(card)
- }
- }
- })
- // ----------------------------------------------------------
- // window
- function display() {
- let token = localStorage.authToken
- if (token) {
- form_yes.style.display = 'flex'
- form_no.style.display = 'none'
- UserNick.innerText = JSON.parse(window.atob(localStorage.authToken.split('.')[1])).sub.login
- } else {
- form_yes.style.display = 'none'
- form_no.style.display = 'flex'
- }
- }
- display()
- window.onhashchange = () => {
- const [, route, _id] = location.hash.split('/')
- const routes = {
- category() {
- store.dispatch(actionCatById(_id))
- },
- good() {
- store.dispatch(actionGoodById(_id))
- },
- login() {
- main.innerHTML = ''
- let form = document.createElement('div')
- let div1 = document.createElement('div')
- let div2 = document.createElement('div')
- div1.innerHTML = `<h1>LOGIN</h1>`
- let loginInput = document.createElement('input')
- loginInput.placeholder = 'Type your username'
- loginInput.name = 'login'
- div1.style.display = 'flex'
- div1.style.flexDirection = 'column'
- let passwordInput = document.createElement('input')
- passwordInput.placeholder = 'Type your password'
- passwordInput.name = 'password'
- passwordInput.type = 'password';
- div1.append(loginInput)
- div2.append(passwordInput)
- let button = document.createElement('button')
- button.innerText = "LOGIN"
- button.id = 'login-btn'
- button.setAttribute('type', 'button');
- button.onclick = async () => {
- let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))
- let token = await tokenPromise()
- if (token !== null) {
- store.dispatch(actionAuthLogin(token))
- display()
- document.location.href = "#/orderFind";
- }
- else {
- loginInput.value = ''
- passwordInput.value = ''
- alert("Incorrect username or password.")
- store.dispatch(actionAuthLogout())
- }
- }
- form.append(div1, div2, button)
- main.append(form)
- },
- register() {
- main.innerHTML = ''
- let form = document.createElement('div')
- let div1 = document.createElement('div')
- let div2 = document.createElement('div')
- div1.innerHTML += `<h1>REGISTER</h1>`
- let loginInput = document.createElement('input')
- loginInput.placeholder = "Type your username"
- div1.append(loginInput)
- let passwordInput = document.createElement('input')
- passwordInput.placeholder = "Type your password"
- passwordInput.type = 'password'
- div2.append(passwordInput)
- let button = document.createElement('button')
- button.innerText = "CREATE ACCOUNT"
- button.id = 'reg-btn'
- button.setAttribute('type', 'button');
- let textAlert = document.createElement('div')
- let textAlert2 = document.createElement('div')
- let putInText = "Username and password required!"
- let userAlready = "An account with this username already exist!"
- textAlert.append(userAlready)
- textAlert2.append(putInText)
- textAlert2.style = 'display : none; color : red'
- textAlert.style = 'display : none; color : red'
- button.onclick = async () => {
- let register = await store.dispatch(actionFullRegister(loginInput.value, passwordInput.value))
- let tokenPromise = async () => await store.dispatch(actionFullLogin(loginInput.value, passwordInput.value))
- if (loginInput.value == '' || passwordInput.value == '') {
- textAlert2.style.display = 'block'
- } else {
- if (register !== null) {
- let token = await tokenPromise()
- store.dispatch(actionAuthLogin(token))
- // console.log(token)
- display()
- document.location.href = "#/orderFind";
- } else {
- textAlert.style.display = 'block'
- textAlert2.style.display = 'none'
- }
- }
- }
- form.append(div1, div2, button)
- form.append(textAlert, textAlert2)
- main.append(form)
- },
- orderFind() {
- store.dispatch(orderFind())
- },
- cart() {
- main.innerHTML = '<h1>CART</h1>'
- for (const [_id, obj] of Object.entries(store.getState().cart)) {
- const card = document.createElement('div')
- card.style = 'width: 33.33%;border-style: groove;border-color: #ced4da17;padding: 10px;border-radius: 10px;margin: 5px;display: flex; flex-direction: column ; align-items: center ; justify-content: space-between'
- const { count, good } = obj
- card.innerHTML += `Products: <b>${good.name}</b> <br><img src="http://shop-roles.node.ed.asmer.org.ua/${good.images[0].url}" style="width: 100px"/> <br> Цена: <b>$${good.price}</b><br><br>`
- const calculation = document.createElement('div')
- const buttonAdd = document.createElement('button')
- buttonAdd.innerHTML = '+'
- buttonAdd.setAttribute('type', 'button');
- buttonAdd.onclick = async () => {
- inputCount.value = +inputCount.value + 1
- await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value))
- cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
- }
- calculation.append(buttonAdd)
- const inputCount = document.createElement('input')
- inputCount.value = +count
- inputCount.disabled = 'disabled'
- inputCount.className = 'inputCount'
- calculation.append(inputCount)
- const buttonLess = document.createElement('button')
- buttonLess.innerHTML = '-'
- buttonLess.setAttribute('type', 'button');
- buttonLess.onclick = async () => {
- if ((+inputCount.value) > 1) {
- inputCount.value = +inputCount.value - 1
- await store.dispatch(actionCartChange({ _id: _id, name: good.name, price: good.price, images: good.images }, +inputCount.value))
- cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
- }
- }
- calculation.append(buttonLess)
- const buttonDelete = document.createElement('button')
- buttonDelete.innerText = 'Delete'
- buttonDelete.className = 'buttonDelete'
- buttonDelete.setAttribute('type', 'button');
- buttonDelete.onclick = async () => {
- await store.dispatch(actionCartDelete({ _id: _id, name: good.name, price: good.price, images: good.images }))
- card.style.display = 'none'
- cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2><br>`
- }
- card.append(calculation)
- card.append(buttonDelete)
- main.append(card)
- }
- const cardTotalDiv = document.createElement('div')
- cardTotalDiv.id = 'total'
- const cardTotal = document.createElement('div')
- cardTotal.innerHTML = `<br><h2>Total: <b style="color:green;">$${goodPrice()}</b></h2>`
- cardTotalDiv.append(cardTotal)
- let cartAlert = document.createElement('div')
- cartAlert.innerHTML = `<h2 style='color:orange;'>Your cart seems empty 😟</h2>`
- cartAlert.style.display = 'none'
- cartAlert.id = 'cart-alert'
- if (localStorage.authToken != '') {
- const button = document.createElement('button')
- button.innerHTML += '<strong>ORDER</strong>'
- button.setAttribute('type', 'button');
- if(goodPrice() != 0){
- button.onclick = async () => {
- await store.dispatch(actionAddOrder(Object.entries(store.getState().cart).map(([_id, count]) => ({ count: count.count, good: { _id } }))));
- await store.dispatch(actionCartClear());
- document.location.href = "#/orderFind";
- }
- }
- else{
- cartAlert.style.display = 'flex'
- }
- // button.className = 'btn btn-primary'
- cardTotalDiv.append(button)
- }
- main.append(cardTotalDiv, cartAlert)
- }
- }
- if (route in routes) {
- routes[route]()
- }
- }
- window.onhashchange()
- function localStoredReducer(reducer, localStorageKey) {
- function wrapperReducer(state, action) {
- if (state === undefined) { //если загрузка сайта
- try {
- return JSON.parse(localStorage[localStorageKey]) //пытаемся распарсить сохраненный
- //в localStorage state и подсунуть его вместо результата редьюсера
- }
- catch (e) { } //если распарсить не выйдет, то код пойдет как обычно:
- }
- const newState = reducer(state, action)
- localStorage.setItem(localStorageKey, JSON.stringify(newState)) //сохраняем состояние в localStorage
- return newState
- }
- return wrapperReducer
- }
- window.onhashchange()
- function goodPrice() {
- return Object.entries(store.getState().cart).map(i => x += i[1].count * i[1].good.price, x = 0).reverse()[0] || 0
- }
|