diff --git a/client/src/components/toasts/ToastLayout.tsx b/client/src/components/toasts/ToastLayout.tsx
new file mode 100644
index 0000000..8f3567d
--- /dev/null
+++ b/client/src/components/toasts/ToastLayout.tsx
@@ -0,0 +1,61 @@
+import clsx from "clsx";
+import React from "react";
+import Button from "../ui/Button";
+import useToastsStore from "../../store/toastsStore";
+
+export interface ToastLayoutProps {
+ id: string;
+ type: "notification" | "warning";
+ title: string;
+ message: string;
+ onDeny: () => void;
+ onAllow: () => void;
+ image?: string;
+ icon?: React.ReactNode;
+}
+
+export default function ToastLayout({
+ id,
+ type,
+ title,
+ message,
+ onDeny,
+ onAllow,
+}: ToastLayoutProps) {
+ const { removeToast } = useToastsStore();
+
+ function handleDeny() {
+ onDeny();
+ removeToast(id);
+ }
+
+ function handleAllow() {
+ onAllow();
+ removeToast(id);
+ }
+
+ return (
+
+
+
+ {title}
+
+
{message}
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/toasts/ToastsContainer.tsx b/client/src/components/toasts/ToastsContainer.tsx
new file mode 100644
index 0000000..fe7f8b6
--- /dev/null
+++ b/client/src/components/toasts/ToastsContainer.tsx
@@ -0,0 +1,31 @@
+import useToastsStore from "../../store/toastsStore";
+import ToastLayout from "./ToastLayout";
+import { AnimatePresence, motion } from "motion/react";
+
+export default function ToastsContainer() {
+ const { toasts } = useToastsStore();
+
+ return (
+
+
+ {toasts.map((toast) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/components/ui/ActionsPopover.tsx b/client/src/components/ui/ActionsPopover.tsx
index 831a101..bdd8627 100644
--- a/client/src/components/ui/ActionsPopover.tsx
+++ b/client/src/components/ui/ActionsPopover.tsx
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from "react";
import Button from "./Button";
import MoreIcon from "../icons/MoreIcon";
import PopoverWrapper from "./PopoverWrapper";
+import clsx from "clsx";
interface ActionsPopoverProps {
options: {
@@ -10,47 +11,79 @@ interface ActionsPopoverProps {
icon?: React.ReactNode;
disabled?: boolean;
}[];
+ isOpened?: boolean;
+ parentRef?: React.RefObject
;
+ className?: string;
}
-export default function ActionsPopover({ options }: ActionsPopoverProps) {
- const [isOpened, setIsOpened] = useState(false);
-
+/**
+ * @param parentRef - Если передан, то кнопка активации опций не будет отображаться, а сам Popover будет отображаться по клику на parentRef.
+ */
+export default function ActionsPopover({
+ options,
+ isOpened = false,
+ parentRef,
+ className,
+}: ActionsPopoverProps) {
+ const [opened, setOpened] = useState(isOpened);
const popoverRef = useRef(null);
const buttonRef = useRef(null);
useEffect(() => {
+ let isHandling = false;
+
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
- if (
- popoverRef.current &&
- !popoverRef.current.contains(event.target as Node)
- ) {
- setIsOpened(false);
+ // Предотвращаем двойное срабатывание mousedown и touchstart
+ if (isHandling) return;
+ isHandling = true;
+ setTimeout(() => {
+ isHandling = false;
+ }, 200);
+
+ const tgt = event.target as Node;
+ const clickedInsideParentElement = parentRef?.current?.contains(tgt);
+ const clickedInsidePopover = popoverRef.current?.contains(tgt);
+ if (clickedInsideParentElement && !clickedInsidePopover) {
+ //
+ } else {
+ // Если нет parentRef - закрытие только по клику вне popover и вне кнопки
+ const clickedInsideButton = buttonRef.current?.contains(tgt);
+ if (!clickedInsidePopover && !clickedInsideButton) {
+ setOpened(false);
+ }
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
+ parentRef?.current?.addEventListener("touchend", () => setOpened(!opened));
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
+ parentRef?.current?.addEventListener("touchend", () =>
+ setOpened(!opened)
+ );
};
- }, []);
+ }, [parentRef]);
return (
-
-
+
+ {!parentRef && (
+
+ )}
+
{options.map((option) => (
))}
diff --git a/client/src/components/ui/Avatar.tsx b/client/src/components/ui/Avatar.tsx
index 96d3e94..2a7df1c 100644
--- a/client/src/components/ui/Avatar.tsx
+++ b/client/src/components/ui/Avatar.tsx
@@ -15,18 +15,21 @@ export default function Avatar({ size, status, src, name }: AvatarProps) {
{GetAvatarImage(src, name)}
{status === "caution" && (
diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx
index a186fb6..4a80847 100644
--- a/client/src/components/ui/Button.tsx
+++ b/client/src/components/ui/Button.tsx
@@ -29,7 +29,7 @@ function Button({
onClick?.(e);
}}
className={clsx(
- "transition-all select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
+ "transition-colors select-none cursor-pointer disabled:!cursor-default flex outline-none gap-2 items-center justify-center font-medium disabled:bg-[#F6F6F6] disabled:!text-[#D6D6D6]",
isActive && "bg-[#F3F1FD] !text-[#7B60F3]",
variant === "menu" &&
"text-[#7D7D7D] hover:bg-[#F3F3F3] active:bg-[#F3F1FD] active:text-[#7B60F3]",
diff --git a/client/src/components/ui/ControlButton.tsx b/client/src/components/ui/ControlButton.tsx
index 0530f66..8b26d4b 100644
--- a/client/src/components/ui/ControlButton.tsx
+++ b/client/src/components/ui/ControlButton.tsx
@@ -4,12 +4,10 @@ interface ControlButtonProps
extends React.ButtonHTMLAttributes
{
size: "small" | "large";
icon: React.ReactNode;
- enabled: boolean;
}
function ControlButton({
size,
- enabled,
icon,
className,
onClick,
@@ -20,13 +18,10 @@ function ControlButton({
onClick={onClick}
{...props}
className={clsx(
- "backdrop-blur-[10px] rounded-full transition-all cursor-pointer disabled:!cursor-default outline-none",
- size === "large" ? "2xl:p-[0.833vw] p-3" : "2xl:p-[0.417vw] p-[6px]",
- !enabled
- ? "bg-[#FF4517] hover:bg-[#FF4517]/85"
- : size === "large"
- ? "bg-[#FFFFFF]/15 hover:bg-[#FFFFFF]/25"
- : "bg-[#141414]/15 hover:bg-[#141414]/25",
+ "backdrop-blur-[10px] rounded-full transition-colors cursor-pointer disabled:!cursor-default outline-none disabled:bg-[#FF4517] disabled:hover:bg-[#FF4517]/85",
+ size === "large"
+ ? "2xl:p-[0.833vw] p-3 enabled:bg-[#FFFFFF]/15 enabled:hover:bg-[#FFFFFF]/25"
+ : "2xl:p-[0.417vw] p-[6px] enabled:bg-[#141414]/15 enabled:hover:bg-[#141414]/25",
className
)}
>
diff --git a/client/src/components/ui/ControlsPopover.tsx b/client/src/components/ui/ControlsPopover.tsx
index d99830f..1c3b19b 100644
--- a/client/src/components/ui/ControlsPopover.tsx
+++ b/client/src/components/ui/ControlsPopover.tsx
@@ -13,6 +13,7 @@ import ChatPopup from "../popups/ChatPopup";
import ParticipantsPopup from "../popups/ParticipantsPopup";
import SharePopup from "../popups/SharePopup";
import SettingsModal from "../modals/SettingsModal";
+import clsx from "clsx";
function ControlsPopover() {
const [isOpened, setIsOpened] = useState(false);
@@ -41,10 +42,10 @@ function ControlsPopover() {
const { setModal } = useModalStore();
return (
-
+
setIsOpened(!isOpened)}
>
@@ -53,7 +54,7 @@ function ControlsPopover() {