feat: swiper

This commit is contained in:
2025-08-16 14:59:37 +04:00
parent 219689b947
commit 0a13070d9e
10 changed files with 121 additions and 80 deletions

View File

@ -39,6 +39,7 @@
"react-redux": "^9.2.0",
"redux-persist": "^6.0.0",
"sharp": "^0.34.3",
"swiper": "^11.2.10",
"zod": "^4.0.14"
},
"devDependencies": {

View File

@ -1,11 +1,11 @@
.create-button {
padding: 10px 10px 11px 10px;
padding: 8px 10px 9px;
border-bottom: 2px solid gray;
}
.spacer {
height: 46px;
height: 43px;
border-bottom: 2px solid gray;
width: 100%;
}

View File

@ -1,8 +1,6 @@
"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";
@ -26,15 +24,17 @@ const Funnel: FC = () => {
handleDragEnd,
activeStatus,
activeDeal,
swiperRef,
} = useDealsAndStatusesDnd();
const renderFunnelDnd = () => (
return (
<FunnelDnd
containers={sortedStatuses}
items={deals}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
swiperRef={swiperRef}
getContainerId={(status: StatusSchema) => `${status.id}-status`}
getItemsByContainer={(status: StatusSchema, items: DealSchema[]) =>
sortByLexorank(
@ -71,22 +71,6 @@ const Funnel: FC = () => {
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;

View File

@ -1,8 +1,5 @@
.container {
min-width: 150px;
width: 15vw;
@media (max-width: 48em) {
width: 80vw;
}

View File

@ -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 { useDebouncedCallback } from "@mantine/hooks";
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 { DealSchema, StatusSchema } from "@/lib/client";
import { sortByLexorank } from "@/utils/lexorank";
import { SwiperRef } from "swiper/swiper-react";
type ReturnType = {
sortedStatuses: StatusSchema[];
@ -15,9 +16,11 @@ type ReturnType = {
handleDragEnd: ({ active, over }: DragOverEvent) => void;
activeStatus: StatusSchema | null;
activeDeal: DealSchema | null;
swiperRef: RefObject<SwiperRef | null>;
};
const useDealsAndStatusesDnd = (): ReturnType => {
const swiperRef = useRef<SwiperRef>(null);
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
const { statuses, setStatuses, updateStatus } = useStatusesContext();
@ -43,6 +46,30 @@ const useDealsAndStatusesDnd = (): ReturnType => {
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 (typeof activeId === "string" && isStatusId(activeId)) {
handleColumnDragOver(activeId, over);
return;
@ -242,6 +269,7 @@ const useDealsAndStatusesDnd = (): ReturnType => {
};
return {
swiperRef,
sortedStatuses,
handleDragStart,
handleDragOver,

View File

@ -1,6 +1,9 @@
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
import "@mantine/carousel/styles.css";
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/scrollbar";
import { ReactNode } from "react";
import {
ColorSchemeScript,

View File

@ -13,21 +13,11 @@
}
}
.indicator {
height: 4px;
@mixin light {
background-color: lightgray;
}
.swiper-scrollbar-drag {
@mixin dark {
background-color: var(--mantine-color-dark-6);
color: var(--mantine-color-dark-7);
}
&[data-active] {
@mixin light {
background-color: gray;
}
@mixin dark {
background-color: var(--mantine-color-dark-5);
}
@mixin light {
color: var(--color-light-whitesmoke);
}
}

View File

@ -1,6 +1,6 @@
"use client";
import React, { ReactNode } from "react";
import React, { ReactNode, RefObject } from "react";
import {
closestCorners,
DndContext,
@ -12,8 +12,8 @@ import {
horizontalListSortingStrategy,
SortableContext,
} from "@dnd-kit/sortable";
import { Carousel } from "@mantine/carousel";
import { Group } from "@mantine/core";
import { FreeMode, Mousewheel, Pagination, Scrollbar } from "swiper/modules";
import { Swiper, SwiperRef, SwiperSlide } from "swiper/react";
import CreateStatusButton from "@/app/deals/components/shared/CreateStatusButton/CreateStatusButton";
import useDndSensors from "@/app/deals/hooks/useSensors";
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 useIsMobile from "@/hooks/useIsMobile";
import SortableItem from "../SortableItem";
import styles from "./FunnelDnd.module.css";
import "./FunnelDnd.module.css";
type Props<TContainer, TItem> = {
containers: TContainer[];
@ -29,6 +29,7 @@ type Props<TContainer, TItem> = {
onDragStart: (event: DragStartEvent) => void;
onDragOver: (event: DragOverEvent) => void;
onDragEnd: (event: DragEndEvent) => void;
swiperRef: RefObject<SwiperRef | null>;
renderContainer: (container: TContainer, children: ReactNode) => ReactNode;
renderContainerOverlay: (
container: TContainer,
@ -53,6 +54,7 @@ const FunnelDnd = <
onDragStart,
onDragOver,
onDragEnd,
swiperRef,
renderContainer,
renderContainerOverlay,
renderItem,
@ -71,54 +73,81 @@ const FunnelDnd = <
containers.map(container => {
const containerItems = getItemsByContainer(container, items);
const containerId = getContainerId(container);
const item = (
<SortableItem
key={containerId}
id={containerId}
disabled={disabledColumns}
renderItem={() =>
renderContainer(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
/>
)
}
/>
return (
<SwiperSlide key={containerId}>
<SortableItem
key={containerId}
id={containerId}
disabled={disabledColumns}
renderItem={() =>
renderContainer(
container,
<FunnelColumn
id={containerId}
items={containerItems}
renderItem={renderItem}
/>
)
}
/>
</SwiperSlide>
);
if (!isMobile) return item;
return <Carousel.Slide key={containerId}>{item}</Carousel.Slide>;
});
const renderBody = () => {
if (isMobile) {
return (
<Carousel
slideSize={"90%"}
slideGap={"md"}
pb={"xl"}
withControls={false}
withIndicators
styles={{
container: {
marginInline: "10vw",
},
}}
classNames={styles}>
<Swiper
ref={swiperRef}
slidesPerView={1.2}
modules={[Pagination]}
freeMode={{ enabled: false }}
pagination={{ enabled: true, clickable: true }}>
{renderContainers()}
{isCreatingContainerEnabled && <CreateStatusButton />}
</Carousel>
{isCreatingContainerEnabled && (
<SwiperSlide>
<CreateStatusButton />
</SwiperSlide>
)}
</Swiper>
);
}
return (
<Group
gap={"xs"}
wrap="nowrap"
align="start">
<Swiper
ref={swiperRef}
modules={[Scrollbar, Mousewheel, FreeMode]}
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()}
</Group>
{isCreatingContainerEnabled && (
<SwiperSlide>
<CreateStatusButton />
</SwiperSlide>
)}
</Swiper>
);
};

View File

@ -23,6 +23,7 @@ const DragHandle = ({ id, children, style, disabled }: Props) => {
cursor: disabled ? "default" : "grab",
...style,
}}
className={disabled ? "" : "swiper-no-swiping"}
ref={setNodeRef}>
{children}
</div>

View File

@ -6179,6 +6179,7 @@ __metadata:
storybook-dark-mode: "npm:^4.0.2"
stylelint: "npm:^16.20.0"
stylelint-config-standard-scss: "npm:^15.0.1"
swiper: "npm:^11.2.10"
tailwindcss: "npm:^4.1.11"
ts-jest: "npm:^29.4.0"
typescript: "npm:5.8.3"
@ -13278,6 +13279,13 @@ __metadata:
languageName: node
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":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"