From f52fde00973e9d9a9f14dc7c3e3d03b67ea0cf9f Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 8 Aug 2025 11:31:27 +0400 Subject: [PATCH] feat: status creating --- src/app/deals/components/Board/Board.tsx | 94 ++++----------- src/app/deals/components/Board/BoardMenu.tsx | 11 +- .../CreateBoardButton/CreateBoardButton.tsx | 52 ++------- .../CreateStatusButton/CreateStatusButton.tsx | 34 ++++++ src/app/deals/components/Funnel/Funnel.tsx | 95 +++++++++------ .../StatusColumnWrapper.tsx | 101 +++++----------- .../StatusColumnWrapper/StatusMenu.tsx | 9 +- src/components/dnd/FunnelDnd/FunnelDnd.tsx | 108 +++++++++--------- .../ui/InPlaceInput/InPlaceInput.tsx | 85 ++++++++++++++ 9 files changed, 308 insertions(+), 281 deletions(-) create mode 100644 src/app/deals/components/CreateStatusButton/CreateStatusButton.tsx create mode 100644 src/components/ui/InPlaceInput/InPlaceInput.tsx diff --git a/src/app/deals/components/Board/Board.tsx b/src/app/deals/components/Board/Board.tsx index fbda00c..97392da 100644 --- a/src/app/deals/components/Board/Board.tsx +++ b/src/app/deals/components/Board/Board.tsx @@ -1,7 +1,8 @@ -import React, { FC, useEffect, useRef, useState } from "react"; -import { Group, Text, TextInput } from "@mantine/core"; +import React, { FC, useState } from "react"; +import { Box, Group, Text } from "@mantine/core"; import BoardMenu from "@/app/deals/components/Board/BoardMenu"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; +import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; import { BoardSchema } from "@/lib/client"; type Props = { @@ -10,48 +11,7 @@ type Props = { const Board: FC = ({ board }) => { const [isHovered, setIsHovered] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(board.name); const { onUpdateBoard } = useBoardsContext(); - const inputRef = useRef(null); - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - } - }, [isEditing]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - isEditing && - inputRef.current && - !inputRef.current.contains(event.target as Node) - ) { - handleSave(); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - }, [isEditing, editValue]); - - const handleSave = () => { - const newValue = editValue.trim(); - if (newValue && newValue !== board.name) { - onUpdateBoard(board.id, { name: newValue }); - } - setIsEditing(false); - }; - - const handleEdit = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - setEditValue(board.name); - setIsEditing(true); - }; return ( = ({ board }) => { style={{ borderWidth: 1 }} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> - {isEditing ? ( - setEditValue(e.target.value)} - onKeyDown={e => { - if (e.key === "Enter") handleSave(); - if (e.key === "Escape") { - setEditValue(board.name); - setIsEditing(false); - } - }} - variant="unstyled" - styles={{ - input: { - height: 25, - minHeight: 25, - }, - }} - /> - ) : ( - {board.name} - )} - onUpdateBoard(board.id, { name: value })} + inputStyles={{ + input: { + height: 25, + minHeight: 25, + }, + }} + getChildren={startEditing => ( + <> + + {board.name} + + + + )} /> ); diff --git a/src/app/deals/components/Board/BoardMenu.tsx b/src/app/deals/components/Board/BoardMenu.tsx index fa470d3..c566512 100644 --- a/src/app/deals/components/Board/BoardMenu.tsx +++ b/src/app/deals/components/Board/BoardMenu.tsx @@ -7,10 +7,10 @@ import { BoardSchema } from "@/lib/client"; type Props = { isHovered: boolean; board: BoardSchema; - handleEdit: (e: React.MouseEvent) => void; + startEditing: () => void; }; -const BoardMenu: FC = ({ isHovered, board, handleEdit }) => { +const BoardMenu: FC = ({ isHovered, board, startEditing }) => { const { selectedBoard, onDeleteBoard } = useBoardsContext(); return ( @@ -30,7 +30,12 @@ const BoardMenu: FC = ({ isHovered, board, handleEdit }) => { - + { + e.preventDefault(); + e.stopPropagation(); + startEditing(); + }}> Переименовать diff --git a/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx index 6a771bf..88b557f 100644 --- a/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx +++ b/src/app/deals/components/CreateBoardButton/CreateBoardButton.tsx @@ -1,52 +1,22 @@ -import { useState } from "react"; -import { IconCheck, IconPlus, IconX } from "@tabler/icons-react"; -import { Box, Group, TextInput } from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import { Box } from "@mantine/core"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; +import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; const CreateBoardButton = () => { const { onCreateBoard } = useBoardsContext(); - const [isWriting, setIsWriting] = useState(false); - const [name, setName] = useState(""); - - const onStartCreating = () => { - setName(""); - setIsWriting(true); - }; - - const onCancelCreating = () => setIsWriting(false); - - const onCompleteCreating = () => { - if (name) { - onCreateBoard(name); - } - setIsWriting(false); - }; return ( - {isWriting ? ( - - setName(e.target.value)} - onKeyDown={e => - e.key === "Enter" && onCompleteCreating() - } - /> - - + ( + + - - - - - ) : ( - - - - )} + )} + /> ); }; diff --git a/src/app/deals/components/CreateStatusButton/CreateStatusButton.tsx b/src/app/deals/components/CreateStatusButton/CreateStatusButton.tsx new file mode 100644 index 0000000..fcef172 --- /dev/null +++ b/src/app/deals/components/CreateStatusButton/CreateStatusButton.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Box, Text } from "@mantine/core"; +import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; +import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; + +const CreateStatusButton = () => { + const { onCreateStatus } = useStatusesContext(); + + return ( + + ( + startEditing()}> + Создать колонку + + )} + /> + + ); +}; + +export default CreateStatusButton; diff --git a/src/app/deals/components/Funnel/Funnel.tsx b/src/app/deals/components/Funnel/Funnel.tsx index da88377..94f1b7d 100644 --- a/src/app/deals/components/Funnel/Funnel.tsx +++ b/src/app/deals/components/Funnel/Funnel.tsx @@ -1,6 +1,8 @@ "use client"; import React, { FC, ReactNode } from "react"; +import { Group, ScrollArea } from "@mantine/core"; +import CreateStatusButton from "@/app/deals/components/CreateStatusButton/CreateStatusButton"; import DealCard from "@/app/deals/components/DealCard/DealCard"; import DealContainer from "@/app/deals/components/DealContainer/DealContainer"; import StatusColumnWrapper from "@/app/deals/components/StatusColumnWrapper/StatusColumnWrapper"; @@ -23,45 +25,62 @@ const Funnel: FC = () => { } = useDealsAndStatusesDnd(); return ( - `${status.id}-status`} - getItemsByContainer={(status: StatusSchema, items: DealSchema[]) => - sortByLexorank( - items.filter(deal => deal.statusId === status.id) - ) - } - renderContainer={( - status: StatusSchema, - funnelColumnComponent: ReactNode - ) => ( - - {funnelColumnComponent} - - )} - renderItem={(deal: DealSchema) => ( - + + + `${status.id}-status` + } + getItemsByContainer={( + status: StatusSchema, + items: DealSchema[] + ) => + sortByLexorank( + items.filter(deal => deal.statusId === status.id) + ) + } + renderContainer={( + status: StatusSchema, + funnelColumnComponent: ReactNode + ) => ( + + {funnelColumnComponent} + + )} + renderItem={(deal: DealSchema) => ( + + )} + activeContainer={activeStatus} + activeItem={activeDeal} + renderItemOverlay={(deal: DealSchema) => ( + + )} + renderContainerOverlay={( + status: StatusSchema, + children + ) => ( + + {children} + + )} /> - )} - activeContainer={activeStatus} - activeItem={activeDeal} - renderItemOverlay={(deal: DealSchema) => } - renderContainerOverlay={(status: StatusSchema, children) => ( - - {children} - - )} - /> + + + ); }; diff --git a/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx b/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx index c884fd2..2c80b22 100644 --- a/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx +++ b/src/app/deals/components/StatusColumnWrapper/StatusColumnWrapper.tsx @@ -1,7 +1,8 @@ -import React, { ReactNode, useEffect, useRef, useState } from "react"; -import { Box, Group, Text, TextInput } from "@mantine/core"; +import React, { ReactNode } from "react"; +import { Box, Group, Text } from "@mantine/core"; import StatusMenu from "@/app/deals/components/StatusColumnWrapper/StatusMenu"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; +import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput"; import { StatusSchema } from "@/lib/client"; type Props = { @@ -16,46 +17,12 @@ const StatusColumnWrapper = ({ isDragging = false, }: Props) => { const { onUpdateStatus } = useStatusesContext(); - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(status.name); - const inputRef = useRef(null); - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - } - }, [isEditing]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - isEditing && - inputRef.current && - !inputRef.current.contains(event.target as Node) - ) { - handleSave(); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - }, [isEditing, editValue]); - - const handleEdit = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - setEditValue(status.name); - setIsEditing(true); - }; - - const handleSave = () => { - const newValue = editValue.trim(); + const handleSave = (value: string) => { + const newValue = value.trim(); if (newValue && newValue !== status.name) { onUpdateStatus(status.id, { name: newValue }); } - setIsEditing(false); }; return ( @@ -72,39 +39,31 @@ const StatusColumnWrapper = ({ justify={"space-between"} ml={"xs"} mb={"xs"}> - {isEditing ? ( - setEditValue(e.target.value)} - onKeyDown={e => { - if (e.key === "Enter") handleSave(); - if (e.key === "Escape") { - setEditValue(status.name); - setIsEditing(false); - } - }} - variant="unstyled" - styles={{ - input: { - height: 25, - minHeight: 25, - }, - }} - /> - ) : ( - - {status.name} - - )} - handleSave(value)} + inputStyles={{ + input: { + height: 25, + minHeight: 25, + }, + }} + getChildren={startEditing => ( + <> + + {status.name} + + + + )} /> {children} diff --git a/src/app/deals/components/StatusColumnWrapper/StatusMenu.tsx b/src/app/deals/components/StatusColumnWrapper/StatusMenu.tsx index e5b4d30..5476f5f 100644 --- a/src/app/deals/components/StatusColumnWrapper/StatusMenu.tsx +++ b/src/app/deals/components/StatusColumnWrapper/StatusMenu.tsx @@ -6,7 +6,7 @@ import { StatusSchema } from "@/lib/client"; type Props = { status: StatusSchema; - handleEdit: (e: React.MouseEvent) => void; + handleEdit: () => void; }; const StatusMenu: FC = ({ status, handleEdit }) => { @@ -28,7 +28,12 @@ const StatusMenu: FC = ({ status, handleEdit }) => { - + { + e.preventDefault(); + e.stopPropagation(); + handleEdit(); + }}> Переименовать diff --git a/src/components/dnd/FunnelDnd/FunnelDnd.tsx b/src/components/dnd/FunnelDnd/FunnelDnd.tsx index bdd5683..c82f8c1 100644 --- a/src/components/dnd/FunnelDnd/FunnelDnd.tsx +++ b/src/components/dnd/FunnelDnd/FunnelDnd.tsx @@ -12,7 +12,7 @@ import { horizontalListSortingStrategy, SortableContext, } from "@dnd-kit/sortable"; -import { Group, ScrollArea } from "@mantine/core"; +import { Group } from "@mantine/core"; import useDndSensors from "@/app/deals/hooks/useSensors"; import SortableItem from "@/components/dnd/SortableItem"; import { BaseDraggable } from "@/components/dnd/types/types"; @@ -59,67 +59,63 @@ const FunnelDnd = < const sensors = useDndSensors(); return ( - - - - - {containers.map(container => { - const containerItems = getItemsByContainer( - container, - items - ); - const containerId = getContainerId(container); - return ( - - {renderContainer( - container, - - )} - - ); - })} - { - const containerItems = getItemsByContainer( - container, - items - ); - const containerId = getContainerId(container); - return renderContainerOverlay( + + + + {containers.map(container => { + const containerItems = getItemsByContainer( + container, + items + ); + const containerId = getContainerId(container); + return ( + + {renderContainer( container, - ); - }} - renderItem={renderItemOverlay} - /> - - - - + )} + + ); + })} + { + const containerItems = getItemsByContainer( + container, + items + ); + const containerId = getContainerId(container); + return renderContainerOverlay( + container, + + ); + }} + renderItem={renderItemOverlay} + /> + + + ); }; diff --git a/src/components/ui/InPlaceInput/InPlaceInput.tsx b/src/components/ui/InPlaceInput/InPlaceInput.tsx new file mode 100644 index 0000000..ebd6080 --- /dev/null +++ b/src/components/ui/InPlaceInput/InPlaceInput.tsx @@ -0,0 +1,85 @@ +import React, { FC, ReactNode, useEffect, useRef, useState } from "react"; +import { TextInput } from "@mantine/core"; +import { Styles } from "@mantine/core/lib/core/styles-api/styles-api.types"; + +type Props = { + defaultValue?: string; + onComplete: (value: string) => void; + placeholder?: string; + getChildren: (startEditing: () => void) => ReactNode; + inputStyles?: Styles; +}; + +const InPlaceInput: FC = ({ + onComplete, + placeholder, + inputStyles, + getChildren, + defaultValue = "", +}) => { + const [isWriting, setIsWriting] = useState(false); + const [value, setValue] = useState(defaultValue); + console.log(value); + const inputRef = useRef(null); + + useEffect(() => { + if (isWriting && inputRef.current) { + inputRef.current.focus(); + } + }, [isWriting]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + isWriting && + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + onCompleteCreating(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, [isWriting, value]); + + const onStartCreating = () => { + setValue(defaultValue); + setIsWriting(true); + }; + + const onCancelCreating = () => { + setValue(defaultValue); + setIsWriting(false); + }; + + const onCompleteCreating = () => { + const localValue = value.trim(); + if (localValue) { + onComplete(localValue); + } + setIsWriting(false); + }; + + if (isWriting) { + return ( + setValue(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") onCompleteCreating(); + if (e.key === "Escape") onCancelCreating(); + }} + styles={inputStyles} + /> + ); + } + + return getChildren(onStartCreating); +}; + +export default InPlaceInput;