feat: projects redux storage and select

This commit is contained in:
2025-07-30 17:44:30 +04:00
parent b8d431ae99
commit cb168b6415
10 changed files with 226 additions and 2 deletions

View File

@ -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",

View File

@ -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 (
<Group
justify={"flex-end"}
w={"100%"}>
<ProjectSelect
data={projectsState.projects}
value={projectsState.selectedProject}
onChange={value => value && dispatch(selectProject(value))}
/>
</Group>
);
};
export default Header;

View File

@ -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 (
<PageContainer>
<PageBlock>
<Header />
<Boards />
</PageBlock>
</PageContainer>

View File

@ -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> = T;
type ControlledValueProps<T> = {
value: SelectObjectType<T>;
onChange: (value: SelectObjectType<T>) => void;
};
type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: SelectObjectType<T>) => string;
getValueFn: (item: SelectObjectType<T>) => string;
};
type RestProps<T> = {
defaultValue?: SelectObjectType<T>;
onChange: (value: SelectObjectType<T>) => void;
data: SelectObjectType<T>[];
groupBy?: (item: SelectObjectType<T>) => string;
filterBy?: (item: SelectObjectType<T>) => boolean;
};
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name;
};
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
if (!item) return item;
return item.id.toString();
};
export type ObjectSelectProps<T> = (RestProps<T> &
Partial<ControlledValueProps<T>>) &
Omit<SelectProps, "value" | "onChange" | "data"> &
(T extends ObjectWithIdAndName
? Partial<CustomLabelAndKeyProps<T>>
: CustomLabelAndKeyProps<T>);
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
const isControlled = "value" in props;
const haveGetValueFn = "getValueFn" in props;
const haveGetLabelFn = "getLabelFn" in props;
const [internalValue, setInternalValue] = useState<
SelectObjectType<T> | 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 (
<Select
{...restProps}
value={value && getValueFn(value)}
onChange={handleOnChange}
data={data}
/>
);
};
export default ObjectSelect;

View File

@ -0,0 +1,26 @@
"use client";
import { FC } from "react";
import { ProjectSchema } from "@/types/ProjectSchema";
import ObjectSelect, { ObjectSelectProps } from "@/components/selects/ObjectSelect/ObjectSelect";
type Props = Omit<
ObjectSelectProps<ProjectSchema | null>,
"getLabelFn" | "getValueFn"
>;
const ProjectSelect: FC<Props> = ({ data, ...props }) => {
const onClear = () => props.onChange(null);
return (
<ObjectSelect
data={data}
searchable
placeholder={"Выберите проект"}
onClear={onClear}
{...props}
/>
);
};
export default ProjectSelect;

View File

@ -0,0 +1,29 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ProjectSchema } from "@/types/ProjectSchema";
interface ProjectsState {
projects: ProjectSchema[];
selectedProject: ProjectSchema | null;
}
const initialState: ProjectsState = {
projects: [],
selectedProject: null,
};
export const projectsSlice = createSlice({
name: "projects",
initialState,
reducers: {
setProjects: (state, action: PayloadAction<ProjectSchema[]>) => {
state.projects = action.payload;
},
selectProject: (state, action: PayloadAction<ProjectSchema>) => {
state.selectedProject = action.payload;
},
},
});
export const { setProjects, selectProject } = projectsSlice.actions;
export default projectsSlice.reducer;

View File

@ -1,8 +1,10 @@
import { combineReducers } from "@reduxjs/toolkit";
import authReducer from "@/lib/features/auth/authSlice";
import projectsReducer from "@/lib/features/projects/projectsSlice";
const rootReducer = combineReducers({
auth: authReducer,
projectsState: projectsReducer,
});
export default rootReducer;

View File

@ -13,8 +13,8 @@ export const myColor: MantineColorsTuple = [
"#00718c",
];
const radius = "lg";
const size = "lg";
const radius = "md";
const size = "md";
export const theme = createTheme({
colors: {

View File

@ -0,0 +1,4 @@
export type ProjectSchema = {
id: number;
name: string;
};

View File

@ -3904,6 +3904,13 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash@npm:^4.17.20":
version: 4.17.20
resolution: "@types/lodash@npm:4.17.20"
checksum: 10c0/98cdd0faae22cbb8079a01a3bb65aa8f8c41143367486c1cbf5adc83f16c9272a2a5d2c1f541f61d0d73da543c16ee1d21cf2ef86cb93cd0cc0ac3bced6dd88f
languageName: node
linkType: hard
"@types/node@npm:*":
version: 24.1.0
resolution: "@types/node@npm:24.1.0"
@ -5919,6 +5926,7 @@ __metadata:
"@testing-library/user-event": "npm:^14.6.1"
"@types/eslint-plugin-jsx-a11y": "npm:^6"
"@types/jest": "npm:^29.5.14"
"@types/lodash": "npm:^4.17.20"
"@types/node": "npm:^22.13.11"
"@types/react": "npm:19.1.8"
"@types/react-redux": "npm:^7.1.34"