createReducer & createAction

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-01-17 09:22:59 +03:00
parent 2d2ed497ca
commit 67fa902c75
13 changed files with 300 additions and 113 deletions

20
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@brojs/cli": "1.6.1", "@brojs/cli": "1.6.1",
"@chakra-ui/react": "^2.10.3", "@chakra-ui/react": "^2.10.3",
"@eslint/js": "^9.11.0", "@eslint/js": "^9.11.0",
"@redux-devtools/extension": "^3.3.0",
"@stylistic/eslint-plugin": "^2.8.0", "@stylistic/eslint-plugin": "^2.8.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
@ -2437,6 +2438,19 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@redux-devtools/extension": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz",
"integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"immutable": "^4.3.4"
},
"peerDependencies": {
"redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
@ -6348,6 +6362,12 @@
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
} }
}, },
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"license": "MIT"
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",

View File

@ -19,6 +19,7 @@
"@brojs/cli": "1.6.1", "@brojs/cli": "1.6.1",
"@chakra-ui/react": "^2.10.3", "@chakra-ui/react": "^2.10.3",
"@eslint/js": "^9.11.0", "@eslint/js": "^9.11.0",
"@redux-devtools/extension": "^3.3.0",
"@stylistic/eslint-plugin": "^2.8.0", "@stylistic/eslint-plugin": "^2.8.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",

View File

@ -1,23 +1,26 @@
import * as types from '../const' import * as types from '../const'
export const setStars = (userId: string, value: number) => ({ const createAction = <Payload>(type: string) => (payload?: Payload) => ({ type, payload })
type: types.SET_STARS,
payload: {
id: userId,
value,
},
})
export const incrementStart = (userId: string) => ({ export const setStars = createAction<{ id: string, value: number }>(types.SET_STARS)
type: types.INCREMENT_STARS, export const incrementStars = createAction<{ id: string }>(types.INCREMENT_STARS)
payload: { export const decrementStars = createAction<{ id: string }>(types.DECREMENT_STARS)
id: userId
}
})
export const decrementStart = (userId: string) => ({ // export const setStarsOld = (payload) => ({
type: types.INCREMENT_STARS, // type: types.SET_STARS,
payload: { // payload,
id: userId // })
}
}) // export const incrementStart = (userId: string) => ({
// type: types.INCREMENT_STARS,
// payload: {
// id: userId
// }
// })
// export const decrementStart = (userId: string) => ({
// type: types.INCREMENT_STARS,
// payload: {
// id: userId
// }
// })

View File

@ -5,34 +5,30 @@ const initialState = {
user: {}, user: {},
} }
export const starsReducer = (state = initialState, action) => { const setStars = (state, action) => ({
switch (action.type) { ...state,
case types.SET_STARS: friends: {
return { ...state.friends,
...state, [action.payload.id]: action.payload.value,
friends: { },
...state.friends, })
[action.payload.id]: action.payload.value,
},
}
case types.INCREMENT_STARS:
return {
...state,
friends: {
...state.friends,
[action.payload.id]: state[action.payload.id] + 1,
},
}
case types.DECREMENT_STARS: const changeStars = (value) => (state, action) => ({
return { ...state,
...state, friends: {
friends: { ...state.friends,
...state.friends, [action.payload.id]: state.friends[action.payload.id] + value,
[action.payload.id]: state[action.payload.id] - 1, },
}, })
}
default: return state const createReducer = (initialState, reducers) => (state = initialState, action) => {
} const reducer = reducers[action.type]
if (!reducer) return state
return reducer(state, action)
} }
export const starsReducer = createReducer(initialState, {
[types.SET_STARS]: setStars,
[types.INCREMENT_STARS]: changeStars(1),
[types.DECREMENT_STARS]: changeStars(-1),
})

View File

@ -1,4 +1,5 @@
import { combineReducers, legacy_createStore } from 'redux' import { combineReducers, legacy_createStore } from 'redux'
import { devToolsEnhancer } from '@redux-devtools/extension'
import { starsReducer } from './reducers/stars' import { starsReducer } from './reducers/stars'
@ -6,7 +7,9 @@ export const store = legacy_createStore(
combineReducers({ combineReducers({
stars: starsReducer, stars: starsReducer,
}), }),
); devToolsEnhancer({
name: 'nav2',
(window as unknown as { redux: string }).redux = store as unknown as string }),
)
;(window as unknown as { redux: string }).redux = store as unknown as string

View File

