From 50e62eac132c6e56a79bce478b7712eee8e499a3 Mon Sep 17 00:00:00 2001 From: Lanskikh Date: Mon, 20 Oct 2025 17:06:54 +0500 Subject: [PATCH] Update API URL in .env; add responsive design adjustments in tailwind.config.js; enhance ModalWrapper and Popup components for improved mobile interactions; refactor audio handling in VoiceCheckModal and SoundCheckModal; implement drag functionality in PopupContainer; improve ChatPopup and SharePopup UI for better user experience. --- client/.env | 2 +- client/src/components/ModalWrapper.tsx | 7 +- client/src/components/PopupContainer.tsx | 33 +++- client/src/components/PopupHeader.tsx | 2 +- client/src/components/PopupWrapper.tsx | 7 +- client/src/components/SessionUsersPanel.tsx | 42 +++-- .../src/components/modals/SettingsModal.tsx | 6 +- .../src/components/modals/SoundCheckModal.tsx | 134 ++++++++++++-- .../src/components/modals/VoiceCheckModal.tsx | 166 ++++++++---------- client/src/components/popups/ChatPopup.tsx | 45 ++--- client/src/components/popups/SharePopup.tsx | 5 +- client/src/components/ui/LinkShare.tsx | 12 +- client/src/components/ui/Select.tsx | 16 +- client/src/pages/HomePage.tsx | 14 +- client/tailwind.config.js | 1 + 15 files changed, 314 insertions(+), 178 deletions(-) diff --git a/client/.env b/client/.env index f41d947..101d1fc 100644 --- a/client/.env +++ b/client/.env @@ -1,2 +1,2 @@ # VITE_API_URL=http://192.168.1.23:3000 -VITE_API_URL=http://localhost:3000 \ No newline at end of file +VITE_API_URL=http://192.168.1.224:3000 \ No newline at end of file diff --git a/client/src/components/ModalWrapper.tsx b/client/src/components/ModalWrapper.tsx index d6d1873..d17bae6 100644 --- a/client/src/components/ModalWrapper.tsx +++ b/client/src/components/ModalWrapper.tsx @@ -15,7 +15,12 @@ function ModalWrapper({ className, }: ModalWrapperProps) { return ( -
+
{children} diff --git a/client/src/components/PopupContainer.tsx b/client/src/components/PopupContainer.tsx index dd42e28..fe25491 100644 --- a/client/src/components/PopupContainer.tsx +++ b/client/src/components/PopupContainer.tsx @@ -2,17 +2,38 @@ import { AnimatePresence, motion } from "motion/react"; import usePopupStore from "../store/popupStore"; function PopupContainer() { - const { popup, position } = usePopupStore(); + const { popup, position, setPopup } = usePopupStore(); + + const isMobile = innerWidth < 640; + + function handleDragEnd( + _event: unknown, + info: { offset: { y: number }; velocity: { y: number } } + ) { + // Закрываем попап если свайпнули вниз больше чем на 100px или со скоростью > 500 + if (info.offset.y > 100 || info.velocity.y > 500) { + setPopup(null); + } + } return ( {popup && ( {popup} diff --git a/client/src/components/PopupHeader.tsx b/client/src/components/PopupHeader.tsx index 3888d38..62c5533 100644 --- a/client/src/components/PopupHeader.tsx +++ b/client/src/components/PopupHeader.tsx @@ -22,7 +22,7 @@ function PopupHeader({
diff --git a/client/src/components/PopupWrapper.tsx b/client/src/components/PopupWrapper.tsx index b4745b5..11c1df3 100644 --- a/client/src/components/PopupWrapper.tsx +++ b/client/src/components/PopupWrapper.tsx @@ -99,10 +99,15 @@ function PopupWrapper({
+ {/* Полоска-ручка для свайпа на мобильных */} +
+
+
+ { + const handleMove = (e: MouseEvent | TouchEvent) => { if (!containerRef.current) return; if (!isDragStarted.current) { const distance = Math.hypot( - e.clientX - dragStartPos.current.x, - e.clientY - dragStartPos.current.y + ("clientX" in e ? e.clientX : e.touches[0].clientX) - + dragStartPos.current.x, + ("clientY" in e ? e.clientY : e.touches[0].clientY) - + dragStartPos.current.y ); if (distance >= DRAG_THRESHOLD) { @@ -72,8 +74,12 @@ export default function SessionUsersPanel() { if (isDragStarted.current) { setDragPosition({ - x: e.clientX - dragOffset.current.x, - y: e.clientY - dragOffset.current.y, + x: + ("clientX" in e ? e.clientX : e.touches[0].clientX) - + dragOffset.current.x, + y: + ("clientY" in e ? e.clientY : e.touches[0].clientY) - + dragOffset.current.y, }); } }; @@ -94,30 +100,39 @@ export default function SessionUsersPanel() { setIsLeft(shouldBeLeft); isDragStarted.current = false; - window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleMouseUp); }; - const handleMouseDown = (e: React.MouseEvent) => { + const handleMouseDown = ( + e: React.MouseEvent | React.TouchEvent + ) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const r_pos = { x: rect.left, y: rect.top }; - const c_pos = { x: e.clientX, y: e.clientY }; + const c_pos = { + x: "clientX" in e ? e.clientX : e.touches[0].clientX, + y: "clientY" in e ? e.clientY : e.touches[0].clientY, + }; dragStartPos.current = c_pos; dragOffset.current = { x: c_pos.x - r_pos.x, y: c_pos.y - r_pos.y }; setDragPosition({ x: r_pos.x, y: r_pos.y }); isDragStarted.current = false; - window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mousemove", handleMove); + window.addEventListener("touchmove", handleMove); window.addEventListener("mouseup", handleMouseUp); + window.addEventListener("touchend", handleMouseUp); }; useEffect(() => { return () => { - window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mousemove", handleMove); window.removeEventListener("mouseup", handleMouseUp); + window.removeEventListener("touchmove", handleMove); + window.removeEventListener("touchend", handleMouseUp); }; }, []); @@ -135,7 +150,7 @@ export default function SessionUsersPanel() { transform: `translate(${isLeft ? "0" : "-100%"}, ${ isTop ? "0" : "-100%" })`, - transition: "all 0.3s ease-in-out", + transition: "all 0.5s cubic-bezier(.63,.08,.37,.89)", }; }; @@ -143,9 +158,8 @@ export default function SessionUsersPanel() {
{ setModal( { setModal( ); }; diff --git a/client/src/components/modals/SoundCheckModal.tsx b/client/src/components/modals/SoundCheckModal.tsx index a523e8d..422ec36 100644 --- a/client/src/components/modals/SoundCheckModal.tsx +++ b/client/src/components/modals/SoundCheckModal.tsx @@ -7,29 +7,42 @@ import useModalStore from "../../store/modalStore"; import Select from "../ui/Select"; interface SoundCheckModalProps { - selectedSpeaker: string; + initialSpeaker: string; speakers: { deviceId: string; label: string }[]; speakerVolume: number; onSelectSpeaker: (label: string) => void; } function SoundCheckModal({ - selectedSpeaker, + initialSpeaker, speakers, speakerVolume, onSelectSpeaker, }: SoundCheckModalProps) { const { setModal } = useModalStore(); + + const [selectedSpeaker, setSelectedSpeaker] = useState(initialSpeaker); const audioContextRef = useRef(null); const animationFrameRef = useRef(null); + const audioElementRef = useRef(null); + const mediaStreamDestinationRef = + useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [playProgress, setPlayProgress] = useState(0); // Прогресс воспроизведения 0-1 const playStartTimeRef = useRef(0); - // Высоты баров эквалайзера (16 баров как в дизайне) - const barCount = 16; + // Обработчик выбора динамика + const handleSelectSpeaker = (label: string) => { + setSelectedSpeaker(label); + onSelectSpeaker(label); + }; + + // Высоты баров эквалайзера (40 баров с несколькими волнами как на макете) + const barCount = 40; const barHeights = [ - 3, 12, 16, 20, 44, 28, 34, 60, 8, 40, 18, 36, 24, 13, 10, 6, + 3, 12, 16, 20, 44, 28, 12, 12, 16, 20, 34, 60, 28, 12, 8, 16, 12, 20, 40, + 28, 12, 12, 16, 12, 20, 18, 36, 24, 16, 3, 8, 12, 20, 44, 28, 16, 13, 10, 6, + 3, ]; // Обновление прогресса воспроизведения @@ -61,6 +74,14 @@ function SoundCheckModal({ if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } + if (audioElementRef.current) { + audioElementRef.current.pause(); + audioElementRef.current.srcObject = null; + audioElementRef.current = null; + } + if (mediaStreamDestinationRef.current) { + mediaStreamDestinationRef.current = null; + } if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; @@ -68,30 +89,99 @@ function SoundCheckModal({ }; }, []); - const playTestSound = () => { + const playTestSound = async () => { // Создаём AudioContext если его нет if (!audioContextRef.current) { audioContextRef.current = new AudioContext(); } const audioContext = audioContextRef.current; + + // Создаём MediaStreamDestination для вывода на выбранное устройство + if (!mediaStreamDestinationRef.current) { + mediaStreamDestinationRef.current = + audioContext.createMediaStreamDestination(); + } + + const destination = mediaStreamDestinationRef.current; + + // Создаём аудио элемент для вывода на конкретное устройство + if (!audioElementRef.current) { + audioElementRef.current = new Audio(); + audioElementRef.current.srcObject = destination.stream; + } + + // Устанавливаем выбранное устройство вывода + const selectedDevice = speakers.find((s) => s.label === selectedSpeaker); + if ( + selectedDevice && + audioElementRef.current && + "setSinkId" in audioElementRef.current + ) { + try { + await ( + audioElementRef.current as HTMLAudioElement & { + setSinkId: (sinkId: string) => Promise; + } + ).setSinkId(selectedDevice.deviceId); + } catch (error) { + console.error("Ошибка при установке устройства вывода:", error); + } + } + + // Запускаем воспроизведение через аудио элемент + audioElementRef.current + .play() + .catch((e) => console.error("Ошибка воспроизведения:", e)); + const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); - // Подключаем напрямую + // Подключаем к MediaStreamDestination вместо audioContext.destination oscillator.connect(gainNode); - gainNode.connect(audioContext.destination); + gainNode.connect(destination); - // Настраиваем звук (440 Hz - нота A) - oscillator.frequency.value = 440; oscillator.type = "sine"; - // Применяем громкость из настроек и плавное затухание + // Применяем громкость из настроек const baseVolume = speakerVolume / 100; gainNode.gain.setValueAtTime(baseVolume * 0.3, audioContext.currentTime); + + // Создаём динамическое изменение частоты на основе высот баров + const duration = 3; // 3 секунды + const timePerBar = duration / barCount; + + // Начальная частота + const minFreq = 200; // Минимальная частота (низкая нота) + const maxFreq = 1200; // Максимальная частота (высокая нота) + const minHeight = 3; + const maxHeight = 60; + + // Устанавливаем начальную частоту + const initialFreq = + minFreq + + ((barHeights[0] - minHeight) / (maxHeight - minHeight)) * + (maxFreq - minFreq); + oscillator.frequency.setValueAtTime(initialFreq, audioContext.currentTime); + + // Создаём плавное изменение частоты для каждого бара + barHeights.forEach((height, index) => { + const time = audioContext.currentTime + index * timePerBar; + // Преобразуем высоту бара в частоту + const frequency = + minFreq + + ((height - minHeight) / (maxHeight - minHeight)) * (maxFreq - minFreq); + oscillator.frequency.linearRampToValueAtTime(frequency, time); + }); + + // Плавное затухание громкости к концу + gainNode.gain.linearRampToValueAtTime( + baseVolume * 0.3, + audioContext.currentTime + duration * 0.8 + ); gainNode.gain.exponentialRampToValueAtTime( 0.01, - audioContext.currentTime + 3 + audioContext.currentTime + duration ); // Запускаем анимацию прогресса @@ -101,13 +191,13 @@ function SoundCheckModal({ // Играем звук 3 секунды oscillator.start(audioContext.currentTime); - oscillator.stop(audioContext.currentTime + 3); + oscillator.stop(audioContext.currentTime + duration); // Останавливаем анимацию после окончания звука setTimeout(() => { setIsPlaying(false); setPlayProgress(0); - }, 3000); + }, duration * 1000); }; return ( @@ -116,9 +206,10 @@ function SoundCheckModal({

Динамик

m.label)} defaultOption={selectedMicrophone} - onSelect={onSelectMicrophone} + onSelect={handleSelectMicrophone} />
@@ -304,7 +298,7 @@ function VoiceCheckModal({ key={index} className={clsx( "2xl:w-[0.208vw] w-[3px] 2xl:rounded-[0.556vw] rounded-lg transition-all duration-150 ease-out", - soundDetected ? "bg-[#7B60F3]" : "bg-[#D6D6D6]" + currentVolume > 10 ? "bg-[#7B60F3]" : "bg-[#D6D6D6]" )} style={{ height: `${Math.max(3, height)}px`, @@ -314,17 +308,22 @@ function VoiceCheckModal({
{/* Статус */} - {!isTestRunning && status === "success" && ( + {isActive && detectionStatus === "waiting" && ( +

+ Говорите... +

+ )} + {isActive && detectionStatus === "detected" && (

Звук есть!

)} - {!isTestRunning && status === "error" && ( + {isActive && detectionStatus === "not_detected" && (

Звук не обнаружен

)} - {isTestRunning && ( -

- Проверка... + {!isActive && ( +

+ Ошибка подключения

)}
@@ -337,29 +336,14 @@ function VoiceCheckModal({

-
- - - -
+
); diff --git a/client/src/components/popups/ChatPopup.tsx b/client/src/components/popups/ChatPopup.tsx index d967558..cc0ba1a 100644 --- a/client/src/components/popups/ChatPopup.tsx +++ b/client/src/components/popups/ChatPopup.tsx @@ -32,12 +32,8 @@ export default function ChatPopup() { } return ( - -
+ +
@@ -55,28 +51,21 @@ function MessageFeed({ messages }: { messages: MessageItemProps[] }) { return (
- {messages.length === 0 ? ( -
+
Здесь пока нет сообщений.
Можно начать беседу с приветствия
) : ( -
+
{messages.map((message, index) => ( ))} @@ -95,35 +84,34 @@ interface MessageItemProps { function MessageItem({ senderId, timestamp, content }: MessageItemProps) { const { data: user } = useMe(); + const isFromMe = senderId === "1"; return (
-
+
{!isFromMe && ( -
- {user?.fullName} -
+
{user?.fullName}
)}
{content}
{timestamp}
-
+
textareaRef.current?.focus()} - className="flex w-full min-h-[4.444vw] p-[1.111vw] items-end justify-between absolute bottom-0 left-0 bg-white" + className="flex w-full 2xl:min-h-[4.444vw] min-h-16 2xl:p-[1.111vw] p-4 items-end justify-between absolute bottom-0 left-0 bg-white" >