import { CloseOutlined } from "@ant-design/icons"; import { Button } from "antd"; import { useCallback, useEffect, useRef, useState } from "react"; import { useAppSelector } from "redux/hooks"; import { darkColors, lightColors } from "ThemeConstants"; interface ProBottomSheetProps { isOpen: boolean; onClose: () => void; title?: string; children: React.ReactNode; height?: string | number; showHandle?: boolean; showCloseButton?: boolean; backdrop?: boolean; snapPoints?: string[] | number[]; initialSnap?: number; className?: string; themeName?: "light" | "dark"; contentStyle?: React.CSSProperties; } export function ProBottomSheet({ isOpen, onClose, title, children, height = 500, showHandle = true, showCloseButton = true, backdrop = true, snapPoints = [500], initialSnap = 0, className = "", contentStyle = {}, }: ProBottomSheetProps) { const [currentSnap, setCurrentSnap] = useState(initialSnap); const [isDragging, setIsDragging] = useState(false); const startPosRef = useRef(0); const sheetRef = useRef(null); const { themeName } = useAppSelector((state) => state.theme); // Handle body scroll locking useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [isOpen]); // Close mobile keyboard when bottom sheet opens useEffect(() => { if (isOpen) { // Blur any active input element to close the mobile keyboard const activeElement = document.activeElement as HTMLElement; if ( activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable) ) { activeElement.blur(); } } }, [isOpen]); // Event handlers const startDrag = useCallback((clientY: number) => { setIsDragging(true); startPosRef.current = clientY; }, []); const handleDrag = useCallback( (clientY: number) => { if (!isDragging || !sheetRef.current) return; const deltaY = clientY - startPosRef.current; sheetRef.current.style.transform = `translateY(${Math.max(0, deltaY)}px)`; }, [isDragging], ); const endDrag = useCallback( (clientY: number) => { if (!isDragging) return; setIsDragging(false); const deltaY = clientY - startPosRef.current; const threshold = 50; // Reduced threshold for better responsiveness if (deltaY > threshold) { // Swipe down if (currentSnap > 0) { setCurrentSnap(currentSnap - 1); } else { onClose(); } } else if (deltaY < -threshold && currentSnap < snapPoints.length - 1) { // Swipe up setCurrentSnap(currentSnap + 1); } // Reset transform if (sheetRef.current) { sheetRef.current.style.transform = "translateY(0)"; } }, [isDragging, currentSnap, snapPoints.length, onClose], ); // Track if drag started from handle/header area const dragStartElementRef = useRef(null); // Touch event handlers const handleTouchStart = useCallback( (e: React.TouchEvent) => { // Only allow drag if starting from handle or header area const target = e.target as HTMLElement; const isHandle = target.closest('[style*="cursor: grab"]') !== null; const isHeader = target.closest('[style*="borderBottom"]') !== null; if (isHandle || isHeader) { dragStartElementRef.current = target; startDrag(e.touches[0].clientY); } }, [startDrag], ); const handleTouchMove = useCallback( (e: React.TouchEvent) => { // Only handle drag if it started from handle/header if (!isDragging || !dragStartElementRef.current) return; // Prevent scroll from affecting the bottom sheet e.preventDefault(); handleDrag(e.touches[0].clientY); }, [handleDrag, isDragging], ); const handleTouchEnd = useCallback( (e: React.TouchEvent) => { if (dragStartElementRef.current) { endDrag(e.changedTouches[0].clientY); dragStartElementRef.current = null; } }, [endDrag], ); // Mouse event handlers const handleMouseDown = useCallback( (e: React.MouseEvent) => { startDrag(e.clientY); }, [startDrag], ); const handleMouseMove = useCallback( (e: MouseEvent) => { handleDrag(e.clientY); }, [handleDrag], ); const handleMouseUp = useCallback( (e: MouseEvent) => { endDrag(e.clientY); }, [endDrag], ); // Mouse event listeners useEffect(() => { if (isDragging) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging, handleMouseMove, handleMouseUp]); // Enhanced styles with better dark themeName support const backdropStyle: React.CSSProperties = { position: "fixed", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: themeName === "dark" ? "rgba(0, 0, 0, 0.7)" : "rgba(0, 0, 0, 0.5)", backdropFilter: "blur(4px)", zIndex: 1000, opacity: isOpen ? 1 : 0, visibility: isOpen ? "visible" : "hidden", transition: "opacity 0.3s ease, visibility 0.3s ease", pointerEvents: isOpen ? "auto" : "none", }; const sheetStyle: React.CSSProperties = { position: "fixed", left: 0, right: 0, bottom: 0, height: snapPoints[currentSnap] || height, backgroundColor: themeName === "dark" ? darkColors.bgColor : lightColors.bgColor, borderTopLeftRadius: 20, borderTopRightRadius: 20, boxShadow: themeName === "dark" ? "0 -8px 32px rgba(0, 0, 0, 0.6)" : "0 -4px 20px rgba(0, 0, 0, 0.15)", zIndex: 1001, transform: isOpen ? "translateY(0)" : "translateY(100%)", transition: isDragging ? "none" : "transform 0.3s cubic-bezier(0.2, 0, 0, 1), height 0.3s ease", display: "flex", flexDirection: "column", maxHeight: height, border: `1px solid ${themeName === "dark" ? "#363636" : "#e5e7eb"}`, borderBottom: "none", backdropFilter: themeName === "dark" ? "blur(12px)" : "none", width: "100vw", }; const handleStyle: React.CSSProperties = { width: 40, height: 4, backgroundColor: themeName === "dark" ? "#525252" : "#d1d5db", borderRadius: 2, margin: "12px auto 8px", cursor: "grab", transition: "background-color 0.3s ease", }; const headerStyle: React.CSSProperties = { padding: "16px 20px", borderBottom: `1px solid ${themeName === "dark" ? "#363636" : "#e5e7eb"}`, display: "flex", alignItems: "center", justifyContent: "space-between", minHeight: 60, backdropFilter: themeName === "dark" ? "blur(8px)" : "none", }; const contentWrapperStyle: React.CSSProperties = { flex: 1, overflow: "auto", padding: "0 20px", backgroundColor: themeName === "dark" ? darkColors.bgColor : lightColors.bgColor, color: themeName === "dark" ? "#ffffff" : "#000000", ...contentStyle, }; const closeButtonStyle: React.CSSProperties = { color: themeName === "dark" ? "#ffffff" : "#4D5154", display: "flex", alignItems: "center", justifyContent: "center", padding: 8, borderRadius: "50%", backgroundColor: themeName === "dark" ? "rgba(54, 54, 54, 0.8)" : "#EDEEEE", border: `1px solid ${themeName === "dark" ? "#424242" : "transparent"}`, transition: "all 0.3s ease", cursor: "pointer", width: 30, height: 30, }; const titleStyle: React.CSSProperties = { fontSize: 18, fontWeight: 600, color: themeName === "dark" ? "#ffffff" : "#1f2937", margin: 0, textShadow: themeName === "dark" ? "0 1px 2px rgba(0, 0, 0, 0.3)" : "none", transition: "color 0.3s ease", }; return ( <> {backdrop &&
}
{showHandle && (
{ if (handleStyle) { handleStyle.backgroundColor = themeName === "dark" ? "#6b6b6b" : "#9ca3af"; } }} onMouseLeave={() => { if (handleStyle) { handleStyle.backgroundColor = themeName === "dark" ? "#525252" : "#d1d5db"; } }} /> )} {(title || showCloseButton) && (

{title}

{showCloseButton && (
)}
{ // Prevent touch events in content from triggering sheet drag e.stopPropagation(); }} onTouchMove={(e) => { // Allow normal scrolling in content e.stopPropagation(); }} > {children}
); }