@ -1,13 +1,14 @@
import { useForm, Controller } from 'react-hook-form' import { useForm, Controller } from 'react-hook-form'
import { Box, Button, Input } from '@chakra-ui/react' import { Box, Button, Input, Stack } from '@chakra-ui/react'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
import { useDispatch } from 'react-redux'
type Inputs = { type Inputs = {
name: string name: string
age: string age: string
} }
export const FormTest = ({ name }) => { export const FormTest = ({ name, age, onOpenModal, onCancel }) => {
const { const {
register, register,
control, control,
@ -20,12 +21,20 @@ export const FormTest = ({ name }) => {
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
name: name, name: name,
age: '12', age: age,
}, },
}) })
const onSibmit = ({ name, age }) => { const onSibmit = ({ name, age }) => {
// console.log(1111111, name, age) // console.log(name, age)
}
const dispatch = useDispatch()
const handleOpenModalClick = () => {
const data = watch()
dispatch({ type: 'OPEN_MODAL', payload: data })
onOpenModal()
} }
return ( return (
@ -34,7 +43,7 @@ export const FormTest = ({ name }) => {
as="form" as="form"
flexDirection="column" flexDirection="column"
display="flex" display="flex"
onSubmit={handleSubmit(onSibmit)} onSubmit={handleSubmit(console.log)}
> >
<label> <label>
Name: Name:
@ -45,7 +54,7 @@ export const FormTest = ({ name }) => {
required: 'required 4 now', required: 'required 4 now',
minLength: { value: 4, message: 'min 4 now' }, minLength: { value: 4, message: 'min 4 now' },
}} }}
render={({ field, fieldState, formState }) => <Input {...field} />} render={({ field }) => <Input {...field} />}
/> />
</label> </label>
@ -54,12 +63,22 @@ export const FormTest = ({ name }) => {
<Controller <Controller
control={control} control={control}
name="age" name="age"
render={({ field, fieldState, formState }) => ( render={({ field }) => (
<Input type="number" {...field} /> <Input type="number" {...field} />
)} )}
/> />
</label> </label>
<Button type="submit">Submit</Button> <Stack gap={4} pt={8}>
<Button variant="solid" type="submit">
Submit
</Button>
<Button variant="outline" type="button" onClick={handleOpenModalClick}>
Продолжить в модальном окне
</Button>
<Button variant="ghost" type="button" onClick={onCancel}>
Cancel
</Button>
</Stack>
</Box> </Box>
</> </>
) )

View File

