From cb168b64153d73ab7d65d2e18de135983e4eb518 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 30 Jul 2025 17:44:30 +0400 Subject: [PATCH] feat: projects redux storage and select --- package.json | 1 + src/app/deals/components/Header/Header.tsx | 42 +++++++ src/app/deals/page.tsx | 2 + .../selects/ObjectSelect/ObjectSelect.tsx | 110 ++++++++++++++++++ .../selects/ProjectSelect/ProjectSelect.tsx | 26 +++++ src/lib/features/projects/projectsSlice.ts | 29 +++++ src/lib/features/rootReducer.ts | 2 + src/theme.ts | 4 +- src/types/ProjectSchema.ts | 4 + yarn.lock | 8 ++ 10 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/app/deals/components/Header/Header.tsx create mode 100644 src/components/selects/ObjectSelect/ObjectSelect.tsx create mode 100644 src/components/selects/ProjectSelect/ProjectSelect.tsx create mode 100644 src/lib/features/projects/projectsSlice.ts create mode 100644 src/types/ProjectSchema.ts diff --git a/package.json b/package.json index 1650784..97ceb77 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@testing-library/user-event": "^14.6.1", "@types/eslint-plugin-jsx-a11y": "^6", "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.20", "@types/node": "^22.13.11", "@types/react": "19.1.8", "@types/react-redux": "^7.1.34", diff --git a/src/app/deals/components/Header/Header.tsx b/src/app/deals/components/Header/Header.tsx new file mode 100644 index 0000000..16219b2 --- /dev/null +++ b/src/app/deals/components/Header/Header.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useEffect } from "react"; +import { useSelector } from "react-redux"; +import { Group } from "@mantine/core"; +import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect"; +import { + selectProject, + setProjects, +} from "@/lib/features/projects/projectsSlice"; +import { RootState, useAppDispatch } from "@/lib/store"; + +const Header = () => { + const projectsState = useSelector( + (state: RootState) => state.projectsState + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + const mockProjects = [ + { id: 1, name: "Проект 1" }, + { id: 2, name: "Проект 2" }, + { id: 3, name: "Проект 3" }, + ]; + dispatch(setProjects(mockProjects)); + dispatch(selectProject(mockProjects[0])); + }, []); + + return ( + + value && dispatch(selectProject(value))} + /> + + ); +}; + +export default Header; diff --git a/src/app/deals/page.tsx b/src/app/deals/page.tsx index bb4fe41..fedcf06 100644 --- a/src/app/deals/page.tsx +++ b/src/app/deals/page.tsx @@ -1,4 +1,5 @@ import Boards from "@/app/deals/components/Boards/Boards"; +import Header from "@/app/deals/components/Header/Header"; import PageBlock from "@/components/PageBlock/PageBlock"; import PageContainer from "@/components/PageContainer/PageContainer"; @@ -6,6 +7,7 @@ export default function DealsPage() { return ( +
diff --git a/src/components/selects/ObjectSelect/ObjectSelect.tsx b/src/components/selects/ObjectSelect/ObjectSelect.tsx new file mode 100644 index 0000000..e05bb92 --- /dev/null +++ b/src/components/selects/ObjectSelect/ObjectSelect.tsx @@ -0,0 +1,110 @@ +import { useEffect, useMemo, useState } from "react"; +import { groupBy, omit } from "lodash"; +import { Select, SelectProps } from "@mantine/core"; + +interface ObjectWithIdAndName { + id: number; + name: string; +} + +export type SelectObjectType = T; + +type ControlledValueProps = { + value: SelectObjectType; + onChange: (value: SelectObjectType) => void; +}; +type CustomLabelAndKeyProps = { + getLabelFn: (item: SelectObjectType) => string; + getValueFn: (item: SelectObjectType) => string; +}; + +type RestProps = { + defaultValue?: SelectObjectType; + onChange: (value: SelectObjectType) => void; + data: SelectObjectType[]; + groupBy?: (item: SelectObjectType) => string; + filterBy?: (item: SelectObjectType) => boolean; +}; +const defaultGetLabelFn = (item: T): string => { + return item.name; +}; + +const defaultGetValueFn = (item: T): string => { + if (!item) return item; + return item.id.toString(); +}; +export type ObjectSelectProps = (RestProps & + Partial>) & + Omit & + (T extends ObjectWithIdAndName + ? Partial> + : CustomLabelAndKeyProps); + +const ObjectSelect = (props: ObjectSelectProps) => { + const isControlled = "value" in props; + const haveGetValueFn = "getValueFn" in props; + const haveGetLabelFn = "getLabelFn" in props; + const [internalValue, setInternalValue] = useState< + SelectObjectType | undefined + >(props.defaultValue); + + const value = isControlled ? props.value : internalValue; + + const getValueFn = + (haveGetValueFn && props.getValueFn) || defaultGetValueFn; + const getLabelFn = + (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn; + + const data = useMemo(() => { + const propsData = props.filterBy + ? props.data.filter(props.filterBy) + : props.data; + if (props.groupBy) { + const groupedData = groupBy(propsData, props.groupBy); + return Object.entries(groupedData).map(([group, items]) => ({ + group, + items: items.map(item => ({ + label: getLabelFn(item), + value: getValueFn(item), + })), + })); + } + return propsData.map(item => ({ + label: getLabelFn(item), + value: getValueFn(item), + })); + }, [props.data, props.groupBy]); + + const handleOnChange = (event: string | null) => { + if (!event) return; + const object = props.data.find(item => event === getValueFn(item)); + if (!object) return; + if (isControlled) { + props.onChange(object); + return; + } + setInternalValue(object); + }; + + useEffect(() => { + if (isControlled || !internalValue) return; + props.onChange(internalValue); + }, [internalValue]); + + const restProps = omit(props, [ + "filterBy", + "groupBy", + "getValueFn", + "getLabelFn", + ]); + return ( +