feat: boards dnd editor for mobile

This commit is contained in:
2025-08-09 10:13:25 +04:00
parent 5ecdd3d887
commit e3137de46d
11 changed files with 166 additions and 17 deletions

View File

@ -36,7 +36,7 @@ const Boards = () => {
renderItem={renderBoard} renderItem={renderBoard}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onItemClick={selectBoard} onItemClick={selectBoard}
rowStyle={{ flexWrap: "nowrap" }} containerStyle={{ flexWrap: "nowrap" }}
disabled={isMobile} disabled={isMobile}
/> />
<CreateBoardButton /> <CreateBoardButton />

View File

@ -7,7 +7,9 @@ const CreateBoardButton = () => {
const { onCreateBoard } = useBoardsContext(); const { onCreateBoard } = useBoardsContext();
return ( return (
<Box style={{ cursor: "pointer" }}> <Box
style={{ cursor: "pointer" }}
pr={"md"}>
<InPlaceInput <InPlaceInput
placeholder={"Название доски"} placeholder={"Название доски"}
onComplete={onCreateBoard} onComplete={onCreateBoard}

View File

@ -1,22 +1,50 @@
"use client"; "use client";
import { Group } from "@mantine/core"; import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
import { isMobile } from "react-device-detect";
import { Box, Group, Text } from "@mantine/core";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect"; import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
const Header = () => { const Header = () => {
const { projects, setSelectedProject, selectedProject } = const { projects, setSelectedProject, selectedProject } =
useProjectsContext(); useProjectsContext();
const { setIsEditorDrawerOpened } = useBoardsContext();
return ( const getDesktopHeader = () => {
<Group return (
justify={"flex-end"}
w={"100%"}>
<ProjectSelect <ProjectSelect
data={projects} data={projects}
value={selectedProject} value={selectedProject}
onChange={value => value && setSelectedProject(value)} onChange={value => value && setSelectedProject(value)}
/> />
);
};
const getMobileHeader = () => {
return (
<Group
justify={"space-between"}
w={"100%"}>
<Box p={"md"}>
<IconChevronLeft />
</Box>
<Text>{selectedProject?.name}</Text>
<Box
p={"md"}
onClick={() => setIsEditorDrawerOpened(true)}>
<IconSettings />
</Box>
</Group>
);
};
return (
<Group
justify={"flex-end"}
w={"100%"}>
{isMobile ? getMobileHeader() : getDesktopHeader()}
</Group> </Group>
); );
}; };

View File

@ -21,6 +21,8 @@ type BoardsContextState = {
onCreateBoard: (name: string) => void; onCreateBoard: (name: string) => void;
onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void; onUpdateBoard: (boardId: number, board: UpdateBoardSchema) => void;
onDeleteBoard: (board: BoardSchema) => void; onDeleteBoard: (board: BoardSchema) => void;
isEditorDrawerOpened: boolean;
setIsEditorDrawerOpened: React.Dispatch<React.SetStateAction<boolean>>;
}; };
const BoardsContext = createContext<BoardsContextState | undefined>(undefined); const BoardsContext = createContext<BoardsContextState | undefined>(undefined);
@ -35,6 +37,8 @@ const useBoardsContextState = () => {
const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>( const [selectedBoard, setSelectedBoard] = useState<BoardSchema | null>(
null null
); );
const [isEditorDrawerOpened, setIsEditorDrawerOpened] =
useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (boards.length > 0 && selectedBoard === null) { if (boards.length > 0 && selectedBoard === null) {
@ -70,6 +74,8 @@ const useBoardsContextState = () => {
onCreateBoard, onCreateBoard,
onUpdateBoard, onUpdateBoard,
onDeleteBoard, onDeleteBoard,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
}; };
}; };

View File

@ -0,0 +1,69 @@
"use client";
import React, { FC } from "react";
import { IconChevronLeft } from "@tabler/icons-react";
import { Box, Center, Drawer, Group, rem, Text } from "@mantine/core";
import Board from "@/app/deals/components/Board/Board";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import CreateBoardButtonMobile from "@/app/deals/drawers/ProjectBoardsEditorDrawer/components/CreateBoardButtonMobile";
import SortableDnd from "@/components/dnd/SortableDnd";
import { BoardSchema } from "@/lib/client";
const ProjectBoardsEditorDrawer: FC = () => {
const {
boards,
onUpdateBoard,
isEditorDrawerOpened,
setIsEditorDrawerOpened,
} = useBoardsContext();
const { selectedProject } = useProjectsContext();
const onClose = () => setIsEditorDrawerOpened(false);
const renderBoard = (board: BoardSchema) => {
return <Board board={board} />;
};
const onDragEnd = (itemId: number, newLexorank: string) => {
onUpdateBoard(itemId, { lexorank: newLexorank });
};
return (
<Drawer
size={"100%"}
position={"right"}
onClose={onClose}
removeScrollProps={{ allowPinchZoom: true }}
withCloseButton={false}
opened={isEditorDrawerOpened}
styles={{
body: {
height: "100%",
display: "flex",
flexDirection: "column",
gap: rem(10),
},
}}>
<Group justify={"space-between"}>
<Box
onClick={onClose}
p={"xs"}>
<IconChevronLeft />
</Box>
<Center>
<Text>{selectedProject?.name}</Text>
</Center>
<Box p={"lg"} />
</Group>
<SortableDnd
initialItems={boards}
renderItem={renderBoard}
onDragEnd={onDragEnd}
vertical
/>
<CreateBoardButtonMobile />
</Drawer>
);
};
export default ProjectBoardsEditorDrawer;

View File

@ -0,0 +1,30 @@
import { IconPlus } from "@tabler/icons-react";
import { Box, Group, Text } from "@mantine/core";
import { modals } from "@mantine/modals";
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
const CreateBoardButtonMobile = () => {
const { onCreateBoard } = useBoardsContext();
const onStartCreating = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание доски",
withCloseButton: true,
innerProps: {
onComplete: onCreateBoard,
},
});
};
return (
<Group onClick={onStartCreating}>
<IconPlus />
<Box mt={5}>
<Text>Создать доску</Text>
</Box>
</Group>
);
};
export default CreateBoardButtonMobile;

