This commit is contained in:
2025-10-09 15:41:01 +05:00
11 changed files with 322 additions and 32 deletions
+13
View File
@@ -9,6 +9,7 @@
"ky": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-qr-code": "^2.0.18",
"react-router": "^7.9.3",
"zustand": "^5.0.8",
},
@@ -404,6 +405,8 @@
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -426,6 +429,8 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@@ -496,14 +501,22 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-qr-code": ["react-qr-code@2.0.18", "", { "dependencies": { "prop-types": "^15.8.1", "qr.js": "0.0.0" }, "peerDependencies": { "react": "*" } }, "sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg=="],
"react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="],
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
+1
View File
@@ -15,6 +15,7 @@
"ky": "^1.11.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-qr-code": "^2.0.18",
"react-router": "^7.9.3",
"zustand": "^5.0.8"
},
+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;
+24
View File
@@ -4,7 +4,13 @@ import { useMe, useLogout } from "../hooks/useAuth";
import { useNavigate } from "react-router";
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
import usePopupStore from "../store/popupStore";
<<<<<<< HEAD
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
=======
import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
import CogFilledIcon from "../components/icons/CogFilledIcon";
>>>>>>> 79fb7f2748fcb76e887daa397ff450afc389a2b3
function HomePage() {
const { data: user } = useMe();
@@ -17,6 +23,7 @@ function HomePage() {
};
const { setPopup } = usePopupStore();
const { setModal } = useModalStore();
return (
<div className="py-8 min-h-screen bg-gray-50">
@@ -28,13 +35,30 @@ function HomePage() {
<FloatingActionButton
variant="default"
<<<<<<< HEAD
onClick={() => setPopup(<ParticipantsPopup />)}
=======
onClick={() =>
setPopup(
<SharePopup link={"https://estate.stream/ahdy12jdco1"} />
)
}
>>>>>>> 79fb7f2748fcb76e887daa397ff450afc389a2b3
>
<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">