diff --git a/package-lock.json b/package-lock.json index cc1f315..d22f829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@chakra-ui/react": "^2.10.3", "@eslint/js": "^9.11.0", "@redux-devtools/extension": "^3.3.0", + "@reduxjs/toolkit": "^2.5.0", "@stylistic/eslint-plugin": "^2.8.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -28,6 +29,7 @@ "react-redux": "^9.2.0", "react-router-dom": "^6.23.1", "redux": "^5.0.1", + "redux-thunk": "^3.1.0", "typescript-eslint": "^8.6.0" } }, @@ -2451,6 +2453,40 @@ "redux": "^3.1.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", + "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -8621,6 +8657,15 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -8737,6 +8782,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index 35f02ca..78a524e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@chakra-ui/react": "^2.10.3", "@eslint/js": "^9.11.0", "@redux-devtools/extension": "^3.3.0", + "@reduxjs/toolkit": "^2.5.0", "@stylistic/eslint-plugin": "^2.8.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", @@ -35,6 +36,7 @@ "react-redux": "^9.2.0", "react-router-dom": "^6.23.1", "redux": "^5.0.1", + "redux-thunk": "^3.1.0", "typescript-eslint": "^8.6.0" } } diff --git a/src/__data__/actions/stars.ts b/src/__data__/actions/stars.ts index 698cb13..fd131f8 100644 --- a/src/__data__/actions/stars.ts +++ b/src/__data__/actions/stars.ts @@ -1,6 +1,6 @@ -import * as types from '../const' +import { createAction } from '@reduxjs/toolkit' -const createAction = (type: string) => (payload?: Payload) => ({ type, payload }) +import * as types from '../const' export const setStars = createAction<{ id: string, value: number }>(types.SET_STARS) export const incrementStars = createAction<{ id: string }>(types.INCREMENT_STARS) diff --git a/src/__data__/actions/users.ts b/src/__data__/actions/users.ts new file mode 100644 index 0000000..55a1b57 --- /dev/null +++ b/src/__data__/actions/users.ts @@ -0,0 +1,32 @@ +import * as types from '../const' + +import { setStars } from './stars' + +const createAction = (type: string) => (payload?: Payload) => ({ type, payload }) + +const fetchUsers = createAction(types.USER_FETCH) +const success = createAction(types.USER_FETCH_SUCCESS) +const error = createAction(types.USER_FETCH_ERROR) +export const reset = createAction(types.USER_FETCH_RESET) + +export const getUsers = () => async (dispatch, getState) => { + dispatch(fetchUsers()) + + try { + const response = await fetch('/api/users/') + + if (response.ok) { + const data = (await response.json()).body + dispatch(success(data)) + + Object.keys(data).forEach(userId => { + dispatch(setStars({ id: userId, value: data[userId].rated })) + }) + } else { + throw 'Что-то пошло не так' + } + + } catch (e) { + dispatch(error('Что-то пошло не так')) + } +} \ No newline at end of file diff --git a/src/__data__/const.ts b/src/__data__/const.ts index 5396fb1..330eedc 100644 --- a/src/__data__/const.ts +++ b/src/__data__/const.ts @@ -1,3 +1,8 @@ export const SET_STARS = 'SET_STARS' export const INCREMENT_STARS = 'INCREMENT_STARS' export const DECREMENT_STARS = 'DECREMENT_STARS' + +export const USER_FETCH = 'USER_FETCH' +export const USER_FETCH_SUCCESS = 'USER_FETCH_SUCCESS' +export const USER_FETCH_ERROR = 'USER_FETCH_ERROR' +export const USER_FETCH_RESET = 'USER_FETCH_RESET' diff --git a/src/__data__/reducers/stars.ts b/src/__data__/reducers/stars.ts index 08dad0b..e0a3d36 100644 --- a/src/__data__/reducers/stars.ts +++ b/src/__data__/reducers/stars.ts @@ -1,3 +1,5 @@ +import { createSlice } from '@reduxjs/toolkit' + import * as types from '../const' const initialState = { @@ -21,7 +23,7 @@ const changeStars = (value) => (state, action) => ({ }, }) -const createReducer = (initialState, reducers) => (state = initialState, action) => { +const createReducer = (initialState: State, reducers) => (state = initialState, action): State => { const reducer = reducers[action.type] if (!reducer) return state return reducer(state, action) @@ -32,3 +34,15 @@ export const starsReducer = createReducer(initialState, { [types.INCREMENT_STARS]: changeStars(1), [types.DECREMENT_STARS]: changeStars(-1), }) + +const slice = createSlice({ + name: 'stars', + initialState: {}, + reducers: { + setStars(state, action) { + state[action.payload.id] = action.payload.value + }, + changeStars, + }, + +}) diff --git a/src/__data__/reducers/users.ts b/src/__data__/reducers/users.ts new file mode 100644 index 0000000..0f2c223 --- /dev/null +++ b/src/__data__/reducers/users.ts @@ -0,0 +1,50 @@ +import * as types from '../const' + +const createReducer = (initialState: State, reducers) => (state = initialState, action): State => { + const reducer = reducers[action.type] + if (!reducer) return state + return reducer(state, action) +} + +export enum Statuses { + IDLE = 'IDLE', + FETCHING = 'FETCHING', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} + +const initialState = { + data: null, + status: Statuses.IDLE, + error: null as null | string, +} + +const startFetching = (state) => ({ + ...state, + error: null, + status: Statuses.FETCHING, +}) + +const fetchSuccess = (state, action) => ({ + ...state, + error: null, + data: action.payload, + status: Statuses.SUCCESS, +}) + +const fetchError = (state, action) => ({ + ...state, + error: action.payload, + status: Statuses.ERROR, +}) + +const reset = () => ({ + ...initialState +}) + +export const reducer = createReducer(initialState, { + [types.USER_FETCH]: startFetching, + [types.USER_FETCH_SUCCESS]: fetchSuccess, + [types.USER_FETCH_ERROR]: fetchError, + [types.USER_FETCH_RESET]: reset, +}) \ No newline at end of file diff --git a/src/__data__/selectors/index.ts b/src/__data__/selectors/index.ts new file mode 100644 index 0000000..2ebef71 --- /dev/null +++ b/src/__data__/selectors/index.ts @@ -0,0 +1 @@ +export * as userSelectors from './users' diff --git a/src/__data__/selectors/rootSelector.ts b/src/__data__/selectors/rootSelector.ts new file mode 100644 index 0000000..5888ef4 --- /dev/null +++ b/src/__data__/selectors/rootSelector.ts @@ -0,0 +1,8 @@ +import { createSelector } from '@reduxjs/toolkit' + +import { StoreType } from '../store' + +export const rootSelector = createSelector( + (state: StoreType) => state, + (state: StoreType) => state +) diff --git a/src/__data__/selectors/users.ts b/src/__data__/selectors/users.ts new file mode 100644 index 0000000..31b4b56 --- /dev/null +++ b/src/__data__/selectors/users.ts @@ -0,0 +1,12 @@ +import { createSelector } from '@reduxjs/toolkit' + +import { StoreType } from '../store' +import { Statuses } from '../reducers/users' + +import { rootSelector } from './rootSelector' + +const usersRootSelector = createSelector(rootSelector, (state: StoreType) => state.user) + +export const isLoading = createSelector(usersRootSelector, (state) => state.status === Statuses.FETCHING) +export const data = createSelector(usersRootSelector, (state) => state.data) +export const error = createSelector(usersRootSelector, (state) => state.error) diff --git a/src/__data__/store.ts b/src/__data__/store.ts index 166a5dd..e1f93c0 100644 --- a/src/__data__/store.ts +++ b/src/__data__/store.ts @@ -1,15 +1,20 @@ -import { combineReducers, legacy_createStore } from 'redux' -import { devToolsEnhancer } from '@redux-devtools/extension' +import { configureStore } from '@reduxjs/toolkit' +import { useSelector } from 'react-redux' import { starsReducer } from './reducers/stars' +import { reducer as usersFetchReducer } from './reducers/users' -export const store = legacy_createStore( - combineReducers({ +export const store = configureStore({ + reducer: { stars: starsReducer, - }), - devToolsEnhancer({ - name: 'nav2', - }), -) + user: usersFetchReducer, + }, + devTools: { + name: 'stars', + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), +}) -;(window as unknown as { redux: string }).redux = store as unknown as string +export type StoreType = ReturnType + +export const useAppSelector = useSelector diff --git a/src/index.tsx b/src/index.tsx index 58571bc..009bfd2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,10 +14,10 @@ export default () => let rootElement: ReactDOM.Root -export const mount = async (Сomponent, element = document.getElementById('app')) => { +export const mount = async (Сomponent, element = document.getElementById('app'), extraData) => { await config rootElement = ReactDOM.createRoot(element) - rootElement.render(<Сomponent/>) + rootElement.render(<Сomponent />) if(module.hot) { module.hot.accept('./app', ()=> { diff --git a/src/pages/friends.tsx b/src/pages/friends.tsx index f7bd893..07fcfa5 100644 --- a/src/pages/friends.tsx +++ b/src/pages/friends.tsx @@ -6,40 +6,25 @@ import { Profile } from '../components' import { users } from '../__data__/users' import { setStars } from '../__data__/actions/stars' import { ProfileEditModal } from '../components/profileEdit' +import { getUsers } from '../__data__/actions/users' +import { UnknownAction } from 'redux' +import { useAppSelector } from '../__data__/store' +import { Statuses } from '../__data__/reducers/users' +import { userSelectors } from '../__data__/selectors' export const Friends = memo(() => { - const [isLoading, setIsLoading] = useState(false) - const [data, setData] = useState(null) - const [error, setError] = useState(null) + // const [isLoading, setIsLoading] = useState(false) + // const [data, setData] = useState(null) + // const [error, setError] = useState(null) const dispatch = useDispatch() const [showModal, setShowModal] = useState(false) + const isLoading = useAppSelector(userSelectors.isLoading) + const data = useAppSelector(userSelectors.data) + const error = useAppSelector(userSelectors.error) + useEffect(() => { - const getUser = async () => { - setIsLoading(true) - - try { - const response = await fetch('/api/users/') - - if (response.ok) { - const data = (await response.json()).body - setData(data) - - Object.keys(data).forEach(userId => { - dispatch(setStars({ id: userId, value: data[userId].rated })) - }) - } else { - throw 'Что-то пошло не так' - } - - } catch (e) { - setError(e.message) - } finally { - setIsLoading(false) - } - } - - getUser() + dispatch(getUsers() as unknown as UnknownAction) }, []) if(!data || isLoading) return

loading...