View File

@ -0,0 +1,3 @@
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer/ProjectBoardsEditorDrawer";
export default ProjectBoardsEditorDrawer;

View File

@ -5,6 +5,7 @@ import Header from "@/app/deals/components/Header/Header";
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext"; import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext"; import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext"; import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
import ProjectBoardsEditorDrawer from "@/app/deals/drawers/ProjectBoardsEditorDrawer";
import PageBlock from "@/components/layout/PageBlock/PageBlock"; import PageBlock from "@/components/layout/PageBlock/PageBlock";
import PageContainer from "@/components/layout/PageContainer/PageContainer"; import PageContainer from "@/components/layout/PageContainer/PageContainer";
import { DealsContextProvider } from "./contexts/DealsContext"; import { DealsContextProvider } from "./contexts/DealsContext";
@ -23,6 +24,7 @@ export default function DealsPage() {
<Funnel /> <Funnel />
</DealsContextProvider> </DealsContextProvider>
</StatusesContextProvider> </StatusesContextProvider>
<ProjectBoardsEditorDrawer />
</BoardsContextProvider> </BoardsContextProvider>
</ProjectsContextProvider> </ProjectsContextProvider>
</PageBlock> </PageBlock>

View File

@ -10,7 +10,7 @@ import React, {
import { Active, DndContext, DragEndEvent } from "@dnd-kit/core"; import { Active, DndContext, DragEndEvent } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable"; import { SortableContext } from "@dnd-kit/sortable";
import { LexoRank } from "lexorank"; import { LexoRank } from "lexorank";
import { Box, Group } from "@mantine/core"; import { Box, Flex } from "@mantine/core";
import useDndSensors from "@/app/deals/hooks/useSensors"; import useDndSensors from "@/app/deals/hooks/useSensors";
import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay"; import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay";
import SortableItem from "@/components/dnd/SortableItem"; import SortableItem from "@/components/dnd/SortableItem";
@ -25,9 +25,10 @@ type Props<T extends BaseItem> = {
initialItems: T[]; initialItems: T[];
renderItem: (item: T) => ReactNode; renderItem: (item: T) => ReactNode;
onDragEnd: (itemId: number, newLexorank: string) => void; onDragEnd: (itemId: number, newLexorank: string) => void;
onItemClick: (item: T) => void; onItemClick?: (item: T) => void;
rowStyle?: CSSProperties; containerStyle?: CSSProperties;
itemStyle?: CSSProperties; itemStyle?: CSSProperties;
vertical?: boolean;
disabled?: boolean; disabled?: boolean;
}; };
@ -36,8 +37,9 @@ const SortableDnd = <T extends BaseItem>({
renderItem, renderItem,
onDragEnd, onDragEnd,
onItemClick, onItemClick,
rowStyle, containerStyle,
itemStyle, itemStyle,
vertical,
disabled = false, disabled = false,
}: Props<T>) => { }: Props<T>) => {
const [active, setActive] = useState<Active | null>(null); const [active, setActive] = useState<Active | null>(null);
@ -100,14 +102,19 @@ const SortableDnd = <T extends BaseItem>({
<SortableContext <SortableContext
items={items} items={items}
disabled={disabled}> disabled={disabled}>
<Group <Flex
gap={0} gap={0}
style={rowStyle} style={{
flexWrap: "nowrap",
flexDirection: vertical ? "column" : "row",
...containerStyle,
}}
role="application"> role="application">
{items.map((item, index) => ( {items.map((item, index) => (
<Box <Box
key={index} key={index}
onClick={e => { onClick={e => {
if (!onItemClick) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onItemClick(item); onItemClick(item);
@ -121,7 +128,7 @@ const SortableDnd = <T extends BaseItem>({
</SortableItem> </SortableItem>
</Box> </Box>
))} ))}
</Group> </Flex>
</SortableContext> </SortableContext>
<SortableOverlay> <SortableOverlay>
{activeItem ? renderItem(activeItem) : null} {activeItem ? renderItem(activeItem) : null}

View File

@ -1,5 +1,4 @@
.container { .container {
border-radius: rem(40);
background-color: white; background-color: white;
@mixin dark { @mixin dark {
background-color: var(--mantine-color-dark-8); background-color: var(--mantine-color-dark-8);
@ -8,7 +7,10 @@
@mixin light { @mixin light {
box-shadow: 5px 5px 24px rgba(0, 0, 0, 0.16); box-shadow: 5px 5px 24px rgba(0, 0, 0, 0.16);
} }
padding: rem(35); @media (min-width: 48em) {
padding: rem(35);
border-radius: rem(40);
}
} }
.container-full-height { .container-full-height {

View File

@ -20,7 +20,7 @@ const EnterNameModal = ({
}: ContextModalProps<Props>) => { }: ContextModalProps<Props>) => {
const form = useForm<FormType>({ const form = useForm<FormType>({
initialValues: { initialValues: {
name: innerProps.defaultValue, name: innerProps.defaultValue ?? "",
}, },
validate: { validate: {
name: name => !name && "Введите название", name: name => !name && "Введите название",