Refactor Popup components to remove position handling and drag functionality; integrate DraggableContainer for improved drag-and-drop experience. Update PopupHeader and PopupWrapper for cleaner structure and enhanced mobile responsiveness.

This commit is contained in:
2025-10-21 19:09:03 +05:00
parent d2b818ea90
commit 75b57b879e
8 changed files with 30 additions and 144 deletions
+1 -21
View File
@@ -2,20 +2,10 @@ import { AnimatePresence, motion } from "motion/react";
import usePopupStore from "../store/popupStore";
function PopupContainer() {
const { popup, position, setPopup } = usePopupStore();
const { popup } = usePopupStore();
const isMobile = innerWidth < 640;
function handleDragEnd(
_event: unknown,
info: { offset: { y: number }; velocity: { y: number } }
) {
// Закрываем попап если свайпнули вниз больше чем на 100px или со скоростью > 500
if (info.offset.y > 100 || info.velocity.y > 500) {
setPopup(null);
}
}
return (
<AnimatePresence>
{popup && (
@@ -24,16 +14,6 @@ function PopupContainer() {
initial={{ opacity: 0, y: isMobile ? "100%" : undefined }}
animate={{ opacity: 1, y: isMobile ? "0%" : undefined }}
exit={{ opacity: 0, y: isMobile ? "100%" : undefined }}
drag={isMobile ? "y" : false}
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.5 }}
onDragEnd={isMobile ? handleDragEnd : undefined}
transition={{ bounce: 0 }}
style={
!isMobile
? { top: position.y, left: position.x }
: { bottom: 0, left: 0, right: 0 }
}
>
{popup}
</motion.div>
+1 -8
View File
@@ -6,21 +6,14 @@ import Button from "./ui/Button";
interface PopupHeaderProps {
title?: string;
leftButton?: React.ReactNode;
headerRef: React.RefObject<HTMLDivElement | null>;
draggable?: boolean;
}
function PopupHeader({
title,
leftButton,
headerRef,
draggable,
}: PopupHeaderProps) {
function PopupHeader({ title, leftButton, draggable }: PopupHeaderProps) {
const { setPopup } = usePopupStore();
return (
<div
ref={headerRef}
className={clsx(
"2xl:p-[1.111vw] p-4 flex justify-between items-center select-none relative",
draggable && "cursor-grab active:cursor-grabbing"
+4 -82
View File
@@ -1,9 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import clsx from "clsx";
import PopupHeader from "./PopupHeader";
import { useEffect, useState } from "react";
import { useRef } from "react";
import usePopupStore from "../store/popupStore";
import DraggableContainer from "./DraggableContainer";
interface PopupWrapperProps {
children: React.ReactNode;
@@ -20,84 +18,9 @@ function PopupWrapper({
leftButton,
draggable,
}: PopupWrapperProps) {
const { position, setPosition } = usePopupStore();
const [mouseDown, setMouseDown] = useState(false);
const [mouseDownPosition, setMouseDownPosition] = useState(position);
const wrapperRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
addEventListener("mouseup", () => setMouseDown(false));
addEventListener("touchend", () => setMouseDown(false));
return () => {
removeEventListener("mouseup", () => setMouseDown(false));
removeEventListener("touchend", () => setMouseDown(false));
};
}, []);
function handleMove(e: MouseEvent | TouchEvent) {
if (draggable && mouseDown && wrapperRef.current) {
e.preventDefault();
const x = "clientX" in e ? e.clientX : e.touches[0].clientX;
const y = "clientY" in e ? e.clientY : e.touches[0].clientY;
setPosition({
x: Math.min(
Math.max(0, position.x + x - mouseDownPosition.x),
innerWidth - wrapperRef.current.clientWidth
),
y: Math.min(
Math.max(0, position.y + y - mouseDownPosition.y),
innerHeight - wrapperRef.current.clientHeight
),
});
setMouseDownPosition({ x, y });
}
}
useEffect(() => {
addEventListener("mousemove", handleMove);
addEventListener("touchmove", handleMove);
return () => {
removeEventListener("mousemove", handleMove);
removeEventListener("touchmove", handleMove);
};
}, [handleMove]);
useEffect(() => {
if (headerRef.current) {
headerRef.current.addEventListener("mousedown", (e) => {
setMouseDown(true);
setMouseDownPosition({ x: e.clientX, y: e.clientY });
});
headerRef.current.addEventListener("touchstart", (e) => {
setMouseDown(true);
setMouseDownPosition({
x: e.touches[0].clientX,
y: e.touches[0].clientY,
});
});
}
return () => {
if (headerRef.current) {
headerRef.current.removeEventListener("mousedown", (e) => {
setMouseDown(true);
setMouseDownPosition({ x: e.clientX, y: e.clientY });
});
headerRef.current.removeEventListener("touchstart", (e) => {
setMouseDown(true);
setMouseDownPosition({
x: e.touches[0].clientX,
y: e.touches[0].clientY,
});
});
}
};
}, []);
return (
<div
ref={wrapperRef}
<DraggableContainer
constrainToBounds
className={clsx(
"2xl:rounded-[2.222vw] relative bg-white shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] 2xl:w-[21.667vw] sm:rounded-[32px] max-sm:w-screen max-sm:rounded-t-[32px]",
className
@@ -109,13 +32,12 @@ function PopupWrapper({
</div>
<PopupHeader
headerRef={headerRef}
title={title}
leftButton={leftButton}
draggable={draggable}
/>
<div className="2xl:p-[1.389vw] p-5">{children}</div>
</div>
</DraggableContainer>
);
}
+1 -1
View File
@@ -35,7 +35,7 @@ function SessionUsersPanel2() {
<DraggableContainer
enableSnapping={true}
autoAlign={true}
initialPosition={{ top: 20, left: 20 }}
initialPosition={{ bottom: 16, right: 16 }}
padding={20}
className="flex gap-4"
>
@@ -15,13 +15,9 @@ export default function ParticipantsPopup() {
const participants = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return (
<PopupWrapper
title="Участники"
draggable
className="h-max 2xl:w-[21.667vw]"
>
<div className="flex flex-col gap-[1.667vw] relative">
<ul className="flex flex-col gap-0 2xl:gap-[1.111vw] 2xl:max-h-auto max-h-[calc(100dvh-50vw)] overflow-y-auto">
<PopupWrapper title="Участники" draggable>
<div className="flex flex-col 2xl:gap-[1.667vw] gap-6 relative 2xl:-mt-[1.389vw] -mt-5">
<ul className="flex flex-col gap-0 2xl:gap-[1.111vw] 2xl:max-h-[calc(11.944vw+1.389vw)] overflow-y-auto 2xl:pt-[1.389vw] pt-5">
{participants.map((participant, index) => (
<Fragment key={index}>
<ParticipantItem id={participant.toString()} />
-1
View File
@@ -8,7 +8,6 @@ import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
import CogFilledIcon from "../components/icons/CogFilledIcon";
// import SessionUsersPanel from "../components/SessionUsersPanel";
import SharePopup from "../components/popups/SharePopup";
// import { useEffect } from "react";
// import useToastsStore from "../store/toastsStore";
import ChatPopup from "../components/popups/ChatPopup";
+16 -20
View File
@@ -25,7 +25,7 @@ import LoaderIcon from "../components/icons/LoaderIcon";
import SessionUsersPanel from "../components/SessionUsersPanel2";
function NewSessionPage() {
const { setPopup, setPosition } = usePopupStore();
const { setPopup } = usePopupStore();
const [isFullscreen, setIsFullscreen] = useState(false);
@@ -75,6 +75,18 @@ function NewSessionPage() {
const session = sessionData?.session;
function handleChatOpen() {
setPopup(<ChatPopup />);
}
function handleParticipantsOpen() {
setPopup(<ParticipantsPopup />);
}
function handleShareOpen() {
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
}
// Перенаправление на тестовую страницу при завершении сессии
useEffect(() => {
if (session?.status === "ended") {
@@ -150,13 +162,7 @@ function NewSessionPage() {
<ActionsSidebarWrapper>
<FloatingActionButton
className="max-2xl:hidden"
onClick={() => {
setPosition({
x: ((1440 - 384) / 1440) * innerWidth,
y: (200 / 1440) * innerWidth,
});
setPopup(<ChatPopup />);
}}
onClick={handleChatOpen}
>
<div className="size-[1.111vw] text-white">
<ChatFilledIcon />
@@ -164,13 +170,7 @@ function NewSessionPage() {
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={() => {
setPosition({
x: ((1440 - 384) / 1440) * innerWidth,
y: (234 / 800) * innerHeight,
});
setPopup(<ParticipantsPopup />);
}}
onClick={handleParticipantsOpen}
>
<div className="size-[1.111vw] text-white">
<UsersFilledIcon />
@@ -178,11 +178,7 @@ function NewSessionPage() {
</FloatingActionButton>
<FloatingActionButton
className="max-2xl:hidden"
onClick={() =>
setPopup(
<SharePopup link={`${window.location.origin}/sessions/${id}`} />
)
}
onClick={handleShareOpen}
>
<div className="size-[1.111vw] text-white">
<ShareFilledIcon />
+4 -4
View File
@@ -4,15 +4,15 @@ import type { ReactNode } from "react";
interface PopupState {
popup: ReactNode | null;
setPopup: (popup: ReactNode | null) => void;
position: { x: number; y: number };
setPosition: (position: { x: number; y: number }) => void;
// position: { x: number; y: number };
// setPosition: (position: { x: number; y: number }) => void;
}
const usePopupStore = create<PopupState>()((set) => ({
popup: null,
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
setPopup: (popup) => set({ popup }),
// position: { x: 0, y: 0 },
// setPosition: (position) => set({ position }),
}));
export default usePopupStore;