Participants mobile;

This commit is contained in:
2025-10-21 11:51:47 +05:00
parent 006abd1e43
commit 6bff06ae84
7 changed files with 134 additions and 72 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ export default function Warning({
return (
<div
className={clsx(
"size-[1.111vw] border-[1px] border-white rounded-full flex items-center justify-center ",
"2xl:size-[1.111vw] size-[4.444vw] border-[1px] border-white rounded-full flex items-center justify-center ",
type === "caution" && "bg-[#F9A530]",
type === "critical" && "bg-[#FF4517]",
className
@@ -7,9 +7,12 @@ import XMarkFilledIcon from "../icons/XMarkFilledIcon";
import Avatar from "../ui/Avatar";
import Button from "../ui/Button";
import ShareFilledIcon from "../icons/ShareFilledIcon";
import HandRaisedOffFilledIcon from "../icons/HandRaisedOffFilledIcon";
import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon";
import { Fragment, useRef } from "react";
export default function ParticipantsPopup() {
const participants = [1, 2, 3];
const participants = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return (
<PopupWrapper
@@ -18,14 +21,14 @@ export default function ParticipantsPopup() {
className="h-max 2xl:w-[21.667vw]"
>
<div className="flex flex-col gap-[1.667vw] relative">
<ul className="flex flex-col gap-[1.111vw]">
<ul className="flex flex-col gap-0 2xl:gap-[1.111vw] 2xl:max-h-auto max-h-[calc(100dvh-50vw)] overflow-y-auto">
{participants.map((participant, index) => (
<>
<ParticipantItem key={participant} id={participant.toString()} />
<Fragment key={index}>
<ParticipantItem id={participant.toString()} />
{index !== participants.length - 1 && (
<div className="w-full h-[1px] bg-[#F6F6F6]" />
)}
</>
</Fragment>
))}
</ul>
<Button
@@ -53,9 +56,18 @@ export default function ParticipantsPopup() {
}
function ParticipantItem({ id }: { id: string }) {
const isMuted = true;
const isNotControlling = true;
const isMobile = window.innerWidth <= 360;
const parentRef = useRef<HTMLDivElement>(null);
return (
<div className="flex items-center justify-between w-full h-[2.5vw]">
<div className="flex items-center gap-[0.833vw]">
<div
ref={parentRef}
onTouchEnd={(ev) => ev.stopPropagation()}
className="flex items-center justify-between w-full relative py-[2.222vw] 2xl:p-0"
>
<div className="flex items-center 2xl:gap-[0.833vw] gap-[3.333vw]">
<Avatar size="medium" status="caution" />
<div className="flex flex-col gap-[0.278vw]">
<span className="button-m">Иван Иванович {id}</span>
@@ -63,7 +75,18 @@ function ParticipantItem({ id }: { id: string }) {
</div>
</div>
<div>
<div className="flex 2xl:gap-[0.556vw] gap-[2.222vw] items-center">
{isNotControlling && (
<div className="2xl:size-[1.111vw] size-[4.444vw] text-[#FF4517]">
<HandRaisedOffFilledIcon />
</div>
)}
{isMuted && (
<div className="2xl:size-[1.111vw] size-[4.444vw] text-[#FF4517]">
<MicrophoneOffFilledIcon />
</div>
)}
<ActionsPopover
options={[
{
@@ -88,6 +111,8 @@ function ParticipantItem({ id }: { id: string }) {
onClick: () => {},
},
]}
parentRef={isMobile ? parentRef : undefined}
className={isMobile ? "left-0" : undefined}
/>
</div>
</div>
+58 -23
View File
@@ -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<HTMLDivElement | null>;
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<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(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 (
<div className="relative" ref={popoverRef}>
<button
ref={buttonRef}
className="flex items-center justify-center gap-[0.139vw] size-[1.667vw] rounded-[0.556vw] text-[#CCCCCC] hover:text-[#7D7D7D] hover:bg-[#F3F3F3] active:text-[#141414] "
onClick={() => setIsOpened(!isOpened)}
>
<div className="2xl:size-[1.111vw] size-4 2xl:rounded-[0.556vw] rounded-2xl">
<MoreIcon />
</div>
</button>
<div className={!parentRef ? "relative" : ""} ref={popoverRef}>
{!parentRef && (
<button
ref={buttonRef}
className="flex items-center justify-center gap-[0.139vw] size-[1.667vw] rounded-[0.556vw] text-[#CCCCCC] hover:text-[#7D7D7D] hover:bg-[#F3F3F3] active:text-[#141414] "
onClick={() => setOpened(!opened)}
>
<div className="2xl:size-[1.111vw] size-4 2xl:rounded-[0.556vw] rounded-2xl">
<MoreIcon />
</div>
</button>
)}
<PopoverWrapper
isOpened={isOpened}
buttonRef={buttonRef}
className="w-[17.222vw]"
isOpened={opened}
parentElRef={parentRef ? parentRef : buttonRef}
className={clsx("2xl:w-[17.222vw] w-[53.333vw]", className)}
>
{options.map((option) => (
<Button
@@ -60,7 +93,9 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
onClick={option.onClick}
disabled={option.disabled}
>
<div className="size-[1.111vw] ">{option.icon}</div>
<div className="2xl:size-[1.111vw] size-[4.444vw]">
{option.icon}
</div>
{option.label}
</Button>
))}
+9 -6
View File
@@ -15,18 +15,21 @@ export default function Avatar({ size, status, src, name }: AvatarProps) {
<div
className={clsx(
"rounded-full text-white relative",
size === "small" && "size-[2.222vw] button-s",
size === "medium" && "size-[2.5vw] button-m",
size === "large" && "size-[3.333vw] title-s"
size === "small" && "2xl:size-[2.222vw] size-[8.889vw] button-s",
size === "medium" && "2xl:size-[2.5vw] size-[10vw] button-m",
size === "large" && "2xl:size-[3.333vw] size-[13.333vw] title-s"
)}
>
{GetAvatarImage(src, name)}
<div
className={clsx(
"absolute",
size === "small" && "bottom-[1.389vw] left-[1.389vw]",
size === "medium" && "bottom-[1.667vw] left-[1.667vw]",
size === "large" && "bottom-[2.5vw] left-[2.5vw]"
size === "small" &&
"2xl:bottom-[1.389vw] bottom-[5.556vw] 2xl:left-[1.389vw] left-[5.556vw]",
size === "medium" &&
"2xl:bottom-[1.667vw] bottom-[6.667vw] 2xl:left-[1.667vw] left-[6.667vw]",
size === "large" &&
"2xl:bottom-[2.5vw] bottom-[10vw] 2xl:left-[2.5vw] left-[10vw]"
)}
>
{status === "caution" && (
+1 -1
View File
@@ -54,7 +54,7 @@ function ControlsPopover() {
</FloatingActionButton>
<PopoverWrapper
isOpened={isOpened}
buttonRef={buttonRef}
parentElRef={buttonRef}
className="w-[248px] !bottom-[72px] min-h-full !fixed left-1/2 -translate-x-1/2"
>
<Button
+5 -5
View File
@@ -4,14 +4,14 @@ import clsx from "clsx";
interface PopoverProps {
isOpened: boolean;
buttonRef: React.RefObject<HTMLButtonElement | null>;
parentElRef: React.RefObject<HTMLButtonElement | HTMLDivElement | null>;
children: React.ReactNode;
className?: string;
}
function PopoverWrapper({
isOpened,
buttonRef,
parentElRef,
children,
className,
}: PopoverProps) {
@@ -19,13 +19,13 @@ function PopoverWrapper({
const [menuHeight, setMenuHeight] = useState(0);
useEffect(() => {
if (isOpened && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
if (isOpened && parentElRef.current) {
const buttonRect = parentElRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
}
}, [buttonRef, isOpened, menuHeight]);
}, [parentElRef, isOpened, menuHeight]);
return (
<AnimatePresence>
+27 -28
View File
@@ -9,10 +9,11 @@ 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 { useEffect } from "react";
// import useToastsStore from "../store/toastsStore";
import ChatPopup from "../components/popups/ChatPopup";
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
function HomePage() {
const { data: user } = useMe();
@@ -27,30 +28,30 @@ function HomePage() {
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
// -------------------------------- Toasts test --------------------------------
const { addToast } = useToastsStore();
// -------------------------------- Тосты --------------------------------
// const { addToast } = useToastsStore();
useEffect(() => {
addToast({
id: crypto.randomUUID(),
type: "warning",
title: "Тестовое предупреждение",
message: "Это тестовое предупреждение",
onDeny: () => {},
onAllow: () => {},
});
const timer = setTimeout(() => {
addToast({
id: crypto.randomUUID(),
type: "notification",
title: "Тестовое уведомление",
message: "Это тестовое уведомление",
onDeny: () => {},
onAllow: () => {},
});
}, 1000);
return () => clearTimeout(timer);
}, []);
// useEffect(() => {
// addToast({
// id: crypto.randomUUID(),
// type: "warning",
// title: "Тестовое предупреждение",
// message: "Это тестовое предупреждение",
// onDeny: () => {},
// onAllow: () => {},
// });
// const timer = setTimeout(() => {
// addToast({
// id: crypto.randomUUID(),
// type: "notification",
// title: "Тестовое уведомление",
// message: "Это тестовое уведомление",
// onDeny: () => {},
// onAllow: () => {},
// });
// }, 1000);
// return () => clearTimeout(timer);
// }, []);
return (
<div className="py-8 min-h-screen bg-gray-50">
@@ -62,9 +63,7 @@ function HomePage() {
<FloatingActionButton
variant="default"
onClick={() =>
setPopup(<SharePopup link="https://estate.stream/ahdy12jdco1" />)
}
onClick={() => setPopup(<ParticipantsPopup />)}
>
<div className="2xl:size-[1.111vw] size-4 text-white">
<ShareFilledIcon />