dont work

This commit is contained in:
Виталий Лавшонок
2025-11-03 20:36:29 +03:00
parent 91aa3e1f80
commit 9a2c2a9589
7 changed files with 175 additions and 44 deletions

View File

@@ -48,9 +48,8 @@ export const Modal: React.FC<ModalProps> = ({
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className={cn( className={cn(
" fixed top-0 left-0 h-svh w-svw backdrop-filter transition-all z-50", " fixed top-0 left-0 h-svh w-svw backdrop-filter transition-all z-50",
// " pointer-events-none", backdrop == "blur" && open && "backdrop-blur-sm",
backdrop == "blur" && open ? "backdrop-blur-sm" : "", backdrop == "opaque" && open && "bg-[#00000055] pointer-events-none",
backdrop == "opaque" && open ? "bg-[#00000055]" : ""
)} )}
></motion.div> ></motion.div>
)} )}

View File

@@ -3,6 +3,8 @@ import axios from "../../axios";
// ─── Типы ──────────────────────────────────────────── // ─── Типы ────────────────────────────────────────────
type Status = "idle" | "loading" | "successful" | "failed";
export interface GroupMember { export interface GroupMember {
userId: number; userId: number;
username: string; username: string;
@@ -20,17 +22,34 @@ export interface Group {
interface GroupsState { interface GroupsState {
groups: Group[]; groups: Group[];
currentGroup: Group | null; currentGroup: Group | null;
status: "idle" | "loading" | "successful" | "failed"; statuses: {
create: Status;
update: Status;
delete: Status;
fetchMy: Status;
fetchById: Status;
addMember: Status;
removeMember: Status;
};
error: string | null; error: string | null;
} }
const initialState: GroupsState = { const initialState: GroupsState = {
groups: [], groups: [],
currentGroup: null, currentGroup: null,
status: "idle", statuses: {
create: "idle",
update: "idle",
delete: "idle",
fetchMy: "idle",
fetchById: "idle",
addMember: "idle",
removeMember: "idle",
},
error: null, error: null,
}; };
// ─── Async Thunks ───────────────────────────────────── // ─── Async Thunks ─────────────────────────────────────
// POST /groups // POST /groups
@@ -146,111 +165,117 @@ const groupsSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
// ─── CREATE GROUP ─── // ─── CREATE GROUP ───
builder.addCase(createGroup.pending, (state) => { builder.addCase(createGroup.pending, (state) => {
state.status = "loading"; state.statuses.create = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(createGroup.fulfilled, (state, action: PayloadAction<Group>) => { builder.addCase(createGroup.fulfilled, (state, action: PayloadAction<Group>) => {
state.status = "successful"; state.statuses.create = "successful";
state.groups.push(action.payload); state.groups.push(action.payload);
}); });
builder.addCase(createGroup.rejected, (state, action: PayloadAction<any>) => { builder.addCase(createGroup.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.create = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── UPDATE GROUP ─── // ─── UPDATE GROUP ───
builder.addCase(updateGroup.pending, (state) => { builder.addCase(updateGroup.pending, (state) => {
state.status = "loading"; state.statuses.update = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(updateGroup.fulfilled, (state, action: PayloadAction<Group>) => { builder.addCase(updateGroup.fulfilled, (state, action: PayloadAction<Group>) => {
state.status = "successful"; state.statuses.update = "successful";
const index = state.groups.findIndex((g) => g.id === action.payload.id); const index = state.groups.findIndex((g) => g.id === action.payload.id);
if (index !== -1) state.groups[index] = action.payload; if (index !== -1) state.groups[index] = action.payload;
if (state.currentGroup?.id === action.payload.id) if (state.currentGroup?.id === action.payload.id) {
state.currentGroup = action.payload; state.currentGroup = action.payload;
}
}); });
builder.addCase(updateGroup.rejected, (state, action: PayloadAction<any>) => { builder.addCase(updateGroup.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.update = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── DELETE GROUP ─── // ─── DELETE GROUP ───
builder.addCase(deleteGroup.pending, (state) => { builder.addCase(deleteGroup.pending, (state) => {
state.status = "loading"; state.statuses.delete = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(deleteGroup.fulfilled, (state, action: PayloadAction<number>) => { builder.addCase(deleteGroup.fulfilled, (state, action: PayloadAction<number>) => {
state.status = "successful"; state.statuses.delete = "successful";
state.groups = state.groups.filter((g) => g.id !== action.payload); state.groups = state.groups.filter((g) => g.id !== action.payload);
if (state.currentGroup?.id === action.payload) state.currentGroup = null; if (state.currentGroup?.id === action.payload) state.currentGroup = null;
}); });
builder.addCase(deleteGroup.rejected, (state, action: PayloadAction<any>) => { builder.addCase(deleteGroup.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.delete = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── FETCH MY GROUPS ─── // ─── FETCH MY GROUPS ───
builder.addCase(fetchMyGroups.pending, (state) => { builder.addCase(fetchMyGroups.pending, (state) => {
state.status = "loading"; state.statuses.fetchMy = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(fetchMyGroups.fulfilled, (state, action: PayloadAction<Group[]>) => { builder.addCase(fetchMyGroups.fulfilled, (state, action: PayloadAction<Group[]>) => {
state.status = "successful"; state.statuses.fetchMy = "successful";
state.groups = action.payload; state.groups = action.payload;
}); });
builder.addCase(fetchMyGroups.rejected, (state, action: PayloadAction<any>) => { builder.addCase(fetchMyGroups.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.fetchMy = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── FETCH GROUP BY ID ─── // ─── FETCH GROUP BY ID ───
builder.addCase(fetchGroupById.pending, (state) => { builder.addCase(fetchGroupById.pending, (state) => {
state.status = "loading"; state.statuses.fetchById = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(fetchGroupById.fulfilled, (state, action: PayloadAction<Group>) => { builder.addCase(fetchGroupById.fulfilled, (state, action: PayloadAction<Group>) => {
state.status = "successful"; state.statuses.fetchById = "successful";
state.currentGroup = action.payload; state.currentGroup = action.payload;
}); });
builder.addCase(fetchGroupById.rejected, (state, action: PayloadAction<any>) => { builder.addCase(fetchGroupById.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.fetchById = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── ADD MEMBER ─── // ─── ADD MEMBER ───
builder.addCase(addGroupMember.pending, (state) => { builder.addCase(addGroupMember.pending, (state) => {
state.status = "loading"; state.statuses.addMember = "loading";
state.error = null; state.error = null;
}); });
builder.addCase(addGroupMember.fulfilled, (state) => { builder.addCase(addGroupMember.fulfilled, (state) => {
state.status = "successful"; state.statuses.addMember = "successful";
}); });
builder.addCase(addGroupMember.rejected, (state, action: PayloadAction<any>) => { builder.addCase(addGroupMember.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.addMember = "failed";
state.error = action.payload; state.error = action.payload;
}); });
// ─── REMOVE MEMBER ─── // ─── REMOVE MEMBER ───
builder.addCase(removeGroupMember.pending, (state) => { builder.addCase(removeGroupMember.pending, (state) => {
state.status = "loading"; state.statuses.removeMember = "loading";
state.error = null; state.error = null;
}); });
builder.addCase( builder.addCase(removeGroupMember.fulfilled, (state, action: PayloadAction<{ groupId: number; memberId: number }>) => {
removeGroupMember.fulfilled, state.statuses.removeMember = "successful";
(state, action: PayloadAction<{ groupId: number; memberId: number }>) => { if (state.currentGroup && state.currentGroup.id === action.payload.groupId) {
state.status = "successful"; state.currentGroup.members = state.currentGroup.members.filter(
if (state.currentGroup && state.currentGroup.id === action.payload.groupId) { (m) => m.userId !== action.payload.memberId
state.currentGroup.members = state.currentGroup.members.filter( );
(m) => m.userId !== action.payload.memberId
);
}
} }
); });
builder.addCase(removeGroupMember.rejected, (state, action: PayloadAction<any>) => { builder.addCase(removeGroupMember.rejected, (state, action: PayloadAction<any>) => {
state.status = "failed"; state.statuses.removeMember = "failed";
state.error = action.payload; state.error = action.payload;
}); });
}, },
}); });

View File

@@ -1,31 +1,37 @@
import { cn } from "../../../lib/cn"; import { cn } from "../../../lib/cn";
import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups"; import { Book, UserAdd, Edit, EyeClosed, EyeOpen } from "../../../assets/icons/groups";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { GroupUpdate } from "./Groups";
export interface GroupItemProps { export interface GroupItemProps {
id: number; id: number;
role: "menager" | "member" | "owner" | "viewer"; role: "menager" | "member" | "owner" | "viewer";
visible: boolean; visible: boolean;
name: string; name: string;
description: string;
setUpdateActive: (value: any) => void;
setUpdateGroup: (value: GroupUpdate) => void;
} }
interface IconComponentProps { interface IconComponentProps {
src: string; src: string;
onClick?: () => void;
} }
const IconComponent: React.FC<IconComponentProps> = ({ const IconComponent: React.FC<IconComponentProps> = ({
src src, onClick = () => void
}) => { }) => {
return <img return <img
src={src} src={src}
onClick={() => onClick()}
className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300" className="hover:bg-liquid-light rounded-[5px] cursor-pointer transition-all duration-300"
/> />
} }
const GroupItem: React.FC<GroupItemProps> = ({ const GroupItem: React.FC<GroupItemProps> = ({
id, name, visible, role id, name, visible, role, description, setUpdateGroup, setUpdateActive
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -45,7 +51,10 @@ const GroupItem: React.FC<GroupItemProps> = ({
(role == "menager" || role == "owner") && <IconComponent src={UserAdd}/> (role == "menager" || role == "owner") && <IconComponent src={UserAdd}/>
} }
{ {
(role == "menager" || role == "owner") && <IconComponent src={Edit}/> (role == "menager" || role == "owner") && <IconComponent src={Edit} onClick={() => {
setUpdateGroup({id, });
setUpdateActive(true);
}} />
} }
{ {
visible == false && <IconComponent src={EyeOpen} /> visible == false && <IconComponent src={EyeOpen} />

View File

@@ -5,12 +5,23 @@ import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import GroupsBlock from "./GroupsBlock"; import GroupsBlock from "./GroupsBlock";
import { setMenuActivePage } from "../../../redux/slices/store"; import { setMenuActivePage } from "../../../redux/slices/store";
import { fetchMyGroups } from "../../../redux/slices/groups"; import { fetchMyGroups } from "../../../redux/slices/groups";
import { Modal } from "../../../components/modal/Modal"; import ModalCreate from "./ModalCreate";
import ModalUpdate from "./ModalUpdate";
export interface GroupUpdate {
id: number;
name: string;
description: string;
}
const Groups = () => { const Groups = () => {
const [modalActive, setModalActive] = useState<boolean>(false); const [modalActive, setModalActive] = useState<boolean>(false);
const [modelUpdateActive, setModalUpdateActive] = useState<boolean>(false);
const [updateGroup, setUpdateGroup] = useState<GroupUpdate>({id: 0, name: "", description: ""});
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
// Берём группы из стора // Берём группы из стора
const groups = useAppSelector((store) => store.groups.groups); const groups = useAppSelector((store) => store.groups.groups);
@@ -84,9 +95,8 @@ const Groups = () => {
</div> </div>
<Modal className="bg-liquid-lighter p-[20px] rounded-[20px]" onOpenChange={setModalActive} open={modalActive} backdrop="blur" > <ModalCreate setActive={setModalActive} active={modalActive} />
<div>modal</div> <ModalUpdate setActive={setModalUpdateActive} active={modelUpdateActive} groupId={updateGroup.id} groupName={updateGroup.name}/>
</Modal>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,43 @@
import { FC, useEffect, useState } from "react";
import { Modal } from "../../../components/modal/Modal";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { createGroup } from "../../../redux/slices/groups";
interface ModalCreateProps {
active: boolean;
setActive: (value: boolean) => void;
}
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const status = useAppSelector((state) => state.groups.statuses.create);
const dispatch = useAppDispatch();
useEffect(() => {
if (status == "successful"){
setActive(false);
}
}, [status]);
return (
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
<div className="w-[500px]">
<div className="font-bold text-[30px]">Создать группу</div>
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Название" onChange={(v) => { setName(v)}} placeholder="login" />
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v)}} placeholder="login" />
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton onClick={() => {dispatch(createGroup({name, description}))}} text="Создать" disabled={status=="loading"}/>
<SecondaryButton onClick={() => {setActive(false);}} text="Отмена" />
</div>
</div>
</Modal>
);
};
export default ModalCreate;

View File

@@ -0,0 +1,45 @@
import { FC, useEffect, useState } from "react";
import { Modal } from "../../../components/modal/Modal";
import { PrimaryButton } from "../../../components/button/PrimaryButton";
import { SecondaryButton } from "../../../components/button/SecondaryButton";
import { Input } from "../../../components/input/Input";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { createGroup } from "../../../redux/slices/groups";
interface ModalUpdateProps {
active: boolean;
setActive: (value: boolean) => void;
groupId: number;
groupName: string;
}
const ModalUpdate: FC<ModalUpdateProps> = ({ active, setActive, groupName, groupId }) => {
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const status = useAppSelector((state) => state.groups.statuses.create);
const dispatch = useAppDispatch();
useEffect(() => {
if (status == "successful"){
setActive(false);
}
}, [status]);
return (
<Modal className="bg-liquid-background border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white" onOpenChange={setActive} open={active} backdrop="blur" >
<div className="w-[500px]">
<div className="font-bold text-[30px]">Изменить группу {groupName} #{groupId}</div>
<Input name="name" autocomplete="name" className="mt-[10px]" type="text" label="Новое название" defaultState={groupName} onChange={(v) => { setName(v)}} placeholder="login" />
<Input name="description" autocomplete="description" className="mt-[10px]" type="text" label="Описание" onChange={(v) => { setDescription(v)}} placeholder="login" />
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton onClick={() => {dispatch(createGroup({name, description}))}} text="Обновить" disabled={status=="loading"}/>
<SecondaryButton onClick={() => {setActive(false);}} text="Отмена" />
</div>
</div>
</Modal>
);
};
export default ModalUpdate;

View File

@@ -6,7 +6,7 @@ import { DropDownList } from "../../../components/drop-down-list/DropDownList";
const languageMap: Record<string, string> = { const languageMap: Record<string, string> = {
c: "cpp", c: "cpp",
cpp: "cpp", "C++": "cpp",
java: "java", java: "java",
python: "python", python: "python",
pascal: "pascal", pascal: "pascal",