diff --git a/client/src/components/ModalContainer.tsx b/client/src/components/ModalContainer.tsx index 7aa26b5..17c39e8 100644 --- a/client/src/components/ModalContainer.tsx +++ b/client/src/components/ModalContainer.tsx @@ -1,5 +1,63 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useRef } from "react"; +import useModalStore from "../store/modalStore"; +import clsx from "clsx"; + function ModalContainer() { - return
; + const { modal, setModal, position } = useModalStore(); + const divRef = useRef(null); + const backdropRef = useRef(null); + const modalRef = useRef(null); + + function handleResize() { + if (!modalRef.current) return; + + if (divRef.current!.clientHeight > modalRef.current!.clientHeight) { + backdropRef.current!.style.height = `${divRef.current!.clientHeight}px`; + } else { + backdropRef.current!.style.height = `100%`; + } + } + + function handleKeydown(e: KeyboardEvent) { + if (e.key !== "Escape") return; + setModal(null); + } + + useEffect(() => { + window.addEventListener("resize", handleResize); + window.addEventListener("keydown", handleKeydown); + + return () => { + window.removeEventListener("resize", handleResize); + window.removeEventListener("keydown", handleKeydown); + }; + }, []); + + return ( + modal && ( +
+
+
+
+
setModal(null)} + /> + {modal} +
+
+
+
+ ) + ); } export default ModalContainer; diff --git a/client/src/components/ModalHeader.tsx b/client/src/components/ModalHeader.tsx new file mode 100644 index 0000000..6047dee --- /dev/null +++ b/client/src/components/ModalHeader.tsx @@ -0,0 +1,28 @@ +import Button from "./ui/Button"; +import useModalStore from "../store/modalStore"; +import XMarkIcon from "./icons/XMarkIcon"; + +interface ModalHeaderProps { + title?: string; + leftButton?: React.ReactNode; +} + +function ModalHeader({ title, leftButton }: ModalHeaderProps) { + const { setModal } = useModalStore(); + + return ( +
+
{leftButton}
+ {title && ( +

{title}

+ )} + +
+ ); +} + +export default ModalHeader; diff --git a/client/src/components/ModalWrapper.tsx b/client/src/components/ModalWrapper.tsx new file mode 100644 index 0000000..7e625ea --- /dev/null +++ b/client/src/components/ModalWrapper.tsx @@ -0,0 +1,25 @@ +import ModalHeader from "./ModalHeader"; +import clsx from "clsx"; + +interface ModalWrapperProps { + children: React.ReactNode; + title?: string; + leftButton?: React.ReactNode; + className?: string; +} + +function ModalWrapper({ + children, + title, + leftButton, + className, +}: ModalWrapperProps) { + return ( +
+ +
{children}
+
+ ); +} + +export default ModalWrapper; diff --git a/client/src/components/PopupContainer.tsx b/client/src/components/PopupContainer.tsx new file mode 100644 index 0000000..0bceddb --- /dev/null +++ b/client/src/components/PopupContainer.tsx @@ -0,0 +1,13 @@ +import usePopupStore from "../store/popupStore"; + +function PopupContainer() { + const { popup, position } = usePopupStore(); + + return ( +
+ {popup} +
+ ); +} + +export default PopupContainer; diff --git a/client/src/components/PopupHeader.tsx b/client/src/components/PopupHeader.tsx new file mode 100644 index 0000000..b2ee21a --- /dev/null +++ b/client/src/components/PopupHeader.tsx @@ -0,0 +1,33 @@ +import usePopupStore from "../store/popupStore"; +import XMarkIcon from "./icons/XMarkIcon"; +import Button from "./ui/Button"; + +interface PopupHeaderProps { + title?: string; + leftButton?: React.ReactNode; + headerRef: React.RefObject; +} + +function PopupHeader({ title, leftButton, headerRef }: PopupHeaderProps) { + const { setPopup } = usePopupStore(); + + return ( +
+
{leftButton}
+ {title && ( +

{title}

+ )} + +
+
+ ); +} + +export default PopupHeader; diff --git a/client/src/components/PopupWrapper.tsx b/client/src/components/PopupWrapper.tsx new file mode 100644 index 0000000..e2e633a --- /dev/null +++ b/client/src/components/PopupWrapper.tsx @@ -0,0 +1,92 @@ +/* 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"; + +interface PopupWrapperProps { + children: React.ReactNode; + className?: string; + title?: string; + leftButton?: React.ReactNode; + draggable?: boolean; +} + +function PopupWrapper({ + children, + className, + title, + leftButton, + draggable, +}: PopupWrapperProps) { + const { position, setPosition } = usePopupStore(); + const [mouseDown, setMouseDown] = useState(false); + const [mouseDownPosition, setMouseDownPosition] = useState(position); + + const wrapperRef = useRef(null); + const headerRef = useRef(null); + + useEffect(() => { + addEventListener("mouseup", () => setMouseDown(false)); + return () => removeEventListener("mouseup", () => setMouseDown(false)); + }, []); + + function handleMouseMove(e: MouseEvent) { + if (draggable && mouseDown && wrapperRef.current) { + e.preventDefault(); + setPosition({ + x: Math.min( + Math.max(0, position.x + e.clientX - mouseDownPosition.x), + window.innerWidth - wrapperRef.current.clientWidth + ), + y: Math.min( + Math.max(0, position.y + e.clientY - mouseDownPosition.y), + window.innerHeight - wrapperRef.current.clientHeight + ), + }); + setMouseDownPosition({ x: e.clientX, y: e.clientY }); + } + } + + useEffect(() => { + addEventListener("mousemove", handleMouseMove); + return () => removeEventListener("mousemove", handleMouseMove); + }, [handleMouseMove]); + + useEffect(() => { + if (headerRef.current) { + headerRef.current.addEventListener("mousedown", (e) => { + setMouseDown(true); + setMouseDownPosition({ x: e.clientX, y: e.clientY }); + }); + } + return () => { + if (headerRef.current) { + headerRef.current.removeEventListener("mousedown", (e) => { + setMouseDown(true); + setMouseDownPosition({ x: e.clientX, y: e.clientY }); + }); + } + }; + }, []); + + return ( +
+ +
{children}
+
+ ); +} + +export default PopupWrapper; diff --git a/client/src/components/modals/ShareModal.tsx b/client/src/components/modals/ShareModal.tsx index fcd4b14..4473987 100644 --- a/client/src/components/modals/ShareModal.tsx +++ b/client/src/components/modals/ShareModal.tsx @@ -1,24 +1,21 @@ import Button from "../ui/Button"; import LinkShare from "../ui/LinkShare"; import ShareFilledIcon from "../icons/ShareFilledIcon"; +import ModalWrapper from "../ModalWrapper"; export default function ShareModal() { return ( - <> +
-
Скопировать ссылку
+

Скопировать ссылку

- - +
); } diff --git a/client/src/components/popups/SharePopup.tsx b/client/src/components/popups/SharePopup.tsx new file mode 100644 index 0000000..b7bafa2 --- /dev/null +++ b/client/src/components/popups/SharePopup.tsx @@ -0,0 +1,34 @@ +import QRIcon from "../icons/QRIcon"; +import ShareFilledIcon from "../icons/ShareFilledIcon"; +import PopupWrapper from "../PopupWrapper"; +import Button from "../ui/Button"; +import LinkShare from "../ui/LinkShare"; + +function SharePopup() { + return ( + +
+ +
+ + } + > +
+

Скопировать ссылку

+ +
+ +
+ ); +} + +export default SharePopup; diff --git a/client/src/components/ui/LinkShare.tsx b/client/src/components/ui/LinkShare.tsx index 51ee4f0..a21690a 100644 --- a/client/src/components/ui/LinkShare.tsx +++ b/client/src/components/ui/LinkShare.tsx @@ -20,7 +20,7 @@ export default function LinkShare({ link }: { link: string }) { return (
{link} @@ -30,7 +30,7 @@ export default function LinkShare({ link }: { link: string }) {