feat: swiper
This commit is contained in:
@ -39,6 +39,7 @@
|
|||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
|
"swiper": "^11.2.10",
|
||||||
"zod": "^4.0.14"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
.create-button {
|
.create-button {
|
||||||
padding: 10px 10px 11px 10px;
|
padding: 8px 10px 9px;
|
||||||
border-bottom: 2px solid gray;
|
border-bottom: 2px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
height: 46px;
|
height: 43px;
|
||||||
border-bottom: 2px solid gray;
|
border-bottom: 2px solid gray;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, ReactNode } from "react";
|
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 DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||||
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
import DealContainer from "@/app/deals/components/shared/DealContainer/DealContainer";
|
||||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||||
@ -26,15 +24,17 @@ const Funnel: FC = () => {
|
|||||||
handleDragEnd,
|
handleDragEnd,
|
||||||
activeStatus,
|
activeStatus,
|
||||||
activeDeal,
|
activeDeal,
|
||||||
|
swiperRef,
|
||||||
} = useDealsAndStatusesDnd();
|
} = useDealsAndStatusesDnd();
|
||||||
|
|
||||||
const renderFunnelDnd = () => (
|
return (
|
||||||
<FunnelDnd
|
<FunnelDnd
|
||||||
containers={sortedStatuses}
|
containers={sortedStatuses}
|
||||||
items={deals}
|
items={deals}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
swiperRef={swiperRef}
|
||||||
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
getContainerId={(status: StatusSchema) => `${status.id}-status`}
|
||||||
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
|
||||||
sortByLexorank(
|
sortByLexorank(
|
||||||
@ -71,22 +71,6 @@ const Funnel: FC = () => {
|
|||||||
isCreatingContainerEnabled={!!selectedBoard}
|
isCreatingContainerEnabled={!!selectedBoard}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) return renderFunnelDnd();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollArea
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbarSize={"0.5rem"}>
|
|
||||||
<Group
|
|
||||||
align={"start"}
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={"xs"}>
|
|
||||||
{renderFunnelDnd()}
|
|
||||||
{selectedBoard && <CreateStatusButton />}
|
|
||||||
</Group>
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Funnel;
|
export default Funnel;
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-width: 150px;
|
|
||||||
width: 15vw;
|
|
||||||
|
|
||||||
@media (max-width: 48em) {
|
@media (max-width: 48em) {
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { RefObject, useMemo, useRef, useState } from "react";
|
||||||
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||||
import { useDebouncedCallback } from "@mantine/hooks";
|
import { useDebouncedCallback } from "@mantine/hooks";
|
||||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||||
@ -7,6 +7,7 @@ import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
|||||||
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
import { SwiperRef } from "swiper/swiper-react";
|
||||||
|
|
||||||
type ReturnType = {
|
type ReturnType = {
|
||||||
sortedStatuses: StatusSchema[];
|
sortedStatuses: StatusSchema[];
|
||||||
@ -15,9 +16,11 @@ type ReturnType = {
|
|||||||
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
handleDragEnd: ({ active, over }: DragOverEvent) => void;
|
||||||
activeStatus: StatusSchema | null;
|
activeStatus: StatusSchema | null;
|
||||||
activeDeal: DealSchema | null;
|
activeDeal: DealSchema | null;
|
||||||
|
swiperRef: RefObject<SwiperRef | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDealsAndStatusesDnd = (): ReturnType => {
|
const useDealsAndStatusesDnd = (): ReturnType => {
|
||||||
|
const swiperRef = useRef<SwiperRef>(null);
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
const { statuses, setStatuses, updateStatus } = useStatusesContext();
|
||||||
@ -43,6 +46,30 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
if (!over) return;
|
if (!over) return;
|
||||||
const activeId = active.id as string | number;
|
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 (typeof activeId === "string" && isStatusId(activeId)) {
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
handleColumnDragOver(activeId, over);
|
handleColumnDragOver(activeId, over);
|
||||||
return;
|
return;
|
||||||
@ -242,6 +269,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
swiperRef,
|
||||||
sortedStatuses,
|
sortedStatuses,
|
||||||
handleDragStart,
|
handleDragStart,
|
||||||
handleDragOver,
|
handleDragOver,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "@mantine/notifications/styles.css";
|
import "@mantine/notifications/styles.css";
|
||||||
import "@mantine/carousel/styles.css";
|
import "@mantine/carousel/styles.css";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/pagination";
|
||||||
|
import "swiper/css/scrollbar";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
ColorSchemeScript,
|
ColorSchemeScript,
|
||||||
|
|||||||
@ -13,21 +13,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator {
|
.swiper-scrollbar-drag {
|
||||||
height: 4px;
|
|
||||||
@mixin light {
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
background-color: var(--mantine-color-dark-6);
|
color: var(--mantine-color-dark-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-active] {
|
|
||||||
@mixin light {
|
@mixin light {
|
||||||
background-color: gray;
|
color: var(--color-light-whitesmoke);
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
background-color: var(--mantine-color-dark-5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode, RefObject } from "react";
|
||||||
import {
|
import {
|
||||||
closestCorners,
|
closestCorners,
|
||||||
DndContext,
|
DndContext,
|
||||||
@ -12,8 +12,8 @@ import {
|
|||||||
horizontalListSortingStrategy,
|
horizontalListSortingStrategy,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { Carousel } from "@mantine/carousel";
|
import { FreeMode, Mousewheel, Pagination, Scrollbar } from "swiper/modules";
|
||||||
import { Group } from "@mantine/core";
|
import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
|
||||||
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
|
||||||
import useDndSensors from "@/app/deals/hooks/useSensors";
|
import useDndSensors from "@/app/deals/hooks/useSensors";
|
||||||
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
import FunnelColumn from "@/components/dnd/FunnelDnd/FunnelColumn";
|
||||||
@ -21,7 +21,7 @@ import FunnelOverlay from "@/components/dnd/FunnelDnd/FunnelOverlay";
|
|||||||
import { BaseDraggable } from "@/components/dnd/types/types";
|
import { BaseDraggable } from "@/components/dnd/types/types";
|
||||||
import useIsMobile from "@/hooks/useIsMobile";
|
import useIsMobile from "@/hooks/useIsMobile";
|
||||||
import SortableItem from "../SortableItem";
|
import SortableItem from "../SortableItem";
|
||||||
import styles from "./FunnelDnd.module.css";
|
import "./FunnelDnd.module.css";
|
||||||
|
|
||||||
type Props<TContainer, TItem> = {
|
type Props<TContainer, TItem> = {
|
||||||
containers: TContainer[];
|
containers: TContainer[];
|
||||||
@ -29,6 +29,7 @@ type Props<TContainer, TItem> = {
|
|||||||
onDragStart: (event: DragStartEvent) => void;
|
onDragStart: (event: DragStartEvent) => void;
|
||||||
onDragOver: (event: DragOverEvent) => void;
|
onDragOver: (event: DragOverEvent) => void;
|
||||||
onDragEnd: (event: DragEndEvent) => void;
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
|
swiperRef: RefObject<SwiperRef | null>;
|
||||||
renderContainer: (container: TContainer, children: ReactNode) => ReactNode;
|
renderContainer: (container: TContainer, children: ReactNode) => ReactNode;
|
||||||
renderContainerOverlay: (
|
renderContainerOverlay: (
|
||||||
container: TContainer,
|
container: TContainer,
|
||||||
@ -53,6 +54,7 @@ const FunnelDnd = <
|
|||||||
onDragStart,
|
onDragStart,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
|
swiperRef,
|
||||||
renderContainer,
|
renderContainer,
|
||||||
renderContainerOverlay,
|
renderContainerOverlay,
|
||||||
renderItem,
|
renderItem,
|
||||||
@ -71,7 +73,8 @@ const FunnelDnd = <
|
|||||||
containers.map(container => {
|
containers.map(container => {
|
||||||
const containerItems = getItemsByContainer(container, items);
|
const containerItems = getItemsByContainer(container, items);
|
||||||
const containerId = getContainerId(container);
|
const containerId = getContainerId(container);
|
||||||
const item = (
|
return (
|
||||||
|
<SwiperSlide key={containerId}>
|
||||||
<SortableItem
|
<SortableItem
|
||||||
key={containerId}
|
key={containerId}
|
||||||
id={containerId}
|
id={containerId}
|
||||||
@ -87,38 +90,64 @@ const FunnelDnd = <
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
);
|
);
|
||||||
if (!isMobile) return item;
|
|
||||||
return <Carousel.Slide key={containerId}>{item}</Carousel.Slide>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Carousel
|
<Swiper
|
||||||
slideSize={"90%"}
|
ref={swiperRef}
|
||||||
slideGap={"md"}
|
slidesPerView={1.2}
|
||||||
pb={"xl"}
|
modules={[Pagination]}
|
||||||
withControls={false}
|
freeMode={{ enabled: false }}
|
||||||
withIndicators
|
pagination={{ enabled: true, clickable: true }}>
|
||||||
styles={{
|
|
||||||
container: {
|
|
||||||
marginInline: "10vw",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
classNames={styles}>
|
|
||||||
{renderContainers()}
|
{renderContainers()}
|
||||||
{isCreatingContainerEnabled && <CreateStatusButton />}
|
{isCreatingContainerEnabled && (
|
||||||
</Carousel>
|
<SwiperSlide>
|
||||||
|
<CreateStatusButton />
|
||||||
|
</SwiperSlide>
|
||||||
|
)}
|
||||||
|
</Swiper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Group
|
<Swiper
|
||||||
gap={"xs"}
|
ref={swiperRef}
|
||||||
wrap="nowrap"
|
modules={[Scrollbar, Mousewheel, FreeMode]}
|
||||||
align="start">
|
breakpoints={{
|
||||||
|
650: {
|
||||||
|
slidesPerView: 4,
|
||||||
|
spaceBetween: 15,
|
||||||
|
},
|
||||||
|
850: {
|
||||||
|
slidesPerView: 5.5,
|
||||||
|
spaceBetween: 20,
|
||||||
|
},
|
||||||
|
1024: {
|
||||||
|
slidesPerView: 6.5,
|
||||||
|
spaceBetween: 25,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
scrollbar={{
|
||||||
|
hide: false,
|
||||||
|
}}
|
||||||
|
mousewheel={{
|
||||||
|
enabled: true,
|
||||||
|
sensitivity: 0.2,
|
||||||
|
}}
|
||||||
|
freeMode={{
|
||||||
|
enabled: true,
|
||||||
|
}}
|
||||||
|
grabCursor>
|
||||||
{renderContainers()}
|
{renderContainers()}
|
||||||
</Group>
|
{isCreatingContainerEnabled && (
|
||||||
|
<SwiperSlide>
|
||||||
|
<CreateStatusButton />
|
||||||
|
</SwiperSlide>
|
||||||
|
)}
|
||||||
|
</Swiper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ const DragHandle = ({ id, children, style, disabled }: Props) => {
|
|||||||
cursor: disabled ? "default" : "grab",
|
cursor: disabled ? "default" : "grab",
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
|
className={disabled ? "" : "swiper-no-swiping"}
|
||||||
ref={setNodeRef}>
|
ref={setNodeRef}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6179,6 +6179,7 @@ __metadata:
|
|||||||
storybook-dark-mode: "npm:^4.0.2"
|
storybook-dark-mode: "npm:^4.0.2"
|
||||||
stylelint: "npm:^16.20.0"
|
stylelint: "npm:^16.20.0"
|
||||||
stylelint-config-standard-scss: "npm:^15.0.1"
|
stylelint-config-standard-scss: "npm:^15.0.1"
|
||||||
|
swiper: "npm:^11.2.10"
|
||||||
tailwindcss: "npm:^4.1.11"
|
tailwindcss: "npm:^4.1.11"
|
||||||
ts-jest: "npm:^29.4.0"
|
ts-jest: "npm:^29.4.0"
|
||||||
typescript: "npm:5.8.3"
|
typescript: "npm:5.8.3"
|
||||||
@ -13278,6 +13279,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"swiper@npm:^11.2.10":
|
||||||
|
version: 11.2.10
|
||||||
|
resolution: "swiper@npm:11.2.10"
|
||||||
|
checksum: 10c0/b7e3a7c79d92ccc62af77744edc376c9aefc771a0abd50c4adf07b5e3450b5da9ca7f84f5809e275ff65e1bc9d4500d930628e84a76fc7f2db403291c69b7fac
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"symbol-tree@npm:^3.2.4":
|
"symbol-tree@npm:^3.2.4":
|
||||||
version: 3.2.4
|
version: 3.2.4
|
||||||
resolution: "symbol-tree@npm:3.2.4"
|
resolution: "symbol-tree@npm:3.2.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user