|
@@ -1,13 +1,447 @@
|
|
import { useState } from 'react';
|
|
import { useState } from 'react';
|
|
import './App.css';
|
|
import './App.css';
|
|
|
|
|
|
-const LoginForm = () => {
|
|
|
|
|
|
+import thunk from 'redux-thunk';
|
|
|
|
+import {createStore, combineReducers, applyMiddleware} from 'redux';
|
|
|
|
+import { Provider, connect } from 'react-redux';
|
|
|
|
+
|
|
|
|
+const store = createStore(combineReducers({
|
|
|
|
+ auth: authReducer,
|
|
|
|
+ promise: promiseReducer,
|
|
|
|
+ cart: localStoreReducer(cartReducer, "cart")
|
|
|
|
+}), applyMiddleware(thunk))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+function jwtDecode(token) {
|
|
|
|
+ try {
|
|
|
|
+ return JSON.parse(atob(token.split('.')[1]))
|
|
|
|
+ }
|
|
|
|
+ catch (e) {
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function authReducer(state = {}, { type, token }) {
|
|
|
|
+ //{
|
|
|
|
+ // token, payload
|
|
|
|
+ //}
|
|
|
|
+
|
|
|
|
+ if (type === 'AUTH_LOGIN') {
|
|
|
|
+ //пытаемся токен раскодировать
|
|
|
|
+ const payload = jwtDecode(token)
|
|
|
|
+ if (payload) { //и если получилось
|
|
|
|
+ return {
|
|
|
|
+ token, payload //payload - раскодированный токен;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (type === 'AUTH_LOGOUT') {
|
|
|
|
+ return {}
|
|
|
|
+ }
|
|
|
|
+ return state;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function countReducer(state = { count: 0 }, { type }) {
|
|
|
|
+ if (type === "COUNT_INC") {
|
|
|
|
+ return {
|
|
|
|
+ count: state.count + 1
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (type === "COUNT_DEC") {
|
|
|
|
+ return {
|
|
|
|
+ count: state.count - 1
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return state
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function localStoreReducer(reducer, localStorageKey) {
|
|
|
|
+ function localStoredReducer(state, action) {
|
|
|
|
+ // Если state === undefined, то достать старый state из local storage
|
|
|
|
+ if (state === undefined) {
|
|
|
|
+ try {
|
|
|
|
+ return JSON.parse(localStorage[localStorageKey])
|
|
|
|
+ } catch (e) { }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ const newState = reducer(state, action)
|
|
|
|
+ // Сохранить newState в local storage
|
|
|
|
+ localStorage[localStorageKey] = JSON.stringify(newState)
|
|
|
|
+ return newState
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ return localStoredReducer
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+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') {
|
|
|
|
+ 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))
|
|
|
|
+ return payload
|
|
|
|
+ }
|
|
|
|
+ catch (e) {
|
|
|
|
+ dispatch(actionRejected(name, e))
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+// const delay = ms => new Promise(ok => setTimeout(() => ok(ms), ms))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// function combineReducers(reducers) { //пачку редьюсеров как объект {auth: authReducer, promise: promiseReducer}
|
|
|
|
+// function combinedReducer(combinedState = {}, action) { //combinedState - типа {auth: {...}, promise: {....}}
|
|
|
|
+// 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 //нам возвращают один редьюсер, который имеет стейт вида {auth: {...стейт authReducer-а}, promise: {...стейт promiseReducer-а}}
|
|
|
|
+// }
|
|
|
|
+
|
|
|
|
+function cartReducer(state = {}, { type, count = 1, good }) {
|
|
|
|
+ // type CART_ADD CART_REMOVE CART_CLEAR CART_DEL
|
|
|
|
+ // {
|
|
|
|
+ // id1: {count: 1, good: {name, price, images, id}}
|
|
|
|
+ // }
|
|
|
|
+ if (type === "CART_ADD") {
|
|
|
|
+ return {
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: { count: count + (state[good._id]?.count || 0), good },
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (type === "CART_DELETE") {
|
|
|
|
+ if (state[good._id].count > 1) {
|
|
|
|
+ return {
|
|
|
|
+ ...state,
|
|
|
|
+ [good._id]: {
|
|
|
|
+ count: -count + (state[good._id]?.count || 0),
|
|
|
|
+ good,
|
|
|
|
+ },
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (state[good._id].count === 1) {
|
|
|
|
+ let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
|
|
|
|
+ //delete newState[good._id]
|
|
|
|
+ return newState;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (type === "CART_CLEAR") {
|
|
|
|
+ return {};
|
|
|
|
+ }
|
|
|
|
+ if (type === "CART_REMOVE") {
|
|
|
|
+ // let newState = {...state}
|
|
|
|
+ let { [good._id]: id1, ...newState } = state; //o4en strashnoe koldunstvo
|
|
|
|
+ //delete newState[good._id]
|
|
|
|
+ return newState;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return state;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const backendURL = 'http://shop-roles.node.ed.asmer.org.ua/'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+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 gql = getGQL(backendURL + "graphql");
|
|
|
|
+
|
|
|
|
+// 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/graphql'
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+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
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`,
|
|
|
|
+ { q: JSON.stringify([{ _id }]) }
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+const actionGoodById = (_id) =>
|
|
|
|
+ actionPromise(
|
|
|
|
+ 'goodByID',
|
|
|
|
+ gql(
|
|
|
|
+
|
|
|
|
+ `query goodByID($q:String){
|
|
|
|
+ GoodFindOne(query: $q){
|
|
|
|
+ _id
|
|
|
|
+ name
|
|
|
|
+ description
|
|
|
|
+ price
|
|
|
|
+ categories{
|
|
|
|
+ _id
|
|
|
|
+ name
|
|
|
|
+ }
|
|
|
|
+ images{
|
|
|
|
+ url
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }`,
|
|
|
|
+ { q: JSON.stringify([{ _id }]) }
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+const actionRegistr = (login, password) =>
|
|
|
|
+ actionPromise(
|
|
|
|
+ 'registr',
|
|
|
|
+ gql(
|
|
|
|
+
|
|
|
|
+ `mutation register($login:String, $password:String){
|
|
|
|
+ UserUpsert(user: {login:$login, password:$password}){
|
|
|
|
+ _id login
|
|
|
|
+ }
|
|
|
|
+ }`,
|
|
|
|
+ { login: login, password: password }
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+const actionLogin = (login, password) =>
|
|
|
|
+ actionPromise(
|
|
|
|
+ 'login',
|
|
|
|
+ gql(
|
|
|
|
+
|
|
|
|
+ `query log($login:String, $password:String){
|
|
|
|
+ login(login:$login, password:$password)
|
|
|
|
+ }`,
|
|
|
|
+ { login: login, password: password }
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ const actionOrder = () => async (dispatch, getState) => {
|
|
|
|
+ let { cart } = getState();
|
|
|
|
+ const orderGoods = Object.entries(cart).map(([_id, { count }]) => ({
|
|
|
|
+ good: { _id },
|
|
|
|
+ count,
|
|
|
|
+ }));
|
|
|
|
+
|
|
|
|
+ let result = await dispatch(
|
|
|
|
+ actionPromise(
|
|
|
|
+ "order",
|
|
|
|
+ gql(
|
|
|
|
+ `
|
|
|
|
+ mutation newOrder($order:OrderInput){
|
|
|
|
+ OrderUpsert(order:$order)
|
|
|
|
+ { _id total }
|
|
|
|
+ }
|
|
|
|
+ `,
|
|
|
|
+ { order: { orderGoods } }
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+ );
|
|
|
|
+ if (result?._id) {
|
|
|
|
+ dispatch(actionCartClear());
|
|
|
|
+ document.location.hash = "#/cart/";
|
|
|
|
+ alert("Покупка успішна")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const orderHistory = () =>
|
|
|
|
+ actionPromise(
|
|
|
|
+ "history",
|
|
|
|
+ gql(` query OrderFind{
|
|
|
|
+ OrderFind(query:"[{}]"){
|
|
|
|
+ _id total createdAt orderGoods{
|
|
|
|
+ count good{
|
|
|
|
+ _id name price images{
|
|
|
|
+ url
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ owner{
|
|
|
|
+ _id login
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ `)
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+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 actionCartAdd = (good, count = 1) => ({
|
|
|
|
+ type: "CART_ADD",
|
|
|
|
+ good,
|
|
|
|
+ count
|
|
|
|
+});
|
|
|
|
+const actionCartChange = (good, count = 1) => ({
|
|
|
|
+ type: "CART_CHANGE",
|
|
|
|
+ good,
|
|
|
|
+ count,
|
|
|
|
+}); ///oninput меняяем полностью
|
|
|
|
+const actionCartDelete = (good) => ({
|
|
|
|
+ type: "CART_DELETE",
|
|
|
|
+ good
|
|
|
|
+});
|
|
|
|
+const actionCartClear = () => ({
|
|
|
|
+ type: "CART_CLEAR"
|
|
|
|
+});
|
|
|
|
+const actionCartRemove = (good) =>({
|
|
|
|
+ type: "CART_REMOVE",
|
|
|
|
+ good
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const actionFullLogin = (login, password) => async (dispatch) => {
|
|
|
|
+ let token = await dispatch(actionLogin(login, password))
|
|
|
|
+ if (token) {
|
|
|
|
+ dispatch(actionAuthLogin(token))
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const actionFullRegistr = (login, password) => async (dispatch) => {
|
|
|
|
+ try {
|
|
|
|
+ await dispatch(actionRegistr(login, password))
|
|
|
|
+ }
|
|
|
|
+ catch (e) {
|
|
|
|
+ return console.log(e)
|
|
|
|
+ }
|
|
|
|
+ await dispatch(actionFullLogin(login, password))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ //не забудьте combineReducers если он у вас уже есть
|
|
|
|
+if (localStorage.authToken) {
|
|
|
|
+ store.dispatch(actionAuthLogin(localStorage.authToken))
|
|
|
|
+}
|
|
|
|
+//const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, cart: cartReducer}))
|
|
|
|
+store.subscribe(() => console.log(store.getState()))
|
|
|
|
+
|
|
|
|
+store.dispatch(actionRootCats())
|
|
|
|
+// store.dispatch(actionLogin('test456', '123123'))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+const LoginForm = ({onLogin}) => {
|
|
const [login, setLogin] = useState('')
|
|
const [login, setLogin] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
- const onLogin = () => {
|
|
|
|
|
|
+ const disableButton = () => {
|
|
return !(login !== '' && password !== '')
|
|
return !(login !== '' && password !== '')
|
|
}
|
|
}
|
|
- const Vhod = () => alert('Конгратюлатион')
|
|
|
|
|
|
+
|
|
|
|
+ const Vhod = () => {
|
|
|
|
+ onLogin(login,password)
|
|
|
|
+ }
|
|
|
|
|
|
return(
|
|
return(
|
|
|
|
|
|
@@ -23,7 +457,7 @@ const LoginForm = () => {
|
|
onChange = { (e) => setPassword(e.target.value) }
|
|
onChange = { (e) => setPassword(e.target.value) }
|
|
/>
|
|
/>
|
|
|
|
|
|
- <button disabled = {onLogin()} onClick = {Vhod}
|
|
|
|
|
|
+ <button disabled = {disableButton()} onClick = {Vhod}
|
|
|
|
|
|
>Вход</button>
|
|
>Вход</button>
|
|
</div>
|
|
</div>
|
|
@@ -105,17 +539,19 @@ const Category = () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
+const CLoginForm = connect(null, {onLogin: actionFullLogin})(LoginForm)
|
|
function App() {
|
|
function App() {
|
|
return (
|
|
return (
|
|
|
|
+ <Provider store = {store}>
|
|
<div className="App">
|
|
<div className="App">
|
|
- <LoginForm />
|
|
|
|
|
|
+ <CLoginForm />
|
|
<div className ='shop'>
|
|
<div className ='shop'>
|
|
<CategoryMenu />
|
|
<CategoryMenu />
|
|
<Category />
|
|
<Category />
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
+ </Provider>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|