Enhance modal functionality with ModalContainer component; update ShareModal to use ModalWrapper; improve responsive text sizes in index.css; integrate FloatingActionButton in HomePage for popup sharing.

This commit is contained in:
2025-10-09 12:32:53 +05:00
parent 0b8edce9d6
commit 8aef8a530b
14 changed files with 357 additions and 26 deletions
+59 -1
View File
@@ -1,5 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef } from "react";
import useModalStore from "../store/modalStore";
import clsx from "clsx";
function ModalContainer() {
return <div></div>;
const { modal, setModal, position } = useModalStore();
const divRef = useRef<HTMLDivElement>(null);
const backdropRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
function handleResize() {
if (!modalRef.current) return;
if (divRef.current!.clientHeight > modalRef.current!.clientHeight) {
backdropRef.current!.style.height = `${divRef.current!.clientHeight}px`;
} else {
backdropRef.current!.style.height = `100%`;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key !== "Escape") return;
setModal(null);
}
useEffect(() => {
window.addEventListener("resize", handleResize);
window.addEventListener("keydown", handleKeydown);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("keydown", handleKeydown);
};
}, []);
return (
modal && (
<div className="h-full">
<div
ref={modalRef}
className={clsx(
"bg-black/70 max-md:top-14 flex overflow-y-auto fixed inset-0 z-10 items-center",
position === "center" ? "justify-center" : "justify-end"
)}
>
<div className="max-h-full">
<div ref={divRef} className="2xl:p-[1.111vw]">
<div
ref={backdropRef}
className="absolute inset-0 cursor-pointer"
onClick={() => setModal(null)}
/>
{modal}
</div>
</div>
</div>
</div>
)
);
}
export default ModalContainer;
+28
View File
@@ -0,0 +1,28 @@
import Button from "./ui/Button";
import useModalStore from "../store/modalStore";
import XMarkIcon from "./icons/XMarkIcon";
interface ModalHeaderProps {
title?: string;
leftButton?: React.ReactNode;
}
function ModalHeader({ title, leftButton }: ModalHeaderProps) {
const { setModal } = useModalStore();
return (
<div className="2xl:p-[1.111vw] p-4 flex justify-between items-center">
<div className="2xl:size-[2.222vw] size-8">{leftButton}</div>
{title && (
<p className="title-s flex-1 font-medium text-center">{title}</p>
)}
<Button variant="secondary" size="small" onClick={() => setModal(null)}>
<div className="2xl:size-[1.111vw] size-4">
<XMarkIcon />
</div>
</Button>
</div>
);
}
export default ModalHeader;
+25
View File
@@ -0,0 +1,25 @@
import ModalHeader from "./ModalHeader";
import clsx from "clsx";
interface ModalWrapperProps {
children: React.ReactNode;
title?: string;
leftButton?: React.ReactNode;
className?: string;
}
function ModalWrapper({
children,
title,
leftButton,
className,
}: ModalWrapperProps) {
return (
<div className={clsx("bg-white rounded-[1.111vw] relative", className)}>
<ModalHeader title={title} leftButton={leftButton} />
<div className="2xl:p-[1.389vw] p-5">{children}</div>
</div>
);
}
export default ModalWrapper;
+13
View File
@@ -0,0 +1,13 @@
import usePopupStore from "../store/popupStore";
function PopupContainer() {
const { popup, position } = usePopupStore();
return (
<div className="absolute" style={{ top: position.y, left: position.x }}>
{popup}
</div>
);
}
export default PopupContainer;
+33
View File
@@ -0,0 +1,33 @@
import usePopupStore from "../store/popupStore";
import XMarkIcon from "./icons/XMarkIcon";
import Button from "./ui/Button";
interface PopupHeaderProps {
title?: string;
leftButton?: React.ReactNode;
headerRef: React.RefObject<HTMLDivElement | null>;
}
function PopupHeader({ title, leftButton, headerRef }: PopupHeaderProps) {
const { setPopup } = usePopupStore();
return (
<div
ref={headerRef}
className="2xl:p-[1.111vw] p-4 flex justify-between items-center cursor-grab select-none relative"
>
<div className="2xl:size-[2.222vw] size-8">{leftButton}</div>
{title && (
<p className="title-s flex-1 font-medium text-center">{title}</p>
)}
<Button variant="secondary" size="small" onClick={() => setPopup(null)}>
<div className="2xl:size-[1.111vw] size-4">
<XMarkIcon />
</div>
</Button>
<hr className="bg-[#F2F2F2] 2xl:h-[0.069vw] h-px absolute bottom-0 w-[calc(100%-2.778vw)] left-[1.389vw]" />
</div>
);
}
export default PopupHeader;
+92
View File
@@ -0,0 +1,92 @@
/* eslint-disable react-hooks/exhaustive-deps */
import clsx from "clsx";
import PopupHeader from "./PopupHeader";
import { useEffect, useState } from "react";
import { useRef } from "react";
import usePopupStore from "../store/popupStore";
interface PopupWrapperProps {
children: React.ReactNode;
className?: string;
title?: string;
leftButton?: React.ReactNode;
draggable?: boolean;
}
function PopupWrapper({
children,
className,
title,
leftButton,
draggable,
}: PopupWrapperProps) {
const { position, setPosition } = usePopupStore();
const [mouseDown, setMouseDown] = useState(false);
const [mouseDownPosition, setMouseDownPosition] = useState(position);
const wrapperRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
addEventListener("mouseup", () => setMouseDown(false));
return () => removeEventListener("mouseup", () => setMouseDown(false));
}, []);
function handleMouseMove(e: MouseEvent) {
if (draggable && mouseDown && wrapperRef.current) {
e.preventDefault();
setPosition({
x: Math.min(
Math.max(0, position.x + e.clientX - mouseDownPosition.x),
window.innerWidth - wrapperRef.current.clientWidth
),
y: Math.min(
Math.max(0, position.y + e.clientY - mouseDownPosition.y),
window.innerHeight - wrapperRef.current.clientHeight
),
});
setMouseDownPosition({ x: e.clientX, y: e.clientY });
}
}
useEffect(() => {
addEventListener("mousemove", handleMouseMove);
return () => removeEventListener("mousemove", handleMouseMove);
}, [handleMouseMove]);
useEffect(() => {
if (headerRef.current) {
headerRef.current.addEventListener("mousedown", (e) => {
setMouseDown(true);
setMouseDownPosition({ x: e.clientX, y: e.clientY });
});
}
return () => {
if (headerRef.current) {
headerRef.current.removeEventListener("mousedown", (e) => {
setMouseDown(true);
setMouseDownPosition({ x: e.clientX, y: e.clientY });
});
}
};
}, []);
return (
<div
ref={wrapperRef}
className={clsx(
"2xl:rounded-[2.222vw] rounded-[32px] relative bg-white shadow-[0_4px_40px_0_rgba(15,16,17,0.1)]",
className
)}
>
<PopupHeader
headerRef={headerRef}
title={title}
leftButton={leftButton}
/>
<div className="2xl:p-[1.389vw] p-5">{children}</div>
</div>
);
}
export default PopupWrapper;
+6 -9
View File
@@ -1,24 +1,21 @@
import Button from "../ui/Button";
import LinkShare from "../ui/LinkShare";
import ShareFilledIcon from "../icons/ShareFilledIcon";
import ModalWrapper from "../ModalWrapper";
export default function ShareModal() {
return (
<>
<ModalWrapper title="Пригласить">
<div className="mb-[1.389vw]">
<div className="title-s mb-[0.833vw]">Скопировать ссылку</div>
<p className="title-s mb-[0.833vw] font-medium">Скопировать ссылку</p>
<LinkShare link={"https://estate.stream/ahdy12jdco1"} />
</div>
<Button
variant="primary"
size="large"
className="w-full flex items-center justify-center gap-[0.556vw]"
>
<Button variant="primary" size="large" className="w-full">
Отправить
<div className="size-[1.111vw]">
<div className="2xl:size-[1.111vw] size-4">
<ShareFilledIcon />
</div>
</Button>
</>
</ModalWrapper>
);
}
@@ -0,0 +1,34 @@
import QRIcon from "../icons/QRIcon";
import ShareFilledIcon from "../icons/ShareFilledIcon";
import PopupWrapper from "../PopupWrapper";
import Button from "../ui/Button";
import LinkShare from "../ui/LinkShare";
function SharePopup() {
return (
<PopupWrapper
title="Пригласить"
draggable
leftButton={
<Button variant="secondary" size="small">
<div className="2xl:size-[1.111vw] size-4">
<QRIcon />
</div>
</Button>
}
>
<div className="mb-[1.389vw]">
<p className="title-s mb-[0.833vw] font-medium">Скопировать ссылку</p>
<LinkShare link={"https://estate.stream/ahdy12jdco1"} />
</div>
<Button variant="primary" size="large" className="w-full">
Отправить
<div className="2xl:size-[1.111vw] size-4">
<ShareFilledIcon />
</div>
</Button>
</PopupWrapper>
);
}
export default SharePopup;
+2 -2
View File
@@ -20,7 +20,7 @@ export default function LinkShare({ link }: { link: string }) {
return (
<div className="w-full h-[3.75vw] bg-[#F3F3F3] flex items-center justify-between gap-[0.833vw] px-[1.111vw] rounded-[1.111vw] relative">
<span
className="text-ellipsis overflow-hidden text-s hover:cursor-pointer"
className="text-ellipsis text-s hover:cursor-pointer overflow-hidden"
onClick={handleCopy}
>
{link}
@@ -30,7 +30,7 @@ export default function LinkShare({ link }: { link: string }) {
<Button
variant="cta"
size="medium"
className="h-[2.778vw] translate-x-[0.556vw]"
className="translate-x-[0.556vw]"
onClick={handleCopy}
>
Копировать
+10 -10
View File
@@ -12,23 +12,23 @@ body {
@layer utilities {
.title-l {
@apply text-2xl leading-[120%] tracking-[-0.02em];
@apply 2xl:text-[1.667vw] text-2xl leading-[120%] tracking-[-0.02em];
}
.title-m {
@apply text-xl leading-[110%] tracking-[-0.02em];
@apply 2xl:text-[1.389vw] text-xl leading-[110%] tracking-[-0.02em];
}
.title-s {
@apply text-base leading-[110%] tracking-[-0.02em];
@apply 2xl:text-[1.111vw] text-base leading-[110%] tracking-[-0.02em];
}
.text-m {
@apply text-base leading-[125%] tracking-[-0.02em];
@apply 2xl:text-[1.111vw] text-base leading-[125%] tracking-[-0.02em];
}
.text-s {
@apply text-sm leading-[115%] tracking-[-0.02em];
@apply 2xl:text-[0.972vw] text-sm leading-[115%] tracking-[-0.02em];
}
/* .text-xs {
@@ -36,22 +36,22 @@ body {
} */
.button-m {
@apply text-sm leading-[115%] tracking-[-0.02em];
@apply 2xl:text-[0.972vw] text-sm leading-[115%] tracking-[-0.02em];
}
.button-s {
@apply text-xs leading-[130%] tracking-[-0.02em];
@apply 2xl:text-[0.833vw] text-xs leading-[130%] tracking-[-0.02em];
}
.caption-m {
@apply text-sm leading-[120%] tracking-[-0.02em];
@apply 2xl:text-[0.972vw] text-sm leading-[120%] tracking-[-0.02em];
}
.caption-s {
@apply text-xs leading-[120%] tracking-[-0.02em];
@apply 2xl:text-[0.833vw] text-xs leading-[120%] tracking-[-0.02em];
}
.caption-xs {
@apply text-[10px] leading-[110%] tracking-[-0.02em];
@apply 2xl:text-[0.729vw] text-[10px] leading-[110%] tracking-[-0.02em];
}
}
+4
View File
@@ -9,6 +9,8 @@ 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";
const router = createBrowserRouter([
{
@@ -48,5 +50,7 @@ const router = createBrowserRouter([
createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ModalContainer />
<PopupContainer />
</QueryClientProvider>
);
+15 -4
View File
@@ -1,7 +1,10 @@
import ShareModal from "../components/modals/ShareModal";
import Button from "../components/ui/Button";
import FloatingActionButton from "../components/ui/FloatingActionButton";
import { useMe, useLogout } from "../hooks/useAuth";
import { useNavigate } from "react-router";
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
import SharePopup from "../components/popups/SharePopup";
import usePopupStore from "../store/popupStore";
function HomePage() {
const { data: user } = useMe();
@@ -13,6 +16,8 @@ function HomePage() {
navigate("/login");
};
const { setPopup } = usePopupStore();
return (
<div className="py-8 min-h-screen bg-gray-50">
<div className="px-4 mx-auto max-w-4xl">
@@ -21,9 +26,15 @@ function HomePage() {
{/* Потестить модалки */}
{/* <div className="w-[21.667vw] outline-1 outline">
<ShareModal />
</div> */}
<FloatingActionButton
variant="default"
// onClick={() => setModal(<ShareModal />)}
onClick={() => setPopup(<SharePopup />)}
>
<div className="2xl:size-[1.111vw] size-4 text-white">
<ShareFilledIcon />
</div>
</FloatingActionButton>
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
+18
View File
@@ -0,0 +1,18 @@
import { create } from "zustand";
import type { ReactNode } from "react";
interface ModalState {
modal: ReactNode | null;
setModal: (modal: ReactNode | null) => void;
position: "center" | "right";
setPosition: (position: "center" | "right") => void;
}
const useModalStore = create<ModalState>()((set) => ({
modal: null,
position: "center",
setPosition: (position) => set({ position }),
setModal: (modal) => set({ modal }),
}));
export default useModalStore;
+18
View File
@@ -0,0 +1,18 @@
import { create } from "zustand";
import type { ReactNode } from "react";
interface PopupState {
popup: ReactNode | null;
setPopup: (popup: ReactNode | null) => void;
position: { x: number; y: number };
setPosition: (position: { x: number; y: number }) => void;
}
const usePopupStore = create<PopupState>()((set) => ({
popup: null,
position: { x: 0, y: 0 },
setPosition: (position) => set({ position }),
setPopup: (popup) => set({ popup }),
}));
export default usePopupStore;