Refactor UI components and add NewSessionPage; replace TestPage with NewSessionPage, implement ActionsSidebarWrapper, and enhance ActionsPopover and ControlsPopover with Popover component for improved UI interactions.

This commit is contained in:
2025-10-15 17:00:30 +05:00
parent 728d727cd1
commit d4d5bf609f
8 changed files with 257 additions and 44 deletions
@@ -0,0 +1,34 @@
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx";
interface ActionsSidebarWrapperProps {
children: React.ReactNode;
className?: string;
show?: boolean;
}
function ActionsSidebarWrapper({
children,
className,
show = true,
}: ActionsSidebarWrapperProps) {
return (
<AnimatePresence>
{show && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"flex 2xl:flex 2xl:gap-[0.556vw] 2xl:flex-col gap-2 max-2xl:p-2 max-2xl:rounded-[32px] absolute 2xl:top-1/2 2xl:-translate-y-1/2 2xl:right-[1.111vw] max-2xl:left-1/2 max-2xl:-translate-x-1/2 max-2xl:bottom-2 max-2xl:landscape:bg-[#00000026] max-2xl:landscape:backdrop-blur",
className
)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
export default ActionsSidebarWrapper;
+19 -40
View File
@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from "react";
import Button from "./Button";
import MoreIcon from "../icons/MoreIcon";
import { AnimatePresence, motion } from "motion/react";
import Popover from "./Popover";
interface ActionsPopoverProps {
options: {
@@ -14,10 +14,9 @@ interface ActionsPopoverProps {
export default function ActionsPopover({ options }: ActionsPopoverProps) {
const [isOpened, setIsOpened] = useState(false);
const [openUpwards, setOpenUpwards] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [menuHeight, setMenuHeight] = useState(0);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
@@ -37,15 +36,6 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
};
}, []);
useEffect(() => {
if (isOpened && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
}
}, [isOpened, menuHeight, options.length]);
return (
<div className="relative" ref={popoverRef}>
<button
@@ -57,35 +47,24 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
<MoreIcon />
</div>
</button>
<AnimatePresence>
{isOpened && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={(el) => {
setMenuHeight(el?.offsetHeight || 0);
}}
className={`absolute z-10 right-0 w-[13.333vw] bg-white rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden ${
openUpwards ? "bottom-[100%]" : "top-[100%]"
}`}
<Popover
isOpened={isOpened}
buttonRef={buttonRef}
className="w-[17.222vw]"
>
{options.map((option) => (
<Button
variant="tertiary"
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
key={option.label}
onClick={option.onClick}
disabled={option.disabled}
>
{options.map((option) => (
<Button
variant="tertiary"
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
key={option.label}
onClick={option.onClick}
disabled={option.disabled}
>
<div className="size-[1.111vw] ">{option.icon}</div>
{option.label}
</Button>
))}
</motion.div>
)}
</AnimatePresence>
<div className="size-[1.111vw] ">{option.icon}</div>
{option.label}
</Button>
))}
</Popover>
</div>
);
}
@@ -0,0 +1,78 @@
import FloatingActionButton from "./FloatingActionButton";
import Popover from "./Popover";
import MoreIcon from "../icons/MoreIcon";
import { useEffect, useRef, useState } from "react";
import Button from "./Button";
import ChatFilledIcon from "../icons/ChatFilledIcon";
import UsersFilledIcon from "../icons/UsersFilledIcon";
import ShareFilledIcon from "../icons/ShareFilledIcon";
import CogFilledIcon from "../icons/CogFilledIcon";
function ControlsPopover() {
const [isOpened, setIsOpened] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
if (
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setIsOpened(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, []);
return (
<div className="2xl:hidden order-3 relative">
<FloatingActionButton
ref={buttonRef}
className="!bg-[#7B60F3]"
onClick={() => setIsOpened(!isOpened)}
>
<div className="size-4 text-white">
<MoreIcon />
</div>
</FloatingActionButton>
<Popover
isOpened={isOpened}
buttonRef={buttonRef}
className="w-[248px] bottom-[72px]"
>
<Button variant="tertiary" className="w-full !justify-start">
<div className="size-4">
<ChatFilledIcon />
</div>
Чат
</Button>
<Button variant="tertiary" className="w-full !justify-start">
<div className="size-4">
<UsersFilledIcon />
</div>
Участники
</Button>
<Button variant="tertiary" className="w-full !justify-start">
<div className="size-4">
<ShareFilledIcon />
</div>
Пригласить
</Button>
<Button variant="tertiary" className="w-full !justify-start">
<div className="size-4">
<CogFilledIcon />
</div>
Настройки
</Button>
</Popover>
</div>
);
}
export default ControlsPopover;
@@ -3,7 +3,8 @@ import clsx from "clsx";
interface FloatingActionButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant: "default" | "critical";
variant?: "default" | "critical";
ref?: React.RefObject<HTMLButtonElement | null>;
}
function FloatingActionButton({
@@ -11,11 +12,13 @@ function FloatingActionButton({
variant = "default",
className,
onClick,
ref,
...props
}: FloatingActionButtonProps) {
return (
<button
onClick={onClick}
ref={ref}
className={clsx(
"2xl:p-[0.833vw] p-3 rounded-full transition-all cursor-pointer disabled:!cursor-default outline-none backdrop-blur-[10px]",
variant === "default" &&
+48
View File
@@ -0,0 +1,48 @@
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx";
interface PopoverProps {
isOpened: boolean;
buttonRef: React.RefObject<HTMLButtonElement | null>;
children: React.ReactNode;
className?: string;
}
function Popover({ isOpened, buttonRef, children, className }: PopoverProps) {
const [openUpwards, setOpenUpwards] = useState(false);
const [menuHeight, setMenuHeight] = useState(0);
useEffect(() => {
if (isOpened && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - buttonRect.bottom;
const spaceAbove = buttonRect.top;
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
}
}, [buttonRef, isOpened, menuHeight]);
return (
<AnimatePresence>
{isOpened && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
ref={(el) => {
setMenuHeight(el?.offsetHeight || 0);
}}
className={clsx(
"absolute z-10 right-0 bg-white 2xl:rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden rounded-2xl 2xl:p-[0.278vw] p-1",
openUpwards ? "bottom-full" : "top-full",
className
)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
export default Popover;
+3 -2
View File
@@ -5,13 +5,14 @@ import { createBrowserRouter, RouterProvider } from "react-router";
import SessionPage from "./pages/SessionPage";
import LoginPage from "./pages/LoginPage";
import RegisterPage from "./pages/RegisterPage";
import TestPage from "./pages/TestPage";
// import TestPage from "./pages/TestPage";
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "./lib/queryClient";
import ProtectedRoute from "./components/ProtectedRoute";
import PublicRoute from "./components/PublicRoute";
import ModalContainer from "./components/ModalContainer";
import PopupContainer from "./components/PopupContainer";
import NewSessionPage from "./pages/NewSessionPage";
const router = createBrowserRouter([
{
@@ -40,7 +41,7 @@ const router = createBrowserRouter([
},
{
path: "/test",
element: <TestPage />,
element: <NewSessionPage />,
},
{
path: "/sessions/:id",
+70
View File
@@ -0,0 +1,70 @@
import ActionsSidebarWrapper from "../components/ActionsSidebarWrapper";
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
import CogFilledIcon from "../components/icons/CogFilledIcon";
import ExitFilledIcon from "../components/icons/ExitFilledIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import MicrophoneFilledIcon from "../components/icons/MicrophoneFilledIcon";
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
import UsersFilledIcon from "../components/icons/UsersFilledIcon";
import VideoOffFilledIcon from "../components/icons/VideoOffFilledIcon";
import FloatingActionButton from "../components/ui/FloatingActionButton";
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
import usePopupStore from "../store/popupStore";
import ControlsPopover from "../components/ui/ControlsPopover";
function NewSessionPage() {
const { setPopup } = usePopupStore();
return (
<div className="relative w-screen h-screen bg-[#DADADA] order-3">
<ActionsSidebarWrapper>
<FloatingActionButton
className="max-2xl:hidden"
onClick={() => setPopup(<ParticipantsPopup />)}
>
<div className="2xl:size-[1.111vw] text-white">
<ChatFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="max-2xl:hidden">
<div className="2xl:size-[1.111vw] text-white">
<UsersFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="max-2xl:hidden">
<div className="2xl:size-[1.111vw] text-white">
<ShareFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="max-2xl:hidden">
<div className="2xl:size-[1.111vw] text-white">
<CogFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="2xl:hidden">
<div className="size-4 text-white">
<MicrophoneFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="2xl:hidden">
<div className="size-4 text-white">
<VideoOffFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton className="max-2xl:order-2">
<div className="2xl:size-[1.111vw] size-4 text-white">
<FullscreenIcon />
</div>
</FloatingActionButton>
<FloatingActionButton variant="critical" className="max-2xl:order-1">
<div className="2xl:size-[1.111vw] size-4 text-white">
<ExitFilledIcon />
</div>
</FloatingActionButton>
<ControlsPopover />
</ActionsSidebarWrapper>
</div>
);
}
export default NewSessionPage;
+1 -1
View File
@@ -4,7 +4,7 @@ export default {
theme: {
extend: {},
screens: {
"2xl": { min: "1440px" },
"2xl": "1440px",
},
},
plugins: [],