@ -20,10 +20,13 @@ import { FormTest } from './from'
import { Stars } from '../stars' import { Stars } from '../stars'
import { stars as starsContext } from '../../__data__/context' import { stars as starsContext } from '../../__data__/context'
import { useUsers } from '../../hooks' import { useUsers } from '../../hooks'
import { useDispatch, useSelector } from 'react-redux'
import { decrementStars, incrementStars } from '../../__data__/actions/stars'
type User = Record<string, unknown> & { type User = Record<string, unknown> & {
id: string id: string
name: string name: string
age: number
avatar?: string avatar?: string
rated: number rated: number
friends?: User[] friends?: User[]
@ -35,18 +38,29 @@ export const Profile = ({
user, user,
isLink, isLink,
title, title,
onOpenModal,
}: { }: {
user: User user: User
isLink?: boolean isLink?: boolean
title?: string title?: string
onOpenModal?: () => void
}) => { }) => {
// const [rated, setRated] = useState(user.rated || 0) // const [rated, setRated] = useState(user.rated || 0)
const [editProfile, setEditProfile] = useState(false) const [editProfile, setEditProfile] = useState(false)
return ( return (
<Box mt={3} borderWidth="1px" p={3} overflowX="hidden"> <Box mt={3} borderWidth="1px" p={3} overflowX="hidden">
{!isLink && editProfile && <FormTest name={user.name} />} {!isLink && editProfile && (
{!editProfile && <Button onClick={() => setEditProfile(true)}>Редактировать</Button>} <FormTest
onCancel={() => setEditProfile(false)}
onOpenModal={onOpenModal}
age={user.age}
name={user.name}
/>
)}
{onOpenModal && !editProfile && (
<Button onClick={() => setEditProfile(true)}>Редактировать</Button>
)}
<Heading as="h2">{title || 'Данные профиля'}</Heading> <Heading as="h2">{title || 'Данные профиля'}</Heading>
<Box m={3}> <Box m={3}>
<Card width={'fit-content'} shadow="2xl"> <Card width={'fit-content'} shadow="2xl">
@ -73,12 +87,12 @@ export const Profile = ({
{/* {!isLink && {/* {!isLink &&
features['buttons'] && features['buttons'] &&
user.friends?.map((friend) => ( */} user.friends?.map((friend) => ( */}
<Counter <Counter
key={user.id} key={user.id}
// value={rated} setValue={setRated} // value={rated} setValue={setRated}
userId={user.id} userId={user.id}
/> />
{/* ))} */} {/* ))} */}
</Box> </Box>
) )
} }
@ -127,43 +141,50 @@ const Form = memo<{ initialState: string; onSubmit(value: string): void }>(
Form.displayName = 'FormMemo' Form.displayName = 'FormMemo'
const withStars = // const withStars =
(Component) => // (Component) =>
({ userId }) => { // ( { userId }) => {
const { stars, setStar } = useContext(starsContext) // const { stars, setStar } = useContext(starsContext)
const addStar = useCallback( // const addStar = useCallback(
() => setStar(userId, stars[userId] + 1), // () => setStar(userId, stars[userId] + 1),
[userId, setStar], // [userId, setStar],
) // )
const subStar = useCallback( // const subStar = useCallback(
() => setStar(userId, stars[userId] - 1), // () => setStar(userId, stars[userId] - 1),
[userId, setStar], // [userId, setStar],
) // )
return ( // return (
<Component // <Component
userId={userId} // userId={userId}
stars={stars[userId]} // stars={stars[userId]}
addStar={addStar} // addStar={addStar}
subStar={subStar} // subStar={subStar}
/> // />
) // )
} // }
const Counter = ({ stars, addStar, subStar, userId }: { userId: 'some-user-id' | '2'}) => { const Counter = ({
const { rate, setUserRate } = useUsers((state) => state[userId].rated) // stars, addStar, subStar,
userId,
}: {
userId: string
}) => {
// const { rate, setUserRate } = useUsers((state) => state[userId].rated)
const rate = useSelector((store) => store.stars.friends[userId]) ?? 0
const dispatch = useDispatch()
return ( return (
<Center> <Center>
<VStack> <VStack>
<Heading>{rate}</Heading> <Heading>{rate}</Heading>
<Button onClick={() => setUserRate(userId, rate + 1)}>+</Button> <Button onClick={() => dispatch(incrementStars({ id: userId }))}>+</Button>
<Button onClick={() => setUserRate(userId, rate - 1)}>-</Button> <Button onClick={() => dispatch(decrementStars({ id: userId }))}>-</Button>
</VStack> </VStack>
</Center> </Center>
) )
} }
const CounterWithStars = withStars(Counter) // const CounterWithStars = withStars(Counter)

View File

@ -0,0 +1 @@
export { ProfileEditModal } from './profileEdit'

View File

@ -0,0 +1,114 @@
import { Box, Button, Input, Stack } from '@chakra-ui/react'
import React from 'react'
import { Controller, useForm } from 'react-hook-form'
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react'
type Inputs = {
name: string
lastName: string
age: string
}
export const ProfileEditModal = ({ onClose }) => {
const {
register,
control,
handleSubmit,
watch,
reset,
setValue,
formState: { errors },
} = useForm<Inputs>({
mode: 'onChange',
defaultValues: {
name: 'name',
lastName: 'string',
age: '12',
},
})
const onSibmit = ({ name, age }) => {
// console.log(1111111, name, age)
}
return (
<Modal isOpen onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Box
as="form"
flexDirection="column"
display="flex"
gap={3}
onSubmit={handleSubmit(onSibmit)}
>
<label>
Name:
<Controller
control={control}
name="name"
rules={{
required: 'required 4 now',
minLength: { value: 4, message: 'min 4 now' },
}}
render={({ field }) => (
<Input isInvalid={Boolean(errors.name)} {...field} />
)}
/>
</label>
<label>
Age:
<Controller
control={control}
name="age"
rules={{
required: 'required 4 now',
}}
render={({ field }) => (
<Input
isInvalid={Boolean(errors.age)}
type="number"
{...field}
/>
)}
/>
</label>
<label>
lastName:
<Controller
control={control}
name="lastName"
render={({ field }) => (
<Input type="number" {...field} />
)}
/>
</label>
<Stack gap={4} pt={8}>
<Button variant="solid" type="submit">
Submit
</Button>
</Stack>
</Box>
<ModalFooter>
<Button variant="outline" type="button" onClick={onClose}>
Отмена
</Button>
</ModalFooter>
</ModalBody>
</ModalContent>
</Modal>
)
}

View File

@ -2,10 +2,11 @@ import { HStack, Icon } from '@chakra-ui/react'
import { FaRegStar, FaStar } from 'react-icons/fa6' import { FaRegStar, FaStar } from 'react-icons/fa6'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { getConfigValue } from '@brojs/cli' import { getConfigValue } from '@brojs/cli'
import { useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { stars } from '../__data__/context' import { stars } from '../__data__/context'
import { useUsers } from '../hooks' import { useUsers } from '../hooks'
import { setStars } from '../__data__/actions/stars'
const useStars = (currentRated, starsCount) => { const useStars = (currentRated, starsCount) => {
@ -29,7 +30,7 @@ export const Stars = ({
userId, userId,
}: { }: {
count: number count: number
userId: 'some-user-id' | '2' userId: string
// rated: number // rated: number
// setRated: (rate: number) => void // setRated: (rate: number) => void
}) => { }) => {
@ -37,21 +38,23 @@ export const Stars = ({
// stars, // stars,
// setRated: starsSetRated // setRated: starsSetRated
// } = useStars(rated, count) // } = useStars(rated, count)
const { setUserRate } = useUsers(state => state[userId].rated) // const { setUserRate } = useUsers(state => state[userId].rated)
const rate = useSelector((store) => store.stars.friends[userId]) ?? 0 const rate = useSelector((store) => store.stars.friends[userId]) ?? 0
const dispatch = useDispatch()
const handleStarsClick = (stars: number) => { const handleStarsClick = (value: number) => {
setUserRate(userId, stars) dispatch(setStars({ id: userId, value }))
fetch(getConfigValue('dry-wash.api.url') + '/user-rate', { // setUserRate(userId, stars)
method: 'POST', // fetch(getConfigValue('dry-wash.api.url') + '/user-rate', {
headers: { // method: 'POST',
'Content-Type': 'application/json', // headers: {
}, // 'Content-Type': 'application/json',
body: JSON.stringify({ // },
userId, // body: JSON.stringify({
stars, // userId,
}), // stars,
}) // }),
// })
} }
return ( return (

View File

@ -5,12 +5,14 @@ import { useDispatch } from 'react-redux'
import { Profile } from '../components' import { Profile } from '../components'
import { users } from '../__data__/users' import { users } from '../__data__/users'
import { setStars } from '../__data__/actions/stars' import { setStars } from '../__data__/actions/stars'
import { ProfileEditModal } from '../components/profileEdit'
export const Friends = memo(() => { export const Friends = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [data, setData] = useState(null) const [data, setData] = useState(null)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const dispatch = useDispatch() const dispatch = useDispatch()
const [showModal, setShowModal] = useState(false)
useEffect(() => { useEffect(() => {
const getUser = async () => { const getUser = async () => {
@ -24,7 +26,7 @@ export const Friends = memo(() => {
setData(data) setData(data)
Object.keys(data).forEach(userId => { Object.keys(data).forEach(userId => {
dispatch(setStars(userId, data[userId].rated)) dispatch(setStars({ id: userId, value: data[userId].rated }))
}) })
} else { } else {
throw 'Что-то пошло не так' throw 'Что-то пошло не так'
@ -43,10 +45,13 @@ export const Friends = memo(() => {
if(!data || isLoading) return <h1>loading...</h1> if(!data || isLoading) return <h1>loading...</h1>
return ( return (
<HStack> <>
<Profile user={data['some-user-id']} /> {showModal && <ProfileEditModal onClose={() => setShowModal(false)} />}
<Profile user={data['2']} /> <HStack>
</HStack> <Profile onOpenModal={() => setShowModal(true)} user={data['some-user-id']} />
<Profile onOpenModal={() => setShowModal(true)} user={data['2']} />
</HStack>
</>
) )
}) })

View File

@ -17,7 +17,6 @@ timer.fast = timer(300)
// router.use(timer.fast) // router.use(timer.fast)
router.post('/user-rate', (req, res) => { router.post('/user-rate', (req, res) => {
console.log(req.body)
res.status(500).send({ ok: false }) res.status(500).send({ ok: false })
}) })

View File

@ -4,6 +4,7 @@
"some-user-id": { "some-user-id": {
"id": "some-user-id", "id": "some-user-id",
"name": "alexandr", "name": "alexandr",
"age": 38,
"surname": null, "surname": null,
"email": null, "email": null,
"rated": 4, "rated": 4,
@ -24,6 +25,7 @@
"name": "not alexandr", "name": "not alexandr",
"surname": null, "surname": null,
"email": null, "email": null,
"age": 24,
"rated": 5, "rated": 5,
"avatar": "https://www.gravatar.com/avatar/6e?d=robohash", "avatar": "https://www.gravatar.com/avatar/6e?d=robohash",
"friends": [ "friends": [