|
@@ -0,0 +1,470 @@
|
|
|
|
+function createStore(reducer){
|
|
|
|
+ let state = reducer(undefined, {})
|
|
|
|
+ let cbs = []
|
|
|
|
+ function dispatch(action){
|
|
|
|
+ if (typeof action === 'function'){
|
|
|
|
+ return action(dispatch)
|
|
|
|
+ }
|
|
|
|
+ const newState = reducer(state, action)
|
|
|
|
+ if (state !== newState){
|
|
|
|
+ state = newState
|
|
|
|
+ cbs.forEach(cb => cb())
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return {
|
|
|
|
+ dispatch,
|
|
|
|
+ subscribe(cb){
|
|
|
|
+ cbs.push(cb)
|
|
|
|
+ return () => cbs = cbs.filter(c => c !== cb)
|
|
|
|
+ },
|
|
|
|
+ getState(){
|
|
|
|
+ return state
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function combineReducers(reducers={cart: cartReducer, promise: promiseReducer, auth: authReducer}){
|
|
|
|
+ return (state={}, action) => {
|
|
|
|
+ let newState = {}
|
|
|
|
+ for (let key in reducers) {
|
|
|
|
+ let newSubState = reducers[key](state[key], action)
|
|
|
|
+ if(newSubState !== state[key]) {
|
|
|
|
+ newState[key] = newSubState
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (Object.keys(newState).length) {
|
|
|
|
+ return {...state, ...newState}
|
|
|
|
+ } else {
|
|
|
|
+ return state
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+//promise
|
|
|
|
+function promiseReducer(state={}, {type, status, payload, error, name}){
|
|
|
|
+ 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 delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
|
|
|
|
+
|
|
|
|
+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 => {
|
|
|
|
+ return function(query, variables={}) {
|
|
|
|
+ return fetch(url,
|
|
|
|
+ {
|
|
|
|
+ method: "POST",
|
|
|
|
+ headers:
|
|
|
|
+ {"Content-Type": "application/json",
|
|
|
|
+ ...(localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {})
|
|
|
|
+ },
|
|
|
|
+ body: JSON.stringify({query, variables})
|
|
|
|
+ }).then(resp => resp.json())
|
|
|
|
+ .then(data => {
|
|
|
|
+ if ("errors" in data) {
|
|
|
|
+ let error = new Error('ашипка, угадывай што не так')
|
|
|
|
+ throw error
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ return data.data[Object.keys(variables)[0]]
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+let shopGQL = getGQL('http://shop-roles.asmer.fs.a-level.com.ua/graphql')
|
|
|
|
+
|
|
|
|
+const goodById = goodId => {
|
|
|
|
+ let id = `[{"_id":"${goodId}"}]`
|
|
|
|
+ return shopGQL(`
|
|
|
|
+ query good($id:String){
|
|
|
|
+ GoodFindOne(query: $id) {
|
|
|
|
+ _id name description price images {
|
|
|
|
+ _id text url
|
|
|
|
+ }
|
|
|
|
+ categories {
|
|
|
|
+ _id name
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`, {GoodFindOne: '', id })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionGoodById = id =>
|
|
|
|
+ actionPromise('goodById', goodById(id))
|
|
|
|
+
|
|
|
|
+const actionRootCategories = () =>
|
|
|
|
+ actionPromise('rootCategories', shopGQL(`
|
|
|
|
+ query cats($query:String){
|
|
|
|
+ CategoryFind(query:$query){
|
|
|
|
+ _id name
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ `, {CategoryFind:'', query: JSON.stringify([{parent:null}])}))
|
|
|
|
+
|
|
|
|
+const actionCategoryById = (_id) =>
|
|
|
|
+ actionPromise('catById', shopGQL(`
|
|
|
|
+ query catById($query:String){
|
|
|
|
+ CategoryFindOne(query:$query){
|
|
|
|
+ _id name goods{
|
|
|
|
+ _id name price description images{
|
|
|
|
+ url
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`, { CategoryFindOne: '', query: JSON.stringify([{ _id }]) }))
|
|
|
|
+
|
|
|
|
+const actionAddOrder = (cart) => {
|
|
|
|
+ let string = '['
|
|
|
|
+ for (let goodId in cart) {
|
|
|
|
+ string += `{good: {_id: "${goodId}"}, count: ${cart[goodId].count}},`
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ string = string.slice(0, string.length - 1)
|
|
|
|
+ string += ']'
|
|
|
|
+ store.dispatch(actionPromise('orderAdd', shopGQL(`
|
|
|
|
+ mutation {
|
|
|
|
+ OrderUpsert(order: {
|
|
|
|
+ orderGoods: ${string}
|
|
|
|
+ }) {
|
|
|
|
+ _id total
|
|
|
|
+ }
|
|
|
|
+ }`, {OrderUpsert: ''})))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+//cart
|
|
|
|
+function cartReducer(state={}, {type, good, price, count=1}) {
|
|
|
|
+ if (Object.keys(state).length === 0 && localStorage.cart.length > 10) {
|
|
|
|
+ let newState = JSON.parse(localStorage.cart)
|
|
|
|
+ return newState
|
|
|
|
+ }
|
|
|
|
+ const types = {
|
|
|
|
+ CART_ADD() {
|
|
|
|
+ let newState = {
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: {count: (state[good._id]?.count || 0) + count, good: {id: good._id, name: good.name}, price}
|
|
|
|
+ }
|
|
|
|
+ localStorage.cart = JSON.stringify(newState)
|
|
|
|
+ return newState
|
|
|
|
+ },
|
|
|
|
+ CART_REMOVE() {
|
|
|
|
+ let {[good._id]:poh, ...newState} = state
|
|
|
|
+ localStorage.cart = JSON.stringify(newState)
|
|
|
|
+ return newState
|
|
|
|
+ // let newState = {...state}
|
|
|
|
+ // delete newState[good._id]
|
|
|
|
+ // return newState
|
|
|
|
+ },
|
|
|
|
+ CART_CLEAR() {
|
|
|
|
+ let newState = {}
|
|
|
|
+ localStorage.cart = JSON.stringify(newState)
|
|
|
|
+ return newState
|
|
|
|
+ },
|
|
|
|
+ CART_SET() {
|
|
|
|
+ let newState = {
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: {count, good: {id: good._id, name: good.name}, price}
|
|
|
|
+ }
|
|
|
|
+ localStorage.cart = JSON.stringify(newState)
|
|
|
|
+ return newState
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (type in types) {
|
|
|
|
+ return types[type]()
|
|
|
|
+ }
|
|
|
|
+ return state
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionCartAdd = (_id, name, price, count) => ({type: 'CART_ADD', good: {_id, name}, price, count})
|
|
|
|
+const actionCartRemove = (_id, name) => ({type: 'CART_REMOVE', good: {_id, name}})
|
|
|
|
+const actionCartSet = (_id, name, price, count) => ({type: 'CART_SET', good: {_id, name}, price, count})
|
|
|
|
+const actionCartClear = () => ({type: 'CART_CLEAR'})
|
|
|
|
+
|
|
|
|
+//auth
|
|
|
|
+const jwt_decode = (jwt) => {
|
|
|
|
+ let payload = jwt.split('.')
|
|
|
|
+ return JSON.parse(atob(payload[1]))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function authReducer(state, action={}){ //....
|
|
|
|
+ if (state === undefined){
|
|
|
|
+ //добавить в action token из localStorage, и проимитировать LOGIN (action.type = 'LOGIN')
|
|
|
|
+ if (localStorage.authToken) {
|
|
|
|
+ action.jwt = localStorage.authToken
|
|
|
|
+ return {token: action.jwt, payload: jwt_decode(action.jwt)}
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (action.type === 'LOGIN'){
|
|
|
|
+ console.log('ЛОГИН')
|
|
|
|
+ //+localStorage
|
|
|
|
+ //jwt_decode
|
|
|
|
+ return {token: action.jwt, payload: jwt_decode(action.jwt)}
|
|
|
|
+ }
|
|
|
|
+ if (action.type === 'LOGOUT'){
|
|
|
|
+ console.log('ЛОГАУТ')
|
|
|
|
+ //-localStorage
|
|
|
|
+ //вернуть пустой объект
|
|
|
|
+ return {}
|
|
|
|
+ }
|
|
|
|
+ if (action.type === 'LOGGING_IN'){
|
|
|
|
+ return {loginPageHello: true}
|
|
|
|
+ }
|
|
|
|
+ return state
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionLogin = (jwt) => ({type: 'LOGIN', jwt})
|
|
|
|
+const thunkLogin = (login, password) => {
|
|
|
|
+ return (dispatch) => {
|
|
|
|
+ shopGQL(`query login($login:String, $password: String) {login(login:$login, password:$password)}`, { login, password })
|
|
|
|
+ .then(jwt => {
|
|
|
|
+ if (jwt) {
|
|
|
|
+ localStorage.authToken = jwt
|
|
|
|
+ dispatch(actionLogin(jwt))
|
|
|
|
+ } else {
|
|
|
|
+ throw new Error('wrong')
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionLogout = () => ({type: 'LOGOUT'})
|
|
|
|
+const thunkLogout = () => {
|
|
|
|
+ return (dispatch) => {
|
|
|
|
+ localStorage.authToken = ''
|
|
|
|
+ localStorage.cart = ''
|
|
|
|
+ store.dispatch(actionCartClear())
|
|
|
|
+ dispatch(actionLogout())
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionLoggingIn = () => ({type: 'LOGGING_IN'})
|
|
|
|
+
|
|
|
|
+//store
|
|
|
|
+const store = createStore(combineReducers({cart: cartReducer, promise: promiseReducer, auth: authReducer}))
|
|
|
|
+const unsubscribe1 = store.subscribe(() => console.log(store.getState()))
|
|
|
|
+store.dispatch(actionRootCategories())
|
|
|
|
+
|
|
|
|
+window.onhashchange = () => {
|
|
|
|
+ let {1: route, 2:id} = location.hash.split('/')
|
|
|
|
+ if (route === 'categories'){
|
|
|
|
+ store.dispatch(actionCategoryById(id))
|
|
|
|
+ }
|
|
|
|
+ if (route === 'good'){
|
|
|
|
+ store.dispatch(actionGoodById(id))
|
|
|
|
+ }
|
|
|
|
+ if (route === 'login'){
|
|
|
|
+ store.dispatch(actionLoggingIn())
|
|
|
|
+ }
|
|
|
|
+ if (route === 'cart'){
|
|
|
|
+ drawCart()
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function drawMainMenu(){
|
|
|
|
+ let cats = store.getState().promise.rootCategories.payload
|
|
|
|
+ if (cats){ //каждый раз дорисовываются в body
|
|
|
|
+ aside.innerText = ''
|
|
|
|
+ for (let {_id, name} of cats){
|
|
|
|
+ let catA = document.createElement('a')
|
|
|
|
+ catA.href = `#/categories/${_id}`
|
|
|
|
+ catA.innerText = name
|
|
|
|
+ aside.append(catA)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function drawHeader() {
|
|
|
|
+ login.innerHTML = (store.getState().auth?.payload ? `${store.getState().auth.payload.sub.login} | <a id='logout' href="#/login">Log out</a>` : '<a href="#/login">Log in</a>')
|
|
|
|
+ if (document.querySelector('#logout')) {
|
|
|
|
+ logout.onclick = () => {
|
|
|
|
+ store.dispatch(thunkLogout())
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function drawCart() {
|
|
|
|
+ let cart = store.getState().cart
|
|
|
|
+ if (!localStorage.authToken) {
|
|
|
|
+ main.innerText = 'Залогинтесь плез'
|
|
|
|
+ } else if (!Object.keys(cart).length) {
|
|
|
|
+ main.innerText = 'Корзина пуста'
|
|
|
|
+ } else {
|
|
|
|
+ main.innerText = 'Ваша корзина: '
|
|
|
|
+ for (let goodId in cart) {
|
|
|
|
+ let {good: {id, name}, price, count} = cart[goodId]
|
|
|
|
+ let goodContainer = document.createElement('div')
|
|
|
|
+ goodContainer.classList.add('good-container')
|
|
|
|
+ let goodName = document.createElement('div')
|
|
|
|
+ goodName.innerText = name
|
|
|
|
+ let goodPrice = document.createElement('div')
|
|
|
|
+ goodPrice.innerText = 'Стоимость: ' + price
|
|
|
|
+ let goodCount = document.createElement('input')
|
|
|
|
+ goodCount.type = 'number'
|
|
|
|
+ goodCount.value = count
|
|
|
|
+ goodCount.onchange = () => {
|
|
|
|
+ store.dispatch(actionCartSet(id, name, price, (goodCount.value > 0 ? +goodCount.value : 1)))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let removeBtn = document.createElement('button')
|
|
|
|
+ removeBtn.innerText = 'Удалить товар'
|
|
|
|
+ removeBtn.onclick = () => {
|
|
|
|
+ store.dispatch(actionCartRemove (id, name))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ goodContainer.append(goodName, goodPrice, goodCount, removeBtn)
|
|
|
|
+ main.append(goodContainer)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let price = 0
|
|
|
|
+ for (let goodId in cart) {
|
|
|
|
+ price += cart[goodId].price * cart[goodId].count
|
|
|
|
+ }
|
|
|
|
+ let totalPriceContainer = document.createElement('div')
|
|
|
|
+ totalPriceContainer.innerText = 'Общая стоимость: ' + price
|
|
|
|
+ main.append(totalPriceContainer)
|
|
|
|
+
|
|
|
|
+ let setOrderBtn = document.createElement('button')
|
|
|
|
+ setOrderBtn.innerText = 'Оформить заказ'
|
|
|
|
+ setOrderBtn.onclick = () => {
|
|
|
|
+ actionAddOrder(store.getState().cart)
|
|
|
|
+ }
|
|
|
|
+ main.append(setOrderBtn)
|
|
|
|
+
|
|
|
|
+ let clearBtn = document.createElement('button')
|
|
|
|
+ clearBtn.innerText = 'Очистить корзину'
|
|
|
|
+ clearBtn.onclick = () => {
|
|
|
|
+ store.dispatch(actionCartClear())
|
|
|
|
+ }
|
|
|
|
+ main.append(clearBtn)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function drawOrderSuccessful() {
|
|
|
|
+ if (store.getState().promise.orderAdd?.status === 'RESOLVED') {
|
|
|
|
+ let order = store.getState().promise.orderAdd.payload
|
|
|
|
+ main.innerText = 'Заказ оформился, всё круто'
|
|
|
|
+
|
|
|
|
+ let orderInfo = document.createElement('div')
|
|
|
|
+ orderInfo.innerText = `Номер заказа: ${order._id}. Стоимость: ${order.total}`
|
|
|
|
+ main.append(orderInfo)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+store.subscribe(drawMainMenu)
|
|
|
|
+store.subscribe(drawHeader)
|
|
|
|
+store.subscribe(() => {
|
|
|
|
+ const {1: route, 2:id} = location.hash.split('/')
|
|
|
|
+ if (route === 'categories'){
|
|
|
|
+ const catById = store.getState().promise.catById?.payload
|
|
|
|
+ if (catById){
|
|
|
|
+ main.innerText = ''
|
|
|
|
+ let categoryName = document.createElement('div')
|
|
|
|
+ categoryName.innerText = catById.name
|
|
|
|
+ categoryName.style.fontSize = '25px'
|
|
|
|
+ categoryName.style.fontWeight = 'bold'
|
|
|
|
+ main.append(categoryName)
|
|
|
|
+ for (let {_id, name, price} of catById.goods){
|
|
|
|
+ let good = document.createElement('a')
|
|
|
|
+ good.href = `#/good/${_id}`
|
|
|
|
+ good.innerText = name
|
|
|
|
+
|
|
|
|
+ let btn = document.createElement('button')
|
|
|
|
+ btn.onclick = () => {
|
|
|
|
+ if (!localStorage.authToken) {
|
|
|
|
+ main.innerText = 'Залогинтесь плез'
|
|
|
|
+ } else {
|
|
|
|
+ store.dispatch(actionCartAdd(_id, name, price))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ btn.style.cursor = 'pointer'
|
|
|
|
+ btn.innerText = 'купыть'
|
|
|
|
+ main.append(good, btn)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (route === 'good'){
|
|
|
|
+ const goodById = store.getState().promise.goodById?.payload
|
|
|
|
+ if (goodById){
|
|
|
|
+ main.innerText = ''
|
|
|
|
+ let {name, description, price, _id} = goodById
|
|
|
|
+ let goodName = document.createElement('div')
|
|
|
|
+ goodName.innerText = name
|
|
|
|
+ goodName.style.fontSize = '35px'
|
|
|
|
+ goodName.style.fontWeight = 'bold'
|
|
|
|
+ goodName.style.marginBottom = '25px'
|
|
|
|
+
|
|
|
|
+ let goodDescription = document.createElement('div')
|
|
|
|
+ goodDescription.innerText = description
|
|
|
|
+ goodDescription.style.marginBottom = '25px'
|
|
|
|
+
|
|
|
|
+ let goodPrice = document.createElement('div')
|
|
|
|
+ goodPrice.innerText = 'Цена: ' + price
|
|
|
|
+ goodPrice.style.marginBottom = '5px'
|
|
|
|
+
|
|
|
|
+ let btn = document.createElement('button')
|
|
|
|
+ btn.onclick = () => {
|
|
|
|
+ if (!localStorage.authToken) {
|
|
|
|
+ main.innerText = 'Залогинтесь плез'
|
|
|
|
+ } else {
|
|
|
|
+ store.dispatch(actionCartAdd(_id, name, price))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ btn.style.cursor = 'pointer'
|
|
|
|
+ btn.innerText = 'купыть'
|
|
|
|
+
|
|
|
|
+ main.append(goodName, goodDescription, goodPrice, btn)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (route === 'login') {
|
|
|
|
+ main.innerText = ''
|
|
|
|
+
|
|
|
|
+ let inputsContainer = document.createElement('div')
|
|
|
|
+ inputsContainer.id = 'inputs'
|
|
|
|
+ let loginInput = document.createElement('input')
|
|
|
|
+ loginInput.type = 'text'
|
|
|
|
+ let loginLabel = document.createElement('span')
|
|
|
|
+ loginLabel.innerText = 'Login:'
|
|
|
|
+ let passwordInput = document.createElement('input')
|
|
|
|
+ passwordInput.type = 'password'
|
|
|
|
+ let passwordLabel = document.createElement('span')
|
|
|
|
+ passwordLabel.innerText = 'Password:'
|
|
|
|
+ let button = document.createElement('button')
|
|
|
|
+ button.innerText = 'log in cyka'
|
|
|
|
+ button.onclick = () => {
|
|
|
|
+ if (loginInput.value && passwordInput.value){
|
|
|
|
+ store.dispatch(thunkLogin(loginInput.value, passwordInput.value))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ inputsContainer.append(loginLabel, loginInput, passwordLabel, passwordInput, button)
|
|
|
|
+ main.append(inputsContainer)
|
|
|
|
+
|
|
|
|
+ if (store.getState().auth?.payload) {
|
|
|
|
+ button.disabled = true
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (route === 'cart') {
|
|
|
|
+ drawCart()
|
|
|
|
+ drawOrderSuccessful()
|
|
|
|
+ }
|
|
|
|
+})
|