diff --git a/src/app/deals/components/mobile/BoardsMobile/BoardsMobile.tsx b/src/app/deals/components/mobile/BoardsMobile/BoardsMobile.tsx index 0cd5edf..a333bba 100644 --- a/src/app/deals/components/mobile/BoardsMobile/BoardsMobile.tsx +++ b/src/app/deals/components/mobile/BoardsMobile/BoardsMobile.tsx @@ -29,7 +29,8 @@ const BoardsMobile = () => { scrollbars={"x"} scrollbarSize={0} w={"100vw"} - mt={5}> + mt={5} + mb={"sm"}> diff --git a/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.module.css b/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.module.css index a540cdb..b76f822 100644 --- a/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.module.css +++ b/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.module.css @@ -1,10 +1,16 @@ .container { + border-radius: var(--mantine-spacing-md); flex-wrap: nowrap; - border-bottom: solid dodgerblue 3px; - margin-bottom: 3px; cursor: pointer; + @mixin light { + background-color: var(--color-light-aqua); + } + @mixin dark { + background-color: var(--mantine-color-dark-6); + } + @media (max-width: 48em) { width: 80vw; } diff --git a/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.tsx b/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.tsx index fa3e7f4..d019edd 100644 --- a/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.tsx +++ b/src/app/deals/components/shared/CreateStatusButton/CreateStatusButton.tsx @@ -12,7 +12,9 @@ const CreateStatusButton = () => { return ( - + { modalTitle={"Создание колонки"} inputStyles={{ wrapper: { - padding: 4, + paddingInline: "var(--mantine-spacing-md)", + paddingBlock: "var(--mantine-spacing-xs)", }, }} /> diff --git a/src/app/deals/components/shared/DealCard/DealCard.module.css b/src/app/deals/components/shared/DealCard/DealCard.module.css index 95f5bf9..d1a2131 100644 --- a/src/app/deals/components/shared/DealCard/DealCard.module.css +++ b/src/app/deals/components/shared/DealCard/DealCard.module.css @@ -1,7 +1,7 @@ .container { @mixin light { - background-color: var(--color-light-aqua); + background-color: var(--color-light-white-blue); } @mixin dark { background-color: var(--mantine-color-dark-7); diff --git a/src/app/deals/components/shared/Funnel/Funnel.tsx b/src/app/deals/components/shared/Funnel/Funnel.tsx index ef93a27..917a78b 100644 --- a/src/app/deals/components/shared/Funnel/Funnel.tsx +++ b/src/app/deals/components/shared/Funnel/Funnel.tsx @@ -3,6 +3,7 @@ import React, { FC, ReactNode } from "react"; import DealCard from "@/app/deals/components/shared/DealCard/DealCard"; import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer"; +import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader"; import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper"; import { useBoardsContext } from "@/app/deals/contexts/BoardsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext"; @@ -43,14 +44,21 @@ const Funnel: FC = () => { } renderContainer={( status: StatusSchema, - funnelColumnComponent: ReactNode + funnelColumnComponent: ReactNode, + renderDraggable ) => ( + renderHeader={renderDraggable}> {funnelColumnComponent} )} + renderContainerHeader={status => ( + + )} renderItem={(deal: DealSchema) => ( { renderContainerOverlay={(status: StatusSchema, children) => ( + renderHeader={() => ( + + )}> {children} )} diff --git a/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.module.css b/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.module.css new file mode 100644 index 0000000..bf47dbe --- /dev/null +++ b/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.module.css @@ -0,0 +1,4 @@ + +.header { + border-bottom: solid dodgerblue 3px; +} diff --git a/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.tsx b/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.tsx new file mode 100644 index 0000000..1244a1a --- /dev/null +++ b/src/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader.tsx @@ -0,0 +1,62 @@ +import React, { FC } from "react"; +import { Group, Text } from "@mantine/core"; +import styles from "./StatusColumnHeader.module.css"; +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"; + +type Props = { + status: StatusSchema; + isDragging: boolean; +}; + +const StatusColumnHeader: FC = ({ status, isDragging }) => { + const { onUpdateStatus } = useStatusesContext(); + + const handleSave = (value: string) => { + const newValue = value.trim(); + if (newValue && newValue !== status.name) { + onUpdateStatus(status.id, { name: newValue }); + } + }; + + return ( + + handleSave(value)} + inputStyles={{ + input: { + height: 25, + minHeight: 25, + }, + }} + getChildren={startEditing => ( + <> + + {status.name} + + + + )} + modalTitle={"Редактирование статуса"} + /> + + ); +}; + +export default StatusColumnHeader; diff --git a/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.module.css b/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.module.css index 3ee8022..ce58f41 100644 --- a/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.module.css +++ b/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.module.css @@ -1,10 +1,20 @@ .container { + height: calc(100vh - 150px); + @media (max-width: 48em) { width: 80vw; } } -.header { - border-bottom: solid dodgerblue 3px; +.inner-container { + border-radius: var(--mantine-spacing-md); + gap: 0; + + @mixin light { + background-color: var(--color-light-aqua); + } + @mixin dark { + background-color: var(--mantine-color-dark-6); + } } diff --git a/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.tsx b/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.tsx index 11cd76e..bd03dba 100644 --- a/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.tsx +++ b/src/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper.tsx @@ -1,68 +1,24 @@ 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 { Box, Stack } from "@mantine/core"; import { StatusSchema } from "@/lib/client"; import styles from "./StatusColumnWrapper.module.css"; type Props = { status: StatusSchema; - isDragging?: boolean; + renderHeader: () => ReactNode; 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 }); - } - }; - +const StatusColumnWrapper = ({ renderHeader, children }: Props) => { return ( - - handleSave(value)} - inputStyles={{ - input: { - height: 25, - minHeight: 25, - }, - }} - getChildren={startEditing => ( - <> - - {status.name} - - - - )} - modalTitle={"Редактирование статуса"} - /> - - {children} + + {renderHeader()} + {children} + ); }; diff --git a/src/app/deals/hooks/useDealsAndStatusesDnd.ts b/src/app/deals/hooks/useDealsAndStatusesDnd.ts index d7401c9..3ea4014 100644 --- a/src/app/deals/hooks/useDealsAndStatusesDnd.ts +++ b/src/app/deals/hooks/useDealsAndStatusesDnd.ts @@ -1,13 +1,14 @@ import { RefObject, useMemo, useRef, useState } from "react"; import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core"; +import { SwiperRef } from "swiper/swiper-react"; import { useDebouncedCallback } from "@mantine/hooks"; import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useStatusesContext } from "@/app/deals/contexts/StatusesContext"; import useGetNewRank from "@/app/deals/hooks/useGetNewRank"; import { getStatusId, isStatusId } from "@/app/deals/utils/statusId"; +import useIsMobile from "@/hooks/useIsMobile"; import { DealSchema, StatusSchema } from "@/lib/client"; import { sortByLexorank } from "@/utils/lexorank"; -import { SwiperRef } from "swiper/swiper-react"; type ReturnType = { sortedStatuses: StatusSchema[]; @@ -26,6 +27,7 @@ const useDealsAndStatusesDnd = (): ReturnType => { const { statuses, setStatuses, updateStatus } = useStatusesContext(); const { deals, setDeals, updateDeal } = useDealsContext(); const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]); + const isMobile = useIsMobile(); const { getNewRankForSameStatus, @@ -42,32 +44,50 @@ const useDealsAndStatusesDnd = (): ReturnType => { return statuses.find(status => status.id === deal.statusId); }; + const swipeSliderDuringDrag = (activeId: number, over: Over) => { + const activeStatusLexorank = getStatusByDealId( + Number(activeId) + )?.lexorank; + let overStatusLexorank: string | undefined; + + if (typeof over.id === "string" && isStatusId(over.id)) { + const overStatusId = getStatusId(over.id); + overStatusLexorank = statuses.find( + s => s.id === overStatusId + )?.lexorank; + } else { + overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank; + } + + if ( + !activeStatusLexorank || + !overStatusLexorank || + !swiperRef.current?.swiper + ) + return; + + const activeIndex = sortedStatuses.findIndex( + s => s.lexorank === activeStatusLexorank + ); + const overIndex = sortedStatuses.findIndex( + s => s.lexorank === overStatusLexorank + ); + + if (activeIndex > overIndex) { + swiperRef.current.swiper.slidePrev(); + return; + } + if (activeIndex < overIndex) { + swiperRef.current.swiper.slideNext(); + } + }; + const handleDragOver = ({ active, over }: DragOverEvent) => { if (!over) return; const activeId = active.id as string | number; - // Only perform swiper navigation for deal drag (not status column drag) - if (typeof activeId !== "string") { - const activeStatusLexorank = getStatusByDealId(Number(activeId))?.lexorank; - let overStatusLexorank: string | undefined; - - if (typeof over.id === "string" && isStatusId(over.id)) { - const overStatusId = getStatusId(over.id); - overStatusLexorank = statuses.find(s => s.id === overStatusId)?.lexorank; - } else { - overStatusLexorank = getStatusByDealId(Number(over.id))?.lexorank; - } - - if (activeStatusLexorank && overStatusLexorank && swiperRef.current?.swiper) { - const activeIndex = sortedStatuses.findIndex(s => s.lexorank === activeStatusLexorank); - const overIndex = sortedStatuses.findIndex(s => s.lexorank === overStatusLexorank); - - if (activeIndex > overIndex) { - swiperRef.current.swiper.slidePrev(); - } else if (activeIndex < overIndex) { - swiperRef.current.swiper.slideNext(); - } - } + if (isMobile && typeof activeId !== "string") { + swipeSliderDuringDrag(activeId, over); } if (typeof activeId === "string" && isStatusId(activeId)) { diff --git a/src/app/global.css b/src/app/global.css index b804c0e..cb37267 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -4,6 +4,7 @@ /* Colors */ --color-light-gray-blue: #f4f7fd; --color-light-aqua: #e0f0f4; + --color-light-white-blue: #f5fbfc; --color-light-whitesmoke: #fcfdff; --mantine-color-dark-7-5: #212121; /* Shadows */ diff --git a/src/components/dnd/FunnelDnd/FunnelDnd.tsx b/src/components/dnd/FunnelDnd/FunnelDnd.tsx index 4861c34..db5e965 100644 --- a/src/components/dnd/FunnelDnd/FunnelDnd.tsx +++ b/src/components/dnd/FunnelDnd/FunnelDnd.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, RefObject } from "react"; import { - closestCorners, + closestCenter, DndContext, DragEndEvent, DragOverEvent, @@ -30,7 +30,12 @@ type Props = { onDragOver: (event: DragOverEvent) => void; onDragEnd: (event: DragEndEvent) => void; swiperRef: RefObject; - renderContainer: (container: TContainer, children: ReactNode) => ReactNode; + renderContainer: ( + container: TContainer, + children: ReactNode, + renderDraggable: () => ReactNode + ) => ReactNode; + renderContainerHeader: (container: TContainer) => ReactNode; renderContainerOverlay: ( container: TContainer, children: ReactNode @@ -56,6 +61,7 @@ const FunnelDnd = < onDragEnd, swiperRef, renderContainer, + renderContainerHeader, renderContainerOverlay, renderItem, renderItemOverlay, @@ -81,16 +87,18 @@ const FunnelDnd = < key={containerId} id={containerId} disabled={disabledColumns} - renderItem={() => + renderItem={renderDraggable => renderContainer( container, + />, + renderDraggable! ) } + renderDraggable={() => renderContainerHeader(container)} /> ); @@ -142,7 +150,7 @@ const FunnelDnd = < return ( diff --git a/src/components/dnd/SortableDnd/SortableDnd.module.css b/src/components/dnd/SortableDnd/SortableDnd.module.css new file mode 100644 index 0000000..6606dee --- /dev/null +++ b/src/components/dnd/SortableDnd/SortableDnd.module.css @@ -0,0 +1,9 @@ +.swiper-container :global(.swiper-scrollbar-drag) { + @mixin dark { + background-color: var(--mantine-color-dark-9); + } + @mixin light { + background-color: grey; + } +} + diff --git a/src/components/dnd/SortableDnd/SortableDnd.tsx b/src/components/dnd/SortableDnd/SortableDnd.tsx index 9e31d5a..b1762c9 100644 --- a/src/components/dnd/SortableDnd/SortableDnd.tsx +++ b/src/components/dnd/SortableDnd/SortableDnd.tsx @@ -18,6 +18,7 @@ import useDndSensors from "@/app/deals/hooks/useSensors"; import { SortableOverlay } from "@/components/dnd/SortableDnd/SortableOverlay"; import SortableItem from "@/components/dnd/SortableItem"; import { getNewLexorank, sortByLexorank } from "@/utils/lexorank"; +import classes from "./SortableDnd.module.css"; type BaseItem = { id: number; @@ -112,6 +113,7 @@ const SortableDnd = ({ enabled: true, sensitivity: 0.2, }} + className={classes["swiper-container"]} style={containerStyle} direction={vertical ? "vertical" : "horizontal"} freeMode={{ enabled: true }}