feat: update ManagerSelect positioning and enhance ProjectSelector with dynamic positioning and animations

This commit is contained in:
2025-06-27 17:32:11 +05:00
parent fc7d55b10f
commit e8060c594d
4 changed files with 98 additions and 174 deletions
+2 -2
View File
@@ -78,8 +78,8 @@ function ManagerSelect({
className={clsx(
"absolute left-0 w-full z-10",
position === "top"
? "top-[calc(-11.389vw-0.278vw)]"
: "top-[calc(100%+0.278vw)]"
? "bottom-[calc(100%+4px)]"
: "top-[calc(100%+4px)]"
)}
>
<div
+4 -1
View File
@@ -96,7 +96,10 @@ function MultySelect<T extends { name: string; id: string }>({
<Button
variant="secondary"
className="!justify-start w-full text-s font-medium p-[0.833vw] text-[#7D7D7D] flex items-center gap-[0.278vw] cursor-pointer rounded-[0.278vw] hover:bg-[#F6F6F6] bg-white"
onClick={() => setSelectedValues([])}
onClick={() => {
setSelectedValues([]);
setIsSelectVisible(false);
}}
>
<span className="size-[1.111vw] flex items-center justify-center">
<CloseIcon />
+85 -108
View File
@@ -1,11 +1,10 @@
import { useEffect, useState } from "react";
import { App } from "../types/App";
import ChevronLeftIcon from "./icons/ChevronLeftIcon";
import CloseIcon from "./icons/CloseIcon";
import LightningIcon from "./icons/LightningIcon";
import Button from "./Button";
import CheckIcon from "./icons/CheckIcon";
import ChevronDownIcon from "./icons/ChevronDownIcon";
import CheckIcon from "./icons/CheckIcon";
import { useClickAway } from "@uidotdev/usehooks";
import { AnimatePresence, motion } from "motion/react";
import clsx from "clsx";
interface Props {
projects: App[];
@@ -21,124 +20,102 @@ function ProjectSelector({
activeProject,
}: Props) {
const [isOpen, setIsOpen] = useState(false);
const [pointedProject, setPointedProject] = useState<App | null>(null);
const [position, setPosition] = useState<"top" | "bottom">("bottom");
const selectRef = useClickAway<HTMLDivElement>(() => setIsOpen(false));
useEffect(() => {
setPointedProject(selectedProject);
}, [selectedProject]);
const rect = selectRef.current?.getBoundingClientRect();
if (rect) {
setPosition(rect.top > window.innerHeight / 2 ? "top" : "bottom");
}
}, [isOpen, selectRef]);
useEffect(() => {
const handleScroll = () => {
if (isOpen) {
const rect = selectRef.current?.getBoundingClientRect();
if (rect) {
setPosition(rect.top > window.innerHeight / 2 ? "top" : "bottom");
}
}
};
window.addEventListener("scroll", handleScroll, true);
return () => window.removeEventListener("scroll", handleScroll, true);
}, [isOpen, selectRef]);
const handleToggle = () => {
setIsOpen(!isOpen);
};
return (
<>
<button
className="p-[1.111vw] rounded-[0.833vw] bg-[#F6F6F6] flex justify-between items-center gap-[0.833vw]"
onClick={(e) => {
e.preventDefault();
setIsOpen(true);
}}
>
<div className="space-y-[0.278vw]">
<p className="caption-s text-[#7D7D7D] w-fit font-medium">Проект</p>
<p className="text-s font-medium w-fit">{selectedProject?.name}</p>
</div>
<div
ref={selectRef}
className={clsx(
"relative w-full rounded-[0.833vw] p-[1.111vw] bg-[#F6F6F6] cursor-pointer flex items-center justify-between select-none",
isOpen && "outline outline-[#7B60F3]"
)}
style={{ boxShadow: "0px 2px 2px 0px #0000000D" }}
onClick={handleToggle}
>
<div className="flex flex-col gap-[0.278vw]">
<div className="caption-s font-medium text-[#7D7D7D]">Проект</div>
<div className="flex items-center gap-[0.556vw]">
<img src="/images/app_image.png" className="size-[2.222vw]" alt="" />
<div className="size-[1.389vw] text-[#7D7D7D]">
<ChevronDownIcon />
</div>
<img src="/images/app_image.png" className="size-[1.111vw]" alt="" />
<div className="text-s">{selectedProject?.name}</div>
</div>
</button>
{isOpen && (
<div className="fixed z-1 h-[calc(100vh-16vw)] top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 rounded-[2.222vw] bg-white p-[1.389vw] flex flex-col gap-[2.778vw] w-[25vw]">
<div className="flex justify-between items-center">
<Button variant="secondary" size="small">
<div className="text-[#7D7D7D] size-[0.972vw]">
<ChevronLeftIcon />
</div>
</Button>
<p className="title-s font-medium">Смена проекта</p>
<Button
variant="secondary"
size="small"
onClick={(e) => {
e.preventDefault();
setIsOpen(false);
}}
</div>
<span className="size-[1.389vw] text-[#7D7D7D]">
<ChevronDownIcon />
</span>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx(
"absolute left-0 w-full z-10",
position === "top"
? "bottom-[calc(100%+4px)]"
: "top-[calc(100%+4px)]"
)}
>
<div
className="w-full rounded-[0.833vw] p-[0.833vw] max-h-[11.389vw] bg-white overflow-y-auto [scrollbar-width:thin]"
style={{ boxShadow: "0px 4px 40px 0px #0000000D" }}
>
<span className="text-[#7D7D7D] size-[0.972vw]">
<CloseIcon />
</span>
</Button>
</div>
<div className="flex flex-col gap-[1.667vw] h-[calc(100%-5vw)] [scrollbar-width:thin]">
<div className="flex flex-col overflow-y-auto h-[calc(100%-5vw)] [scrollbar-width:thin]">
{projects.map((project) => (
<button
<div
key={project.id}
className="p-[0.833vw] flex items-center gap-[0.556vw] hover:bg-[#F6F6F6] rounded-[0.278vw]"
onClick={(e) => {
e.preventDefault();
setPointedProject(project);
e.stopPropagation();
setSelectedProject(project);
setIsOpen(false);
}}
className="flex justify-between items-center not-last:border-b py-[0.883vw] border-[#F6F6F6] cursor-pointer px-[0.833vw]"
>
<div className="flex items-center gap-[0.556vw]">
<img
src="/images/app_image.png"
className="size-[2.222vw] object-cover"
alt=""
/>
<div className="space-y-[0.278vw]">
<div className="flex items-center gap-[0.278vw]">
<p className="text-s font-medium">{project.name}</p>
{activeProject &&
project.name === activeProject.name && (
<span className="size-[0.972vw] text-[#7B60F3]">
<LightningIcon />
</span>
)}
</div>
<p className="caption-s text-[#7D7D7D] font-medium">
Доступно 128 квартир
</p>
</div>
<div className="size-[1.111vw] rounded-full text-[#7B60F3] flex items-center justify-center">
{selectedProject?.id === project.id && <CheckIcon />}
</div>
{pointedProject?.name === project.name ? (
<div className="size-[1.389vw] flex items-center justify-center rounded-full bg-[#7B60F3]">
<div className="size-[0.833vw] text-white">
<CheckIcon />
</div>
</div>
) : (
<div className="rounded-full bg-[#F6F6F6] ring ring-[#F0F0F0] size-[1.389vw]" />
)}
</button>
<img
src="/images/app_image.png"
className="size-[1.111vw]"
alt=""
/>
<div className="flex items-center gap-[0.278vw]">
<div className="text-s">{project.name}</div>
{activeProject && project.name === activeProject.name && (
<span className="size-[0.972vw] text-[#7B60F3]"></span>
)}
</div>
</div>
))}
</div>
<div className="flex flex-col gap-y-[0.556vw]">
<Button
variant="cta"
size="large"
onClick={() => {
setSelectedProject(pointedProject);
setIsOpen(false);
}}
>
Переключиться
</Button>
<Button
variant="primary"
size="large"
onClick={() => {
setIsOpen(false);
}}
>
Отменить
</Button>
</div>
</div>
</div>
)}
</>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
+7 -63
View File
@@ -14,6 +14,7 @@ import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
import { AnimatePresence, motion } from "motion/react";
import useClientSearch from "../../hooks/useClientSearch.tsx";
import ManagerSelect from "../ManagerSelect.tsx";
import { Manager } from "../../types/Manager.ts";
interface Props {
targetServerId: string | null;
@@ -36,6 +37,11 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
refetchInterval: 1000,
});
const { data: managers } = useQuery({
queryKey: ["managers"],
queryFn: () => api.get("managers").json<Manager[]>(),
});
const targetServer = targetServerId
? servers?.find((server) => server.id === targetServerId) || null
: null;
@@ -170,10 +176,6 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
selectedServer,
]);
useEffect(() => {
console.log(selectedServer);
}, [selectedServer]);
const ref = useRef<HTMLFormElement>(null);
return (
@@ -208,65 +210,6 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
required
isLoading={isLoading}
/>
<ManagerSelect
placeholder="Менеджер сеанса"
data={[
{
id: "1",
email: "2@2",
fullname: "СЕМЁН Лобанов",
companyId: "1",
},
{
id: "2",
email: "2@2",
fullname: "ВОВА Лобанов",
companyId: "1",
},
{
id: "3",
email: "2@2",
fullname: "САНЯ Лобанов",
companyId: "1",
},
{
id: "4",
email: "2@2",
fullname: "АНТОН Лобанов",
companyId: "1",
},
{
id: "5",
email: "2@2",
fullname: "БОЛЬШОЙ АНТОН",
companyId: "1",
},
{
id: "6",
email: "2@2",
fullname: "Константин Лобанов",
companyId: "1",
},
{
id: "7",
email: "2@2",
fullname: "Константин Лобанов",
companyId: "1",
},
{
id: "8",
email: "2@2",
fullname: "Константин Лобанов",
companyId: "1",
},
{
id: "9",
email: "2@2",
fullname: "Константин Лобанов",
companyId: "1",
},
]}
/>
<AnimatePresence>
{isFullPhone && (
<>
@@ -303,6 +246,7 @@ export default function CreateSessionModal({ targetServerId, client }: Props) {
</div>
<div className="flex flex-col gap-y-[0.833vw]">
<p className="title-s font-medium">Выберите параметры сеанса</p>
<ManagerSelect placeholder="Менеджер сеанса" data={managers || []} />
{selectedServer &&
selectedServer?.appsToServers &&
selectedServer.appsToServers?.length > 0 && (