Add react-qr-code dependency; enhance PopupHeader and SharePopup components with draggable functionality; update LinkShare component for improved UI; integrate SettingsModal in HomePage for better user experience.

This commit is contained in:
2025-10-09 15:31:39 +05:00
parent 8ca825475e
commit 79fb7f2748
11 changed files with 316 additions and 38 deletions
+12 -2
View File
@@ -1,3 +1,4 @@
import clsx from "clsx";
import usePopupStore from "../store/popupStore";
import XMarkIcon from "./icons/XMarkIcon";
import Button from "./ui/Button";
@@ -6,15 +7,24 @@ interface PopupHeaderProps {
title?: string;
leftButton?: React.ReactNode;
headerRef: React.RefObject<HTMLDivElement | null>;
draggable?: boolean;
}
function PopupHeader({ title, leftButton, headerRef }: PopupHeaderProps) {
function PopupHeader({
title,
leftButton,
headerRef,
draggable,
}: 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"
className={clsx(
"2xl:p-[1.111vw] p-4 flex justify-between items-center cursor-graba select-none relative",
draggable && "cursor-grab active:cursor-grabbing"
)}
>
<div className="2xl:size-[2.222vw] size-8">{leftButton}</div>
{title && (
+1
View File
@@ -83,6 +83,7 @@ function PopupWrapper({
headerRef={headerRef}
title={title}
leftButton={leftButton}
draggable={draggable}
/>
<div className="2xl:p-[1.389vw] p-5">{children}</div>
</div>
@@ -0,0 +1,75 @@
import { useState } from "react";
import SoundIcon from "../icons/SoundIcon";
import VideoFilledIcon from "../icons/VideoFilledIcon";
import ModalWrapper from "../ModalWrapper";
import Button from "../ui/Button";
import RangeInput from "../ui/RangeInput";
function SettingsModal() {
const [microphoneVolume, setMicrophoneVolume] = useState(50);
// const [speakerVolume, setSpeakerVolume] = useState(50);
return (
<ModalWrapper title="Настройки">
<div className="2xl:space-y-[1.389vw] space-y-5">
<div className="flex">
<Button variant="menu" size="large" className="w-full">
<div className="flex 2xl:gap-[0.556vw] items-center">
<div className="2xl:size-[1.111vw] size-4 text-[#7B60F3]">
<SoundIcon />
</div>
<p className="font-medium">Звук</p>
</div>
</Button>
<Button variant="menu" size="large" className="w-full">
<div className="flex 2xl:gap-[0.556vw] items-center">
<div className="2xl:size-[1.111vw] size-4 text-[#7B60F3]">
<VideoFilledIcon />
</div>
<p className="font-medium">Видео</p>
</div>
</Button>
</div>
<div className="2xl:space-y-[1.667vw] space-y-6">
<div className="2xl:space-y-[0.833vw] space-y-3">
<p className="title-s font-medium">Микрофон</p>
<div className="2xl:space-y-[1.111vw] space-y-4">
<div className="flex 2xl:gap-[0.556vw]">
<div className="bg-[#F3F3F3] flex-1"></div>
<Button variant="cta" size="large">
Проверить
</Button>
</div>
<div className="flex items-center 2xl:gap-[0.833vw]">
<div className="2xl:size-[1.111vw] size-4 text-[#7D7D7D]">
<SoundIcon />
</div>
<RangeInput
value={microphoneVolume}
onChange={setMicrophoneVolume}
/>
<p className="caption-xs font-medium text-[#7D7D7D] 2xl:w-[1.667vw] w-6">
{microphoneVolume.toFixed(0)}%
</p>
</div>
</div>
</div>
<hr className="bg-[#F6F6F6] 2xl:h-[0.069vw] h-px" />
<div className="2xl:space-y-[0.833vw] space-y-3">
<p className="title-s font-medium">Динамик</p>
<div className="2xl:space-y-[1.111vw] space-y-4">
<div className="flex 2xl:gap-[0.556vw]">
<div className="bg-[#F3F3F3] flex-1"></div>
<Button variant="cta" size="large">
Проверить
</Button>
</div>
</div>
</div>
</div>
</div>
</ModalWrapper>
);
}
export default SettingsModal;
@@ -0,0 +1,43 @@
import PopupWrapper from "../PopupWrapper";
import Button from "../ui/Button";
import ChevronLeftIcon from "../icons/ChevronLeftIcon";
import usePopupStore from "../../store/popupStore";
import SharePopup from "./SharePopup";
import QRCode from "react-qr-code";
interface QRCodePopupProps {
link: string;
}
function QRCodePopup({ link }: QRCodePopupProps) {
const { setPopup } = usePopupStore();
return (
<PopupWrapper
draggable
leftButton={
<Button
variant="secondary"
size="small"
onClick={() => setPopup(<SharePopup link={link} />)}
>
<div className="2xl:size-[1.111vw] size-4">
<ChevronLeftIcon />
</div>
</Button>
}
>
<div className="flex flex-col 2xl:gap-y-[1.667vw] gap-y-6 items-center">
<QRCode value={link} className="2xl:size-[10.556vw] size-[152px]" />
<div className="2xl:space-y-[0.556vw] space-y-2">
<p className="title-m font-medium">Подключайтесь к сеансу</p>
<p className="caption-s font-medium text-center text-[#CCCCCC]">
Можно даже с мобильным интернетом
</p>
</div>
</div>
</PopupWrapper>
);
}
export default QRCodePopup;
+11 -3
View File
@@ -3,14 +3,22 @@ import ShareFilledIcon from "../icons/ShareFilledIcon";
import PopupWrapper from "../PopupWrapper";
import Button from "../ui/Button";
import LinkShare from "../ui/LinkShare";
import usePopupStore from "../../store/popupStore";
import QRCodePopup from "./QRCodePopup";
function SharePopup({ link }: { link: string }) {
const { setPopup } = usePopupStore();
function SharePopup() {
return (
<PopupWrapper
title="Пригласить"
draggable
leftButton={
<Button variant="secondary" size="small">
<Button
variant="secondary"
size="small"
onClick={() => setPopup(<QRCodePopup link={link} />)}
>
<div className="2xl:size-[1.111vw] size-4">
<QRIcon />
</div>
@@ -19,7 +27,7 @@ function SharePopup() {
>
<div className="mb-[1.389vw]">
<p className="title-s mb-[0.833vw] font-medium">Скопировать ссылку</p>
<LinkShare link={"https://estate.stream/ahdy12jdco1"} />
<LinkShare link={link} />
</div>
<Button variant="primary" size="large" className="w-full">
Отправить
+29 -27
View File
@@ -18,40 +18,42 @@ 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 text-s hover:cursor-pointer overflow-hidden"
onClick={handleCopy}
>
{link}
</span>
{shareState === "default" && (
<Button
variant="cta"
size="medium"
className="translate-x-[0.556vw]"
<div className="flex flex-col gap-[0.556vw]">
<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 text-s hover:cursor-pointer overflow-hidden"
onClick={handleCopy}
>
Копировать
</Button>
)}
{link}
</span>
{shareState === "loading" && (
<div className="size-[1.389vw] text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
)}
{shareState === "default" && (
<Button
variant="cta"
size="medium"
className="translate-x-[0.556vw]"
onClick={handleCopy}
>
Копировать
</Button>
)}
{shareState === "done" && (
<>
{shareState === "loading" && (
<div className="size-[1.389vw] text-[#7B60F3] animate-spin">
<LoaderIcon />
</div>
)}
{shareState === "done" && (
<div className="size-[1.389vw] text-[#7B60F3]">
<CheckIcon />
</div>
<div className="caption-s absolute bottom-[-1.25vw] text-[#29AF61] left-0">
Ссылка скопирована
</div>
</>
)}
</div>
{shareState === "done" && (
<div className="caption-s absolutea bottom-[-1.25vw] text-[#29AF61] left-0">
Ссылка скопирована
</div>
)}
</div>
);
+84
View File
@@ -0,0 +1,84 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef, useState } from "react";
interface RangeInputProps {
max?: number;
min?: number;
value?: number;
onChange: (value: number) => void;
}
function RangeInput({
onChange,
value = 50,
max = 100,
min = 0,
}: RangeInputProps) {
const [mouseDown, setMouseDown] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
addEventListener("mouseup", () => setMouseDown(false));
addEventListener("mousemove", handleMouseMove);
return () => {
removeEventListener("mouseup", () => setMouseDown(false));
removeEventListener("mousemove", handleMouseMove);
};
}, [handleMouseMove]);
function handleMouseMove(e: MouseEvent) {
if (mouseDown && ref.current) {
onChange(
Math.min(
Math.max(
min,
((e.clientX - ref.current.getBoundingClientRect().left) /
ref.current.clientWidth) *
(max - min) +
min
),
max
)
);
}
}
function handleMouseDown(e: React.MouseEvent<HTMLDivElement>) {
e.preventDefault();
if (ref.current) {
onChange(
Math.min(
Math.max(
min,
((e.clientX - ref.current.getBoundingClientRect().left) /
ref.current.clientWidth) *
(max - min) +
min
),
max
)
);
}
setMouseDown(true);
}
return (
<div
ref={ref}
className="2xl:w-[21.111vw] w-[304px] 2xl:h-[0.139vw] h-[2px] relative bg-[#F0F0F0] 2xl:rounded-[0.556vw] rounded-lg cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown}
>
<div
className="bg-[#7B60F3] 2xl:rounded-[0.556vw] h-full absolute left-0 top-0 rounded-lg"
style={{ width: `${((value - min) / (max - min)) * 100}%` }}
/>
<div
className="2xl:size-[0.833vw] size-3 rounded-full bg-[#7B60F3] absolute top-1/2 -translate-y-1/2"
style={{ left: `${((value - min) / (max - min)) * 100}%` }}
/>
</div>
);
}
export default RangeInput;
+29
View File
@@ -0,0 +1,29 @@
import clsx from "clsx";
interface SwitchProps {
enabled: boolean;
onChange: (enabled: boolean) => void;
}
function Switch({ enabled, onChange }: SwitchProps) {
return (
<div
onClick={() => onChange(!enabled)}
className={clsx(
"2xl:rounded-[0.833vw] rounded-xl 2xl:w-[2.778vw] w-10 2xl:py-[0.139vw] py-[2px] cursor-pointer transition-colors",
enabled ? "bg-[#7B60F3]" : "bg-[#F0F0F0]"
)}
>
<div
className={clsx(
"rounded-full 2xl:size-[1.389vw] size-5 bg-white shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] transition-all",
enabled
? "2xl:translate-x-[1.25vw] translate-x-[18px]"
: "2xl:translate-x-[0.139vw] translate-x-[2px]"
)}
/>
</div>
);
}
export default Switch;
+18 -6
View File
@@ -1,7 +1,3 @@
<<<<<<< HEAD
import ChatPopup from "../components/popups/ChatPopup";
=======
>>>>>>> 8aef8a530bcdd53af4add911e773f2691e0027e4
import Button from "../components/ui/Button";
import FloatingActionButton from "../components/ui/FloatingActionButton";
import { useMe, useLogout } from "../hooks/useAuth";
@@ -9,6 +5,9 @@ import { useNavigate } from "react-router";
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
import SharePopup from "../components/popups/SharePopup";
import usePopupStore from "../store/popupStore";
import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
import CogFilledIcon from "../components/icons/CogFilledIcon";
function HomePage() {
const { data: user } = useMe();
@@ -21,6 +20,7 @@ function HomePage() {
};
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
return (
<div className="py-8 min-h-screen bg-gray-50">
@@ -32,14 +32,26 @@ function HomePage() {
<FloatingActionButton
variant="default"
// onClick={() => setModal(<ShareModal />)}
onClick={() => setPopup(<SharePopup />)}
onClick={() =>
setPopup(
<SharePopup link={"https://estate.stream/ahdy12jdco1"} />
)
}
>
<div className="2xl:size-[1.111vw] size-4 text-white">
<ShareFilledIcon />
</div>
</FloatingActionButton>
<FloatingActionButton
variant="default"
onClick={() => setModal(<SettingsModal />)}
>
<div className="2xl:size-[1.111vw] size-4 text-white">
<CogFilledIcon />
</div>
</FloatingActionButton>
<div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<h2 className="mb-2 text-xl font-semibold">