feat: update ManagerSelect positioning and enhance ProjectSelector with dynamic positioning and animations
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user