diff --git a/client/bun.lock b/client/bun.lock index 35b3d0d..e30e68a 100644 --- a/client/bun.lock +++ b/client/bun.lock @@ -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=="], diff --git a/client/package.json b/client/package.json index a8c548d..22be199 100644 --- a/client/package.json +++ b/client/package.json @@ -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" }, diff --git a/client/src/components/PopupHeader.tsx b/client/src/components/PopupHeader.tsx index b2ee21a..3888d38 100644 --- a/client/src/components/PopupHeader.tsx +++ b/client/src/components/PopupHeader.tsx @@ -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; + draggable?: boolean; } -function PopupHeader({ title, leftButton, headerRef }: PopupHeaderProps) { +function PopupHeader({ + title, + leftButton, + headerRef, + draggable, +}: PopupHeaderProps) { const { setPopup } = usePopupStore(); return (
{leftButton}
{title && ( diff --git a/client/src/components/PopupWrapper.tsx b/client/src/components/PopupWrapper.tsx index e2e633a..2d3788a 100644 --- a/client/src/components/PopupWrapper.tsx +++ b/client/src/components/PopupWrapper.tsx @@ -83,6 +83,7 @@ function PopupWrapper({ headerRef={headerRef} title={title} leftButton={leftButton} + draggable={draggable} />
{children}
diff --git a/client/src/components/modals/SettingsModal.tsx b/client/src/components/modals/SettingsModal.tsx new file mode 100644 index 0000000..efec99f --- /dev/null +++ b/client/src/components/modals/SettingsModal.tsx @@ -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 ( + +
+
+ + +
+
+
+

Микрофон

+
+
+
+ +
+
+
+ +
+ +

+ {microphoneVolume.toFixed(0)}% +

+
+
+
+
+
+

Динамик

+
+
+
+ +
+
+
+
+
+
+ ); +} + +export default SettingsModal; diff --git a/client/src/components/popups/QRCodePopup.tsx b/client/src/components/popups/QRCodePopup.tsx new file mode 100644 index 0000000..39dfb8d --- /dev/null +++ b/client/src/components/popups/QRCodePopup.tsx @@ -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 ( + setPopup()} + > +
+ +
+ + } + > +
+ +
+

Подключайтесь к сеансу

+

+ Можно даже с мобильным интернетом +

+
+
+
+ ); +} + +export default QRCodePopup; diff --git a/client/src/components/popups/SharePopup.tsx b/client/src/components/popups/SharePopup.tsx index b7bafa2..de971e6 100644 --- a/client/src/components/popups/SharePopup.tsx +++ b/client/src/components/popups/SharePopup.tsx @@ -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 ( + - )} + {link} + - {shareState === "loading" && ( -
- -
- )} + {shareState === "default" && ( + + )} - {shareState === "done" && ( - <> + {shareState === "loading" && ( +
+ +
+ )} + + {shareState === "done" && (
-
- Ссылка скопирована -
- + )} + + {shareState === "done" && ( +
+ Ссылка скопирована +
)} ); diff --git a/client/src/components/ui/RangeInput.tsx b/client/src/components/ui/RangeInput.tsx new file mode 100644 index 0000000..d722a83 --- /dev/null +++ b/client/src/components/ui/RangeInput.tsx @@ -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(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) { + 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 ( +
+
+
+
+ ); +} + +export default RangeInput; diff --git a/client/src/components/ui/Switch.tsx b/client/src/components/ui/Switch.tsx new file mode 100644 index 0000000..e424e3c --- /dev/null +++ b/client/src/components/ui/Switch.tsx @@ -0,0 +1,29 @@ +import clsx from "clsx"; + +interface SwitchProps { + enabled: boolean; + onChange: (enabled: boolean) => void; +} + +function Switch({ enabled, onChange }: SwitchProps) { + return ( +
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]" + )} + > +
+
+ ); +} + +export default Switch; diff --git a/client/src/pages/HomePage.tsx b/client/src/pages/HomePage.tsx index 57bb2b9..1a96ee9 100644 --- a/client/src/pages/HomePage.tsx +++ b/client/src/pages/HomePage.tsx @@ -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 (
@@ -32,14 +32,26 @@ function HomePage() { setModal()} - onClick={() => setPopup()} + onClick={() => + setPopup( + + ) + } >
+ setModal()} + > +
+ +
+
+