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()
}
}
window.onhashchange()
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} | Log out` : 'Log in')
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()
}
})