-
-
+
+
Загрузка
... {countdownTimer} сек
diff --git a/src/CalendarPage.tsx b/src/CalendarPage.tsx
index 5fc0a2b..951621a 100644
--- a/src/CalendarPage.tsx
+++ b/src/CalendarPage.tsx
@@ -46,8 +46,6 @@ function CalendarPage() {
)
.json();
- console.log(result);
-
setScheduledSessions(result);
}
@@ -117,8 +115,6 @@ function CalendarPage() {
const { datesAndTimes } = result;
- console.log(datesAndTimes);
-
setDatesAndTimes(datesAndTimes);
} catch (error) {
if (error instanceof Error) {
diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx
index 16f81a0..f4db315 100644
--- a/src/ErrorBoundary.tsx
+++ b/src/ErrorBoundary.tsx
@@ -6,12 +6,24 @@ function ErrorBoundary() {
console.error(error);
return (
-
-
-
{error.status}
-
{error.statusText}
+
+
+
+ {error.status}
+
+
+ {error.statusText}
+
+
-
+
);
}
diff --git a/src/ScheduledPage.tsx b/src/ScheduledPage.tsx
index beb6f5a..1937467 100644
--- a/src/ScheduledPage.tsx
+++ b/src/ScheduledPage.tsx
@@ -4,16 +4,14 @@ import { differenceInMilliseconds, isAfter, parseISO } from "date-fns";
import ky from "ky";
import { useEffect, useState } from "react";
import Countdown from "react-countdown";
-import { useNavigate, useParams } from "react-router-dom";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
function ScheduledPage() {
const params = useParams();
- // const [searchParams] = useSearchParams();
+ const { search } = useLocation();
const navigate = useNavigate();
const [countdownSeconds, setCountdownSeconds] = useState
();
- console.log(params.sessionId);
-
async function connect() {
try {
const result: any = await ky
@@ -34,7 +32,7 @@ function ScheduledPage() {
return;
}
- navigate(`/stream/${result.activeSessionId}`);
+ navigate(`/stream/${result.activeSessionId}${search}`);
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
diff --git a/src/StreamPage.tsx b/src/StreamPage.tsx
index 11e122a..560490a 100644
--- a/src/StreamPage.tsx
+++ b/src/StreamPage.tsx
@@ -35,7 +35,6 @@ import { differenceInMilliseconds, format, parseISO } from "date-fns";
import HandOnIcon from "./components/icons/HandOnIcon";
import { useMobileOrientation } from "react-device-detect";
import RotateDeviceIcon from "./components/icons/RotateDeviceIcon";
-import LoaderIcon from "./components/icons/LoaderIcon";
function StreamPage() {
const { t } = useTranslation();
@@ -110,14 +109,12 @@ function StreamPage() {
if (!socket) return;
socket.emit("update", socketId, data);
- console.log("Users: ", users);
}
function kick(socketId: string) {
if (!socket) return;
socket.emit("kick", socketId);
- console.log("User kick: ", socketId);
}
function toastWarn(text: string) {
@@ -167,8 +164,6 @@ function StreamPage() {
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
.json();
- console.log(activeSession);
-
if (!activeSession) {
setIsStreamEnded(true);
return;
@@ -192,18 +187,15 @@ function StreamPage() {
});
socket.on("connect", () => {
- console.log("Socket: ", socket.id);
setSocket(socket);
});
- socket.on("join", (socketId, sockets) => {
- console.log("User connected: ", socketId, sockets);
+ socket.on("join", (_socketId, sockets) => {
setUsers(sockets);
toastHandOn(t("notification.newMember"));
});
- socket.on("update", (socketId, data, sockets) => {
- console.log("Update users: ", socketId, data, sockets);
+ socket.on("update", (_socketId, _data, sockets) => {
setUsers(sockets);
});
@@ -272,8 +264,14 @@ function StreamPage() {
>
) : (
-
-
Ожидание потока
+
+
+ Ожидание потока
+
)
) : (
diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx
index 954f6c7..1188b11 100644
--- a/src/components/Calendar.tsx
+++ b/src/components/Calendar.tsx
@@ -2,6 +2,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
add,
+ addDays,
eachDayOfInterval,
endOfMonth,
format,
@@ -9,7 +10,6 @@ import {
isEqual,
isWithinInterval,
parse,
- parseISO,
startOfToday,
} from "date-fns";
import { enUS, ru } from "date-fns/locale";
@@ -99,8 +99,8 @@ function Calendar({ schedules, handleSelect, className }: CalendarProps) {
schedules.some(
(schedule) =>
!isWithinInterval(day, {
- start: parseISO(schedule.startDate),
- end: parseISO(schedule.endDate),
+ start: new Date(schedule.startDate),
+ end: addDays(new Date(schedule.startDate), 14),
})
)
}
diff --git a/src/components/ChatNew.tsx b/src/components/ChatNew.tsx
index 0d782fd..2f33857 100644
--- a/src/components/ChatNew.tsx
+++ b/src/components/ChatNew.tsx
@@ -181,10 +181,11 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
- {messages.map((message) => (
+ {messages.map((message, index) => (
{message.user.id !== userId && (
@@ -217,10 +220,25 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
{message.time}
- {message.user.id !== userId && (
+ {message.user.id !== userId ? (
+ ) : (
+
)}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index d93c331..e4446a6 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,13 +1,13 @@
import LogoIcon from "./icons/LogoIcon";
import LogoMobileIcon from "./icons/LogoMobileIcon";
-import i18n from "../i18n";
+// import i18n from "../i18n";
// import useSidebarStore from "../stores/useSidebarStore";
-interface HeaderProps {
- handleChangeLang: (lang: string) => void;
-}
+// interface HeaderProps {
+// handleChangeLang: (lang: string) => void;
+// }
-function Header({ handleChangeLang }: HeaderProps) {
+function Header() {
// const [setIsOpen] = useSidebarStore((state) => [state.setIsOpen]);
return (
@@ -18,7 +18,7 @@ function Header({ handleChangeLang }: HeaderProps) {
- */}
);
}
diff --git a/src/components/InviteModal.tsx b/src/components/InviteModal.tsx
new file mode 100644
index 0000000..b09c70e
--- /dev/null
+++ b/src/components/InviteModal.tsx
@@ -0,0 +1,122 @@
+import { useClipboard } from "use-clipboard-copy";
+import useModalStore from "../stores/useModalStore";
+import CloseIcon from "./icons/CloseIcon";
+import LinkIcon from "./icons/LinkIcon";
+import Button from "./ui/Button";
+import Input from "./ui/Input";
+import { ChangeEvent, FormEvent, useState } from "react";
+import api from "../utils/api";
+import { ToastContainer, toast } from "react-toastify";
+import MailIcon from "./icons/MailIcon";
+import QRCode from "react-qr-code";
+
+function InviteModal() {
+ const { setModal } = useModalStore();
+ const clipboard = useClipboard();
+ const [email, setEmail] = useState("");
+ const link = window.location.origin + window.location.pathname;
+
+ async function sendInvite(e: FormEvent) {
+ e.preventDefault();
+
+ if (!email) return;
+
+ const result = await api
+ .post("sendInvite", { json: { email, link } })
+ .json();
+ console.log(result);
+
+ toast.success("Приглашение отправлено", {
+ position: "top-center",
+ autoClose: 2000,
+ hideProgressBar: true,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "dark",
+ icon: ,
+ closeButton: false,
+ });
+
+ setEmail("");
+ }
+
+ function handleChangeEmail(e: ChangeEvent) {
+ setEmail(e.target.value.replace(/\s+/g, ""));
+ }
+
+ function handleClickClipboard() {
+ clipboard.copy();
+
+ toast.success("Ссылка скопирована в буфер обмена", {
+ position: "top-center",
+ autoClose: 2000,
+ hideProgressBar: true,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ theme: "dark",
+ icon: ,
+ closeButton: false,
+ });
+ }
+
+ return (
+
+
+
Пригласить
+
}
+ onlyIcon
+ onClick={() => setModal(null)}
+ />
+
+
+
+
+
+ Отсканируйте QR-код,
+
+ чтобы присоедениться
+
к демонстрации
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default InviteModal;
diff --git a/src/components/ModalContainer.tsx b/src/components/ModalContainer.tsx
index 9f42fb3..88c7215 100644
--- a/src/components/ModalContainer.tsx
+++ b/src/components/ModalContainer.tsx
@@ -24,19 +24,21 @@ function ModalContainer({ className }: ModalContainerProps) {
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
- return (
- setModal(null)}
- className={[
- "absolute w-full min-h-screen top-0 left-0 flex flex-col justify-center items-center p-8 bg-black bg-opacity-75 transition-opacity cursor-pointer",
- className,
- ].join(" ")}
- >
-
e.stopPropagation()} className="cursor-default">
- {modal}
+ if (modal) {
+ return (
+
setModal(null)}
+ className={[
+ "absolute w-full min-h-screen top-0 left-0 flex flex-col justify-center items-center p-8 bg-black bg-opacity-75 transition-opacity cursor-pointer z-10",
+ className,
+ ].join(" ")}
+ >
+
e.stopPropagation()} className="cursor-default">
+ {modal}
+
-
- );
+ );
+ }
}
export default ModalContainer;
diff --git a/src/components/PixelStreamingWrapper.tsx b/src/components/PixelStreamingWrapper.tsx
index e36c95e..e77ea12 100644
--- a/src/components/PixelStreamingWrapper.tsx
+++ b/src/components/PixelStreamingWrapper.tsx
@@ -1,8 +1,5 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
-/* eslint-disable no-empty */
-// Copyright Epic Games, Inc. All Rights Reserved.
-
import { useEffect, useRef, useState } from "react";
import {
Config,
@@ -10,6 +7,7 @@ import {
PixelStreaming,
LatencyTestResults,
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3";
+import { Trans } from "react-i18next";
export interface PixelStreamingWrapperProps {
initialSettings?: Partial
;
@@ -24,13 +22,13 @@ export const PixelStreamingWrapper = ({
const videoParent = useRef(null);
// Pixel streaming library instance is stored into this state variable after initialization:
- const [pixelStreaming, setPixelStreaming] = useState();
+ const [_pixelStreaming, setPixelStreaming] = useState();
// A boolean state variable that determines if the Click to play overlay is shown:
- const [clickToPlayVisible, setClickToPlayVisible] = useState(false);
+ const [_clickToPlayVisible, setClickToPlayVisible] = useState(false);
- const [latencyTestResult, setLatencyTestResult] =
- useState();
+ const [, setLatencyTestResult] = useState();
+ const [isVideoInitialized, setIsVideoInitialized] = useState(false);
// Run on component mount:
useEffect(() => {
@@ -41,6 +39,9 @@ export const PixelStreamingWrapper = ({
videoElementParent: videoParent.current,
});
+ document.getElementById("hiddenInput")?.remove();
+ document.getElementById("editTextButton")?.remove();
+
// register a playStreamRejected handler to show Click to play overlay if needed:
streaming.addEventListener("playStreamRejected", () => {
setClickToPlayVisible(true);
@@ -48,6 +49,7 @@ export const PixelStreamingWrapper = ({
streaming.addEventListener("videoInitialized", () => {
onVideoInitialized && onVideoInitialized();
+ setIsVideoInitialized(true);
});
streaming.addEventListener("latencyTestResult", (e) => {
@@ -55,9 +57,9 @@ export const PixelStreamingWrapper = ({
// console.log("Data", e.data.latencyTimings);
});
- setInterval(() => {
- streaming.requestLatencyTest();
- }, 500);
+ // setInterval(() => {
+ // streaming.requestLatencyTest();
+ // }, 500);
// Save the library instance into component state so that it can be accessed later:
setPixelStreaming(streaming);
@@ -66,7 +68,9 @@ export const PixelStreamingWrapper = ({
return () => {
try {
streaming.disconnect();
- } catch {}
+ } catch {
+ //
+ }
};
}
}, []);
@@ -80,13 +84,28 @@ export const PixelStreamingWrapper = ({
}}
>
- {clickToPlayVisible && (
+ {!isVideoInitialized && (
+
+
+
+
+
+ Буферизация потока
+
+
+ )}
+
+ {/* {clickToPlayVisible && (
-
Click to play
+
+
+ Нажмите, чтобы продолжить
+
+
- )}
-
-
- {latencyTestResult &&
- Object.entries(latencyTestResult).map(([key, value], i) => {
- if (
- [
- "EncodeMs",
- "CaptureToSendMs",
- "latencyExcludingDecode",
- "networkLatency",
- "testStartTimeMs",
- ].includes(key)
- )
- return (
-
- {key}: {value}
-
- );
- })}
-
+ )} */}
);
};
diff --git a/src/components/Player.tsx b/src/components/Player.tsx
index ea0c59e..d1a7a99 100644
--- a/src/components/Player.tsx
+++ b/src/components/Player.tsx
@@ -19,6 +19,7 @@ export const Player = ({ ss }: PlayerProps) => {
ss,
StartVideoMuted: false,
HoveringMouse: true,
+ WaitForStreamer: true,
}}
/>
diff --git a/src/components/SidebarTab1.tsx b/src/components/SidebarTab1.tsx
index e5cc6ee..b757481 100644
--- a/src/components/SidebarTab1.tsx
+++ b/src/components/SidebarTab1.tsx
@@ -10,14 +10,14 @@ import ky from "ky";
import LoaderIcon from "./icons/LoaderIcon";
function SidebarTab1() {
- const [currentTab, setCurrentTab, setIsOpen, setSelectedDay, buildId] =
- useSidebarTabStore((state) => [
- state.currentTab,
- state.setCurrentTab,
- state.setIsOpen,
- state.setSelectedDay,
- state.buildId,
- ]);
+ const {
+ currentTab,
+ setCurrentTab,
+ setIsOpen,
+ setSelectedDay,
+ companyId,
+ buildId,
+ } = useSidebarTabStore();
const [schedules, setSchedules] = useState();
function handleSelectDay(day: Date) {
@@ -28,7 +28,11 @@ function SidebarTab1() {
async function getSchedules() {
try {
const result: any[] = await ky
- .get(`${import.meta.env.VITE_CRM_API_URL}/schedules/builds/${buildId}`)
+ .get(
+ `${
+ import.meta.env.VITE_CRM_API_URL
+ }/schedules/companies/${companyId}/builds/${buildId}`
+ )
.json();
setSchedules(result);
diff --git a/src/components/SidebarTab2.tsx b/src/components/SidebarTab2.tsx
index a8e1d81..b74bdc1 100644
--- a/src/components/SidebarTab2.tsx
+++ b/src/components/SidebarTab2.tsx
@@ -5,12 +5,7 @@ import { Trans } from "react-i18next";
import useSidebarTabStore from "../stores/useSidebarStore";
import TimeSelector from "./TimeSelector";
import CloseIcon from "./icons/CloseIcon";
-import {
- eachMinuteOfInterval,
- format,
- isAfter,
- parse,
-} from "date-fns";
+import { eachMinuteOfInterval, format, isAfter, parse } from "date-fns";
import i18n from "../i18n";
import { enUS, ru } from "date-fns/locale";
import ky from "ky";
@@ -18,21 +13,15 @@ import { useEffect, useState } from "react";
import LoaderIcon from "./icons/LoaderIcon";
function SidebarTab2() {
- const [
+ const {
currentTab,
setCurrentTab,
setIsOpen,
setSelectedTime,
selectedDay,
+ companyId,
buildId,
- ] = useSidebarTabStore((state) => [
- state.currentTab,
- state.setCurrentTab,
- state.setIsOpen,
- state.setSelectedTime,
- state.selectedDay,
- state.buildId,
- ]);
+ } = useSidebarTabStore();
const [build, setBuild] = useState<{ [key: string]: any }>();
const [scheduledSessions, setScheduledSessions] = useState();
const [schedule, setSchedule] = useState<{ [key: string]: any }>();
@@ -58,7 +47,7 @@ function SidebarTab2() {
.get(
`${
import.meta.env.VITE_CRM_API_URL
- }/scheduled_sessions/builds/${buildId}?date=${format(
+ }/scheduled_sessions/companies/${companyId}/builds/${buildId}?date=${format(
selectedDay,
"yyyy-MM-dd"
)}`
@@ -76,7 +65,7 @@ function SidebarTab2() {
.get(
`${
import.meta.env.VITE_CRM_API_URL
- }/schedules/builds/${buildId}?date=${format(
+ }/schedules/companies/${companyId}/builds/${buildId}?date=${format(
selectedDay,
"yyyy-MM-dd"
)}`
@@ -103,10 +92,11 @@ function SidebarTab2() {
{ step }
);
+ console.log("scheduledSessions", scheduledSessions);
+
const formatTimes = times.map((time) => ({
value: format(time, "HH:mm"),
active:
- // isAfter(time, addMinutes(new Date(), 30)) &&
isAfter(time, new Date()) &&
scheduledSessions.filter(
(scheduledSession) => scheduledSession.startAt === time.toISOString()
diff --git a/src/components/SidebarTab3.tsx b/src/components/SidebarTab3.tsx
index 3ea0059..8f19b8e 100644
--- a/src/components/SidebarTab3.tsx
+++ b/src/components/SidebarTab3.tsx
@@ -6,23 +6,8 @@ import ArrowRightIcon from "./icons/ArrowRightIcon";
import CloseIcon from "./icons/CloseIcon";
function SidebarTab3() {
- const [
- currentTab,
- setCurrentTab,
- setIsOpen,
- name,
- phone,
- email,
- ] = useSidebarTabStore((state) => [
- state.currentTab,
- state.setCurrentTab,
- state.setIsOpen,
- state.name,
- state.phone,
- state.email,
- state.selectedDay,
- state.selectedTime,
- ]);
+ const { currentTab, setCurrentTab, setIsOpen, name, phone, email } =
+ useSidebarTabStore();
function handleSubmit() {
if (!name || !phone || !email) {
diff --git a/src/components/SidebarTab4.tsx b/src/components/SidebarTab4.tsx
index f05fdec..b5eb15e 100644
--- a/src/components/SidebarTab4.tsx
+++ b/src/components/SidebarTab4.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-irregular-whitespace */
import { format, parse } from "date-fns";
import useSidebarTabStore from "../stores/useSidebarStore";
@@ -9,9 +10,10 @@ import { Trans } from "react-i18next";
import i18n from "../i18n";
import { useState } from "react";
import LoaderIcon from "./icons/LoaderIcon";
+import api from "../utils/api";
function SidebarTab4() {
- const [
+ const {
currentTab,
setCurrentTab,
setIsOpen,
@@ -21,20 +23,22 @@ function SidebarTab4() {
phone,
email,
buildId,
- ] = useSidebarTabStore((state) => [
- state.currentTab,
- state.setCurrentTab,
- state.setIsOpen,
- state.selectedDay,
- state.selectedTime,
- state.name,
- state.phone,
- state.email,
- state.buildId,
- ]);
+ setUrl,
+ } = useSidebarTabStore();
const [isLoading, setIsLoading] = useState(false);
+ async function sendInvite(email: string, link: string) {
+ try {
+ const reuslt: any = await api
+ .post("sendInvite", { json: { email, link } })
+ .json();
+ console.log("reuslt", reuslt);
+ } catch (error) {
+ console.log({ error: (error as Error).message });
+ }
+ }
+
async function handleClickSignUp() {
if (!selectedTime || !selectedDay) {
return;
@@ -45,7 +49,7 @@ function SidebarTab4() {
const startAt = parse(selectedTime, "HH:mm", selectedDay);
try {
- await ky
+ const result: any = await ky
.post(`${import.meta.env.VITE_CRM_API_URL}/scheduled_sessions`, {
json: {
buildId,
@@ -59,8 +63,9 @@ function SidebarTab4() {
})
.json();
+ sendInvite(email, result.url);
+ setUrl(result.url);
setCurrentTab(currentTab + 1);
-
setIsLoading(false);
} catch (error) {
setIsLoading(false);
diff --git a/src/components/SidebarTab5.tsx b/src/components/SidebarTab5.tsx
index 7974b82..d145825 100644
--- a/src/components/SidebarTab5.tsx
+++ b/src/components/SidebarTab5.tsx
@@ -5,21 +5,37 @@ import ArrowRightIcon from "./icons/ArrowRightIcon";
import MailGradientIcon from "./icons/MailGradientIcon";
import PhoneGradientIcon from "./icons/PhoneGradientIcon";
import WebGradientIcon from "./icons/WebGradientIcon";
+import { useCopyToClipboard } from "usehooks-ts";
+import { Bounce, ToastContainer, toast } from "react-toastify";
+import InfoIcon from "./icons/InfoBlueIcon";
function SidebarTab5() {
- const [setIsOpen, name] = useSidebarTabStore((state) => [
- state.setIsOpen,
- state.name,
- ]);
+ const { setIsOpen, name, url } = useSidebarTabStore();
+ const [, copyToClipboard] = useCopyToClipboard();
+
+ function handleClickCopy() {
+ copyToClipboard(url);
+ toast.info("Ссылка скопирована в буфер обмена!", {
+ icon: ,
+ position: "top-center",
+ autoClose: 5000,
+ hideProgressBar: false,
+ closeOnClick: true,
+ pauseOnHover: true,
+ draggable: true,
+ progress: undefined,
+ theme: "light",
+ transition: Bounce,
+ });
+ }
return (
- {name},
-
+ {name},{" "}
спасибо за запись
@@ -36,7 +52,32 @@ function SidebarTab5() {
-
+
+
Ссылка для подключения
+
console.log(1)}
+ />
+
+ Ссылка, получаемая пользователем на почтовый адрес, для подключения
+ к демонстрации.
+
+
+
+
+
+
+
+
+
Возникли вопросы?
@@ -68,26 +109,26 @@ function SidebarTab5() {
-
-
-
-
- Запись на демонстрацию работает в ознакомительном режиме и не
- сохраняет введенные данные
-
-
+
+
+
+ Запись на демонстрацию работает в ознакомительном режиме и не
+ сохраняет введенные данные
+
+
-
-
+
+
+
diff --git a/src/components/ToastContainer.tsx b/src/components/ToastContainer.tsx
new file mode 100644
index 0000000..425f869
--- /dev/null
+++ b/src/components/ToastContainer.tsx
@@ -0,0 +1,5 @@
+function ToastContainer() {
+ return
;
+}
+
+export default ToastContainer;
diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx
new file mode 100644
index 0000000..0b52307
--- /dev/null
+++ b/src/components/Tooltip.tsx
@@ -0,0 +1,27 @@
+interface TooltipProps {
+ text: string;
+}
+
+function Tooltip({ text }: TooltipProps) {
+ return (
+
+ );
+}
+
+export default Tooltip;
diff --git a/src/components/User.tsx b/src/components/User.tsx
new file mode 100644
index 0000000..daf6501
--- /dev/null
+++ b/src/components/User.tsx
@@ -0,0 +1,86 @@
+import { useRef, useState } from "react";
+import CloseIcon from "./icons/CloseIcon";
+import HandOnIcon from "./icons/HandOnIcon";
+import Button from "./ui/Button";
+import MoreIcon from "./icons/MoreIcon";
+import { useOnClickOutside } from "usehooks-ts";
+import Tooltip from "./Tooltip";
+import IUser from "../types/IUser";
+
+interface UserProps {
+ user: IUser;
+ onTransferControl: (userId: string) => void;
+ onKickUser: (userId: string) => void;
+ className?: string;
+ tooltip?: boolean;
+}
+
+function User({
+ user,
+ onTransferControl,
+ onKickUser,
+ className,
+ tooltip = false,
+}: UserProps) {
+ const [isShowMore, setIsShowMore] = useState(false);
+ const moreRef = useRef(null);
+
+ function handleClickOutside() {
+ setIsShowMore(false);
+ }
+
+ useOnClickOutside(moreRef, handleClickOutside);
+
+ function handleClickTransferControl() {
+ onTransferControl(user.id);
+ setIsShowMore(false);
+ }
+
+ function handleClickKickUser() {
+ onKickUser(user.id);
+ setIsShowMore(false);
+ }
+
+ return (
+
+
}
+ onlyIcon
+ onClick={() => setIsShowMore((prev) => !prev)}
+ className="group relative"
+ >
+ {tooltip &&
}
+
+
+ {isShowMore && (
+
+
+
+
+ )}
+
+ );
+}
+
+export default User;
diff --git a/src/components/icons/AlertRedIcon.tsx b/src/components/icons/AlertRedIcon.tsx
new file mode 100644
index 0000000..757088d
--- /dev/null
+++ b/src/components/icons/AlertRedIcon.tsx
@@ -0,0 +1,37 @@
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function AlertRedIcon(
+ props: JSX.IntrinsicAttributes & SVGProps
+) {
+ return (
+
+ );
+}
+
+export default AlertRedIcon;
diff --git a/src/components/icons/ChatIcon.tsx b/src/components/icons/ChatIcon.tsx
index 5886055..b796048 100644
--- a/src/components/icons/ChatIcon.tsx
+++ b/src/components/icons/ChatIcon.tsx
@@ -1,16 +1,19 @@
-function ChatIcon() {
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function ChatIcon(props: JSX.IntrinsicAttributes & SVGProps) {
return (
);
diff --git a/src/components/icons/DesktopIcon.tsx b/src/components/icons/DesktopIcon.tsx
index 4cc1269..223f4ab 100644
--- a/src/components/icons/DesktopIcon.tsx
+++ b/src/components/icons/DesktopIcon.tsx
@@ -1,27 +1,21 @@
-function DesktopIcon() {
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function DesktopIcon(props: JSX.IntrinsicAttributes & SVGProps) {
return (
);
}
diff --git a/src/components/icons/FullscreenIcon.tsx b/src/components/icons/FullscreenIcon.tsx
index 605b5fe..707a6f0 100644
--- a/src/components/icons/FullscreenIcon.tsx
+++ b/src/components/icons/FullscreenIcon.tsx
@@ -1,25 +1,23 @@
-function FullscreenIcon() {
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function FullscreenIcon(
+ props: JSX.IntrinsicAttributes & SVGProps
+) {
return (
);
diff --git a/src/components/icons/HandOnIcon.tsx b/src/components/icons/HandOnIcon.tsx
index 718dcc2..36b4f52 100644
--- a/src/components/icons/HandOnIcon.tsx
+++ b/src/components/icons/HandOnIcon.tsx
@@ -1,23 +1,31 @@
-function HandOnIcon() {
- return (
-
- );
-}
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+const HandOnIcon = (
+ props: JSX.IntrinsicAttributes & SVGProps
+) => (
+
+);
export default HandOnIcon;
diff --git a/src/components/icons/InfoBlueIcon.tsx b/src/components/icons/InfoBlueIcon.tsx
new file mode 100644
index 0000000..26adf97
--- /dev/null
+++ b/src/components/icons/InfoBlueIcon.tsx
@@ -0,0 +1,35 @@
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function InfoIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+ return (
+
+ );
+}
+
+export default InfoIcon;
diff --git a/src/components/icons/InfoIcon.tsx b/src/components/icons/InfoIcon.tsx
new file mode 100644
index 0000000..bfcd69f
--- /dev/null
+++ b/src/components/icons/InfoIcon.tsx
@@ -0,0 +1,35 @@
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function InfoIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+ return (
+
+ );
+}
+
+export default InfoIcon;
diff --git a/src/components/icons/LinkIcon.tsx b/src/components/icons/LinkIcon.tsx
new file mode 100644
index 0000000..f4528b7
--- /dev/null
+++ b/src/components/icons/LinkIcon.tsx
@@ -0,0 +1,21 @@
+function LinkIcon() {
+ return (
+
+ );
+}
+
+export default LinkIcon;
diff --git a/src/components/icons/MicroOffIcon.tsx b/src/components/icons/MicroOffIcon.tsx
index 3d6f3f4..3f8edc2 100644
--- a/src/components/icons/MicroOffIcon.tsx
+++ b/src/components/icons/MicroOffIcon.tsx
@@ -1,26 +1,31 @@
-function MicroOffIcon() {
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function MicroOffIcon(
+ props: JSX.IntrinsicAttributes & SVGProps
+) {
return (
);
diff --git a/src/components/icons/MicroOnIcon.tsx b/src/components/icons/MicroOnIcon.tsx
index c4c5002..48d898a 100644
--- a/src/components/icons/MicroOnIcon.tsx
+++ b/src/components/icons/MicroOnIcon.tsx
@@ -1,18 +1,21 @@
-function MicroOnIcon() {
+import { SVGProps } from "react";
+import { JSX } from "react/jsx-runtime";
+
+function MicroOnIcon(props: JSX.IntrinsicAttributes & SVGProps) {
return (
);
diff --git a/src/components/icons/MobileIcon.tsx b/src/components/icons/MobileIcon.tsx
new file mode 100644
index 0000000..f00c5fb
--- /dev/null
+++ b/src/components/icons/MobileIcon.tsx
@@ -0,0 +1,25 @@
+import React from "react";
+import { JSX } from "react/jsx-runtime";
+
+function MobileIcon(
+ props: JSX.IntrinsicAttributes & React.SVGProps
+) {
+ return (
+
+ );
+}
+
+export default MobileIcon;
diff --git a/src/components/icons/Rotate64Icon.tsx b/src/components/icons/Rotate64Icon.tsx
new file mode 100644
index 0000000..e2c1607
--- /dev/null
+++ b/src/components/icons/Rotate64Icon.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { JSX } from "react/jsx-runtime";
+
+function Rotate64Icon(
+ props: JSX.IntrinsicAttributes & React.SVGProps
+) {
+ return (
+
+ );
+}
+
+export default Rotate64Icon;
diff --git a/src/components/icons/RotateIcon.tsx b/src/components/icons/RotateIcon.tsx
new file mode 100644
index 0000000..b686fc9
--- /dev/null
+++ b/src/components/icons/RotateIcon.tsx
@@ -0,0 +1,39 @@
+import React from "react";
+import { JSX } from "react/jsx-runtime";
+
+function RotateIcon(
+ props: JSX.IntrinsicAttributes & React.SVGProps
+) {
+ return (
+
+ );
+}
+
+export default RotateIcon;
diff --git a/src/components/icons/SendChatIcon.tsx b/src/components/icons/SendChatIcon.tsx
index 54b3210..3e70f51 100644
--- a/src/components/icons/SendChatIcon.tsx
+++ b/src/components/icons/SendChatIcon.tsx
@@ -1,5 +1,3 @@
-import React from "react";
-
function SendChatIcon() {
return (