feat: division between mobile and desktop components, boards for mobile
This commit is contained in:
61
src/app/deals/components/shared/BoardMenu/BoardMenu.tsx
Normal file
61
src/app/deals/components/shared/BoardMenu/BoardMenu.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React, { FC } from "react";
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { BoardSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
board: BoardSchema;
|
||||
startEditing: () => void;
|
||||
isHovered?: boolean;
|
||||
menuIconSize?: number;
|
||||
};
|
||||
|
||||
const BoardMenu: FC<Props> = ({
|
||||
board,
|
||||
startEditing,
|
||||
isHovered = true,
|
||||
menuIconSize = 16,
|
||||
}) => {
|
||||
const { selectedBoard, onDeleteBoard } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
style={{
|
||||
opacity:
|
||||
isHovered || selectedBoard?.id === board.id ? 1 : 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconDotsVertical size={menuIconSize} />
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
startEditing();
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconEdit />
|
||||
<Text>Переименовать</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteBoard(board);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconTrash />
|
||||
<Text>Удалить</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardMenu;
|
||||
@ -0,0 +1,35 @@
|
||||
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 (
|
||||
<Box
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderStyle: "dashed",
|
||||
borderColor: "gray",
|
||||
width: "15vw",
|
||||
minWidth: 150,
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название колонки"}
|
||||
onComplete={onCreateStatus}
|
||||
getChildren={startEditing => (
|
||||
<Box
|
||||
p={9}
|
||||
onClick={() => startEditing()}>
|
||||
<Text>Создать колонку</Text>
|
||||
</Box>
|
||||
)}
|
||||
modalTitle={"Создание колонки"}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusButton;
|
||||
@ -0,0 +1,8 @@
|
||||
|
||||
.container {
|
||||
background-color: #e0f0f4;
|
||||
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
}
|
||||
}
|
||||
13
src/app/deals/components/shared/DealCard/DealCard.tsx
Normal file
13
src/app/deals/components/shared/DealCard/DealCard.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Card } from "@mantine/core";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import styles from "./DealCard.module.css";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
const DealCard = ({ deal }: Props) => {
|
||||
return <Card className={styles.container}>{deal.name}</Card>;
|
||||
};
|
||||
|
||||
export default DealCard;
|
||||
@ -0,0 +1,25 @@
|
||||
import React, { FC, useMemo } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import SortableItem from "@/components/dnd/SortableItem";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
const DealContainer: FC<Props> = ({ deal }) => {
|
||||
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SortableItem
|
||||
dragHandleStyle={{ cursor: "pointer" }}
|
||||
id={deal.id}
|
||||
renderItem={() => dealBody}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealContainer;
|
||||
96
src/app/deals/components/shared/Funnel/Funnel.tsx
Normal file
96
src/app/deals/components/shared/Funnel/Funnel.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import { Group, ScrollArea } from "@mantine/core";
|
||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
const Funnel: FC = () => {
|
||||
const { deals } = useDealsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
sortedStatuses,
|
||||
handleDragStart,
|
||||
handleDragOver,
|
||||
handleDragEnd,
|
||||
activeStatus,
|
||||
activeDeal,
|
||||
} = useDealsAndStatusesDnd();
|
||||
|
||||
const renderFunnelDnd = () => (
|
||||
<>
|
||||
<FunnelDnd
|
||||
containers={sortedStatuses}
|
||||
items={deals}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||
getItemsByContainer={(
|
||||
status: StatusSchema,
|
||||
items: DealSchema[]
|
||||
) =>
|
||||
sortByLexorank(
|
||||
items.filter(deal => deal.statusId === status.id)
|
||||
)
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
funnelColumnComponent: ReactNode
|
||||
) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging={activeStatus?.id === status.id}>
|
||||
{funnelColumnComponent}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealContainer
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
)}
|
||||
activeContainer={activeStatus}
|
||||
activeItem={activeDeal}
|
||||
renderItemOverlay={(deal: DealSchema) => (
|
||||
<DealCard deal={deal} />
|
||||
)}
|
||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
isDragging>
|
||||
{children}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
disabledColumns={isMobile}
|
||||
/>
|
||||
{!isMobile && <CreateStatusButton />}
|
||||
</>
|
||||
);
|
||||
|
||||
if (isMobile) return renderFunnelDnd();
|
||||
|
||||
return (
|
||||
<ScrollArea
|
||||
offsetScrollbars={"x"}
|
||||
scrollbarSize={"0.5rem"}>
|
||||
<Group
|
||||
align={"start"}
|
||||
wrap={"nowrap"}
|
||||
gap={"sm"}>
|
||||
{renderFunnelDnd()}
|
||||
</Group>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export default Funnel;
|
||||
65
src/app/deals/components/shared/Header/Header.tsx
Normal file
65
src/app/deals/components/shared/Header/Header.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import Boards from "@/app/deals/components/desktop/Boards/Boards";
|
||||
import BoardsMobile from "@/app/deals/components/mobile/BoardsMobile/BoardsMobile";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||
import { ColorSchemeToggle } from "@/components/ui/ColorSchemeToggle/ColorSchemeToggle";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
|
||||
const Header = () => {
|
||||
const { projects, setSelectedProject, selectedProject } =
|
||||
useProjectsContext();
|
||||
const { setIsEditorDrawerOpened } = useBoardsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const getDesktopHeader = () => {
|
||||
return (
|
||||
<Group
|
||||
w={"100%"}
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
pr={"md"}>
|
||||
<Boards />
|
||||
<ColorSchemeToggle />
|
||||
<ProjectSelect
|
||||
data={projects}
|
||||
value={selectedProject}
|
||||
onChange={value => value && setSelectedProject(value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const getMobileHeader = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Group justify={"space-between"}>
|
||||
<Box p={"md"}>
|
||||
<IconChevronLeft />
|
||||
</Box>
|
||||
<Text>{selectedProject?.name}</Text>
|
||||
<Box
|
||||
p={"md"}
|
||||
onClick={() => setIsEditorDrawerOpened(true)}>
|
||||
<IconSettings />
|
||||
</Box>
|
||||
</Group>
|
||||
<BoardsMobile />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify={"flex-end"}
|
||||
w={"100%"}>
|
||||
{isMobile ? getMobileHeader() : getDesktopHeader()}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@ -0,0 +1,9 @@
|
||||
|
||||
.container {
|
||||
min-width: 150px;
|
||||
width: 15vw;
|
||||
|
||||
@media (max-width: 48em) {
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
import styles from "./StatusColumnWrapper.module.css";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging?: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StatusColumnWrapper = ({
|
||||
status,
|
||||
children,
|
||||
isDragging = false,
|
||||
}: Props) => {
|
||||
const { onUpdateStatus } = useStatusesContext();
|
||||
|
||||
const handleSave = (value: string) => {
|
||||
const newValue = value.trim();
|
||||
if (newValue && newValue !== status.name) {
|
||||
onUpdateStatus(status.id, { name: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
mx={7}
|
||||
className={styles.container}>
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
className={"flex-nowrap border-b-3 border-blue-500 mb-3 p-3"}>
|
||||
<InPlaceInput
|
||||
defaultValue={status.name}
|
||||
onComplete={value => handleSave(value)}
|
||||
inputStyles={{
|
||||
input: {
|
||||
height: 25,
|
||||
minHeight: 25,
|
||||
},
|
||||
}}
|
||||
getChildren={startEditing => (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}>
|
||||
{status.name}
|
||||
</Text>
|
||||
<StatusMenu
|
||||
status={status}
|
||||
handleEdit={startEditing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
modalTitle={"Редактирование статуса"}
|
||||
/>
|
||||
</Group>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnWrapper;
|
||||
72
src/app/deals/components/shared/StatusMenu/StatusMenu.tsx
Normal file
72
src/app/deals/components/shared/StatusMenu/StatusMenu.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconExchange,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { Box, Group, Menu, Text } from "@mantine/core";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import useIsMobile from "@/hooks/useIsMobile";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
handleEdit: () => void;
|
||||
};
|
||||
|
||||
const StatusMenu: FC<Props> = ({ status, handleEdit }) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { onDeleteStatus, setIsEditorDrawerOpened } = useStatusesContext();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
p={5}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<IconDotsVertical size={16} />
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleEdit();
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconEdit />
|
||||
<Text>Переименовать</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDeleteStatus(status);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconTrash />
|
||||
<Text>Удалить</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
{isMobile && (
|
||||
<Menu.Item
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setIsEditorDrawerOpened(true);
|
||||
}}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<IconExchange />
|
||||
<Text>Изменить порядок</Text>
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusMenu;
|
||||
Reference in New Issue
Block a user