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:
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
Отправить
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user