diff --git a/client/.gitignore b/client/.gitignore index 438657a..01cf12d 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -23,3 +23,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env diff --git a/client/src/components/popups/ParticipantsPopup.tsx b/client/src/components/popups/ParticipantsPopup.tsx index c13bfee..6d3f68d 100644 --- a/client/src/components/popups/ParticipantsPopup.tsx +++ b/client/src/components/popups/ParticipantsPopup.tsx @@ -30,7 +30,7 @@ function ParticipantItem({ id }: { id: string }) { return (
- +
Иван Иванович {id} Роль diff --git a/client/src/components/ui/Avatar.tsx b/client/src/components/ui/Avatar.tsx index 45552ca..96d3e94 100644 --- a/client/src/components/ui/Avatar.tsx +++ b/client/src/components/ui/Avatar.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import Warning from "../indicators/Warning"; import Admin from "../indicators/Admin"; +import Tooltip from "./Tooltip"; interface AvatarProps { size: "small" | "medium" | "large"; @@ -28,8 +29,16 @@ export default function Avatar({ size, status, src, name }: AvatarProps) { size === "large" && "bottom-[2.5vw] left-[2.5vw]" )} > - {status === "caution" && } - {status === "admin" && } + {status === "caution" && ( + + + + )} + {status === "admin" && ( + + + + )}
); diff --git a/client/src/components/ui/Tooltip.tsx b/client/src/components/ui/Tooltip.tsx new file mode 100644 index 0000000..022b4cb --- /dev/null +++ b/client/src/components/ui/Tooltip.tsx @@ -0,0 +1,152 @@ +import { motion } from "motion/react"; +import React, { useEffect, useRef, useState } from "react"; + +interface TooltipProps { + label: string; + children: React.ReactNode; + position?: "top" | "bottom" | "left" | "right" | "cursor"; + showDelay?: number; +} + +interface TooltipPosition { + top?: number | string; + left?: number | string; + bottom?: number | string; + right?: number | string; + transform?: string; +} + +export default function Tooltip({ + label, + children, + position = "top", + showDelay = 500, +}: TooltipProps) { + const [isVisible, setIsVisible] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState({ + top: 0, + left: 0, + transform: "none", + }); + const tooltipWrapperRef = useRef(null); + const tooltipRef = useRef(null); + + useEffect(() => { + if (!tooltipWrapperRef.current) return; + const current = tooltipWrapperRef.current; + + current.addEventListener("mouseenter", () => setIsVisible(true)); + current.addEventListener("mouseleave", () => setIsVisible(false)); + return () => { + current?.removeEventListener("mouseenter", () => setIsVisible(true)); + current?.removeEventListener("mouseleave", () => setIsVisible(false)); + }; + }, []); + + useEffect(() => { + if (!isVisible || !tooltipRef.current || !tooltipWrapperRef.current) return; + const pos = calculatePosition(position, tooltipRef, tooltipWrapperRef); + setTooltipPosition(pos); + }, [isVisible, position]); + + return ( +
+ {children} + {isVisible && ( + + } + /> + + )} +
+ ); +} + +function TooltipContent({ + label, + position, + ref, +}: { + label: string; + position: TooltipPosition; + ref: React.RefObject; +}) { + return ( +
+ {label} +
+ ); +} + +function calculatePosition( + position: TooltipProps["position"], + tooltipRef: React.RefObject, + tooltipWrapperRef: React.RefObject +) { + if (!tooltipWrapperRef.current || !tooltipRef.current) return {}; + + const wrapperRect = tooltipWrapperRef.current.getBoundingClientRect(); + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + const GAP = 8; + + function adjustHorizontal() { + let left = wrapperRect.width / 2; + let transform = "translateX(-50%)"; + + const tooltipLeft = wrapperRect.left + left - tooltipRect.width / 2; + const tooltipRight = tooltipLeft + tooltipRect.width; + + // Если не влезает горизонтально, то сдвигаем + if (tooltipLeft < GAP) { + left = GAP - wrapperRect.left; + transform = "none"; + } else if (tooltipRight > window.innerWidth - GAP) { + left = window.innerWidth - GAP - wrapperRect.left - tooltipRect.width; + transform = "none"; + } + + return { left, transform }; + } + + function adjustVertical() { + return { top: wrapperRect.height / 2, transform: "translateY(-50%)" }; + } + + switch (position) { + case "top": { + return wrapperRect.bottom + tooltipRect.height + GAP < 0 + ? { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() } + : { top: -tooltipRect.height - GAP, ...adjustHorizontal() }; + } + case "bottom": { + return wrapperRect.bottom + tooltipRect.height + GAP > window.innerHeight + ? { top: -tooltipRect.height - GAP, ...adjustHorizontal() } + : { bottom: -tooltipRect.height - GAP, ...adjustHorizontal() }; + } + case "left": { + return wrapperRect.left - tooltipRect.width - GAP < 0 + ? { right: -tooltipRect.width - GAP, ...adjustVertical() } + : { left: -tooltipRect.width - GAP, ...adjustVertical() }; + } + case "right": { + return wrapperRect.right + tooltipRect.width + GAP > window.innerWidth + ? { left: -tooltipRect.width - GAP, ...adjustVertical() } + : { right: -tooltipRect.width - GAP, ...adjustVertical() }; + } + case "cursor": { + return { transform: "none" }; + } + } + return {}; +}