This commit is contained in:
2024-06-06 18:15:30 +05:00
parent 63fea5da4b
commit 4a75925f86
59 changed files with 2733 additions and 5840 deletions
+4 -2
View File
@@ -3,5 +3,7 @@ VITE_COORD_URL=https://coord.graff.tech
# VITE_CRM_API_URL=http://localhost:3001
# VITE_CRM_API_URL=http://192.168.1.170:3001
VITE_CRM_API_URL=https://crm.stream.graff.tech/api
VITE_API_URL=http://192.168.1.171:5002
# VITE_API_URL=https://stream.graff.tech/api
# VITE_API_URL=http://localhost:5002
VITE_API_URL=https://stream.graff.tech/api
# VITE_SOCKET_URL=http://192.168.1.171:5003
VITE_SOCKET_URL=https://stream.graff.tech
+1 -16
View File
@@ -1,27 +1,12 @@
<!DOCTYPE html>
<!--
⠟⢦⡀
⢷⡄⠈⡓⠢⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠤⠂⢹
⠈⡷⡄⠈⠲⢤⣈⠻⠉⠛⠉⠉⠁⠒⠖⠉⠉⠉⠒⠶⢦⣤⠴⠒⢉⣡⠴
⠀⢸⡿⡂⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⡞⠉⠀⢀⣠⡞
⠀⠀⢙⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⢠⡼⡟
⠀⠀⡼⠋⠀⣤⣀⠀⠀⠀⠀⠀⠈⠐⣂⣄⠀⠀⠀⠀⠀⠀⠀⢀⠀⣰⡟⠁
⠀⢠⡇⠀⠀⠘⠛⠃⠀⠀⠀⠀⠾⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⢻
⠀⢸⡇⢺⡀⠀⢠⡒⠠⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡀⠀⠀⠸⡇
⠀⢸⡇⣘⠑⡀⠀⠙⢏⣁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠀⣔⣇
⠀⢸⡇⡁⠀⢳⣶⣾⣷⣦⣄⣀⡀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿
⠀⠸⡇⠁⠀⠀⢏⠉⠀⠀⠙⠛⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⡏
⠀⠀⠯⣀⣈⣀⣈⣐⣲⣄⣄⣤⣴⣆⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣈⣛⡧
-->
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=contain"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=0"
/>
<meta name="theme-color" content="#131317" />
<title>Удаленная демонстрация</title>
</head>
<body>
-4670
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -11,9 +11,9 @@
},
"dependencies": {
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.3": "^1.0.1",
"@livekit/components-react": "^1.1.6",
"@livekit/components-styles": "^1.0.6",
"@uidotdev/usehooks": "^2.0.1",
"@livekit/components-react": "^2.0.3",
"@livekit/components-styles": "^1.0.10",
"@uidotdev/usehooks": "^2.4.1",
"ahooks": "^3.7.10",
"date-fns": "^2.30.0",
"i18next": "^23.8.2",
@@ -33,12 +33,12 @@
"react-router-dom": "^6.11.2",
"react-timeit": "^1.2.12",
"react-timer-hook": "^3.0.7",
"react-toastify": "^9.1.3",
"react-toastify": "^10.0.5",
"react-transition-group": "^4.4.5",
"socket.io-client": "^4.7.4",
"ua-parser-js": "^1.0.35",
"use-clipboard-copy": "^0.2.0",
"usehooks-ts": "^2.9.1",
"usehooks-ts": "^3.0.1",
"uuid": "^9.0.1",
"zustand": "^4.3.9"
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+31
View File
@@ -0,0 +1,31 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0047 23.8317C18.5387 23.8317 23.8355 18.4968 23.8355 11.9158C23.8355 11.1099 23.756 10.3226 23.6046 9.56151H16.4572V14.2862H18.7635C17.7908 17.0932 15.1392 19.107 12.0206 19.107C8.07741 19.107 4.88082 15.8874 4.88082 11.9158C4.88082 7.94429 8.07741 4.7247 12.0206 4.7247V0H12.0047C5.47077 0 0.17395 5.33491 0.17395 11.9158C0.17395 18.4968 5.47077 23.8317 12.0047 23.8317Z" fill="#798FFF"/>
<path d="M11.4098 0.0144617C6.32664 0.806434 2.43579 5.22412 2.43579 10.5552C2.43579 16.4465 7.18735 21.2224 13.0487 21.2224C18.91 21.2224 23.6616 16.4465 23.6616 10.5552C23.6616 10.2154 23.6458 9.87934 23.6149 9.54768H16.4537V14.2655H18.7615C17.7882 17.0684 15.135 19.0794 12.0145 19.0794C8.06881 19.0794 4.87023 15.8644 4.87023 11.8986C4.87023 8.14839 7.7305 5.06963 11.378 4.74598C11.5877 4.72737 11.8 4.71786 12.0145 4.71786V0H11.9985C11.8011 0 11.6049 0.00486052 11.4098 0.0144617Z" fill="#D375FF"/>
<path opacity="0.3" d="M10.6289 4.87071C11.0744 4.78561 11.5344 4.74106 12.0048 4.74106V0H11.9886C11.0362 0 10.1096 0.110228 9.22107 0.318561L10.6289 4.87071Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M4.39083 2.69424L3.65356 13.3944L4.70739 12.3905C4.70113 12.2671 4.69797 12.1429 4.69797 12.0179C4.69797 8.05287 7.88392 4.83342 11.8307 4.78956L7.09778 1.04372C6.12009 1.47354 5.2108 2.0307 4.39083 2.69424Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M0.174411 12.0511L5.56565 14.96C5.13887 14.0439 4.9006 13.0224 4.9006 11.9455C4.9006 10.8671 5.13952 9.84433 5.56739 8.92718L3.91599 3.30513C1.61241 5.46829 0.17395 8.5392 0.17395 11.9455C0.17395 11.9807 0.174104 12.0159 0.174411 12.0511Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M0.17395 11.5734L7.32671 22.2972L15.1364 22.4401L12.0898 19.0826C12.0577 19.0831 12.0256 19.0833 11.9935 19.0833C8.05727 19.0833 4.86634 15.8928 4.86634 11.9571C4.86634 11.7971 4.87162 11.6383 4.882 11.481L0.17726 11.481C0.176038 11.5117 0.174935 11.5426 0.17395 11.5734Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M5.36615 21.7443L9.91699 18.7522C6.97277 17.8417 4.83915 15.1546 4.83915 11.9815C4.83915 11.5738 4.87438 11.1741 4.94202 10.7852L1.91382 18.2839C2.80794 19.6564 3.9855 20.836 5.36615 21.7443Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M15.8324 23.207C14.6183 23.6121 13.3174 23.8317 11.9645 23.8317C9.46563 23.8317 7.14437 23.0824 5.21948 21.7993L9.7758 18.787C10.4713 19.0048 11.2121 19.1222 11.9807 19.1222C12.0091 19.1222 12.0374 19.1221 12.0657 19.1218L15.8324 23.207Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M14.6145 23.5411C13.7626 23.7313 12.8762 23.8317 11.9661 23.8317C11.5718 23.8317 11.1819 23.8129 10.7973 23.776L9.74304 18.787C9.88538 18.8329 10.0297 18.8744 10.1757 18.9116L14.6145 23.5411Z" fill="black" fill-opacity="0.4"/>
<path opacity="0.3" d="M16.5283 11.481L23.6616 9.79512C23.6485 9.71899 23.6347 9.64311 23.6202 9.56747H16.5283V11.481Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M18.0112 14.2642L23.6616 9.57531C23.661 9.5727 23.6605 9.57009 23.66 9.56747L16.7023 14.2642H18.0112Z" fill="black" fill-opacity="0.4"/>
<path opacity="0.3" d="M21.3277 9.56747L16.1803 22.9619C18.2079 22.1872 19.9677 20.8695 21.2822 19.1862L22.0957 9.56747H21.3277Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M23.6091 9.56747L16.1803 22.9619C20.6548 21.2672 23.8355 16.9459 23.8355 11.8827C23.8355 11.0904 23.7576 10.3162 23.6091 9.56747Z" fill="black" fill-opacity="0.4"/>
<path d="M18.9693 0H23.6616V4.69676H18.9693V0Z" fill="#798FFF"/>
<path d="M23.6616 4.69676H18.9693L16.5283 7.13211H21.0468L23.6616 4.69676Z" fill="#798FFF"/>
<path d="M18.9693 4.69676V0L16.5283 2.60931V7.13211L18.9693 4.69676Z" fill="#798FFF"/>
<path opacity="0.3" d="M23.6616 4.04765V4.7029L21.0766 7.13212H20.5299V4.00095L23.6616 4.04765Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M16.5283 7.13211V2.5385L19.8043 2.2614L20.8779 2.54518L16.5283 7.13211Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M18.6152 0.521866L16.5283 2.67119V7.13212H19.312L18.6152 0.521866Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M16.6521 2.4286L16.5283 2.56191V7.13211L19.1489 4.61027L20.3756 4.56207L23.1396 5.02798L22.8768 4.63437L19.5632 1.04372L16.6521 2.4286Z" fill="#D375FF"/>
<path opacity="0.3" d="M19.138 0H18.9653L18.2681 0.742928L19.0907 1.21768L19.138 0Z" fill="black" fill-opacity="0.6"/>
<path opacity="0.3" d="M22.7916 5.59864L21.1639 7.13211H20.8778L21.1334 4.06637L22.3995 3.82698L22.7916 5.59864Z" fill="black" fill-opacity="0.6"/>
<path d="M18.964 4.69676H23.6615V0H18.964V4.69676Z" fill="url(#paint0_linear_1804_16472)"/>
<defs>
<linearGradient id="paint0_linear_1804_16472" x1="21.637" y1="0" x2="21.637" y2="4.69676" gradientUnits="userSpaceOnUse">
<stop stop-color="#D375FF"/>
<stop offset="1" stop-color="#798FFF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+11 -11
View File
@@ -29,18 +29,18 @@
@apply lg:text-base text-sm font-semibold text-[#77787d];
}
/* Scrollbar */
/* Toastify */
*::-webkit-scrollbar {
width: 8px;
.Toastify__toast-body {
padding: 0;
margin: 0 4px 0 0;
align-items: normal;
font-family: "Inter", sans-serif;
font-size: 14px;
color: #111c26;
}
*::-webkit-scrollbar-thumb {
background-color: #858585;
border: 3.5px solid #f2f2f2;
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover {
border-width: 2px;
.Toastify__toast-icon {
width: auto;
margin-inline-end: 8px;
}
+116 -30
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable no-irregular-whitespace */
import "./App.css";
@@ -17,46 +18,78 @@ import useSidebarStore from "./stores/useSidebarStore";
import { useEffect, useState } from "react";
import ky from "ky";
import { useNavigate, useSearchParams } from "react-router-dom";
import LoaderIcon from "./components/icons/LoaderIcon";
import { ToastContainer, toast } from "react-toastify";
import { Bounce, ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import AlertIcon from "./components/icons/AlertIcon";
import api from "./utils/api";
import InfoIcon from "./components/icons/InfoIcon";
function App() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isOpen] = useSidebarStore((state) => [state.isOpen]);
const [isOpen, setIsOpen] = useSidebarStore((state) => [
state.isOpen,
state.setIsOpen,
]);
const [loading, setLoading] = useState<boolean>(false);
const [countdownTimer, setCountdownTimer] = useState(15);
// const host = window.location.host;
const { t, i18n } = useTranslation();
const build = searchParams.get("build") || null;
const location =
searchParams.get("location") || (i18n.language === "ru" ? "a1" : "a2");
const [setIsOpenSidebar] = useSidebarStore((state) => [state.setIsOpen]);
const type = searchParams.get("type") || "demo";
function toastError(text: string) {
toast.error(text, {
icon: <InfoIcon className="text-red-500" />,
position: "top-center",
autoClose: 2000,
hideProgressBar: true,
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
theme: "dark",
icon: <AlertIcon />,
closeButton: false,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
async function startStream(title: string) {
async function getLang() {
const { countryCode, error }: { countryCode: string; error: string } =
await api.get("getCountryCode").json();
if (!error && countryCode !== "RU") {
i18n.changeLanguage("en");
}
}
async function startStream(build: string) {
const { countryCode, error }: { countryCode: string; error: string } =
await api.get("getCountryCode").json();
if (!countryCode) {
toastError("Неизвестная ошибка при получении кода страны");
return;
}
if (error) {
toastError(error);
return;
}
let location = "a1";
if (searchParams.has("location")) {
location = searchParams.get("location") as string;
} else if (countryCode !== "RU") {
location = "a2";
}
setLoading(true);
const response: { stream: string } = await ky
try {
const response: any = await ky
.get(
`${
import.meta.env.VITE_COORD_URL
}/start?location=${location}&title=${title}`
}/start?location=${location}&build=${build}&type=${type}`
)
.json();
@@ -68,13 +101,24 @@ function App() {
setTimeout(() => {
navigate(`/stream/${response.stream}`);
}, 15000);
} else {
} else if (response.error) {
toastError(response.error);
setLoading(false);
} else {
toastError("Неизвестная ошибка");
setLoading(false);
}
} catch (error) {
if (error instanceof Error) {
toastError(`Неизвестная ошибка 2: ${error.message}`);
}
setLoading(false);
toastError("Превышен лимит одновременных сессий, попробуйте позже.");
}
}
useEffect(() => {
getLang();
if (build) {
void startStream(build);
}
@@ -91,7 +135,7 @@ function App() {
<div className="min-h-screen bg-[#14161F] text-white overflow-hidden">
<div className="container mx-auto 2xl:px-10 lg:px-8 sm:px-6 px-4 max-w-[1600px]">
<Header
handleChangeLang={(lang) => void i18n.changeLanguage(lang)}
// handleChangeLang={(lang) => void i18n.changeLanguage(lang)}
/>
<div className="2xl:mt-[72px] lg:mt-16 sm:mt-[88px] mt-14 relative">
@@ -120,14 +164,53 @@ function App() {
<div className="sm:mt-16 mt-8 grid sm:grid-cols-2 lg:gap-4 sm:gap-3 gap-2">
<div
className="group relative sm:h-full h-[264px] bg-gray-700 bg-no-repeat bg-center bg-cover"
style={{ backgroundImage: `url("/images/cards/nks.jpg")` }}
style={{
backgroundImage: `url("/images/cards/upside.jpg")`,
}}
>
<div className="bg-gradient-card h-full transition-opacity group-hover:opacity-60 duration-300"></div>
<div className="p-6 absolute bottom-0 space-y-6">
<div>
<p className="xl:text-2xl text-xl font-gilroy font-semibold">
<Trans i18nKey={"main.cards.title1"}>
<Trans i18nKey={"main.cards.title4"}>
ЖК «Upside Towers»
</Trans>
</p>
<p className="lg:text-sm text-xs">
<Trans i18nKey={"main.cards.city3"}>
Россия, Москва
</Trans>
</p>
</div>
<button
onClick={() => void startStream("upsideTowersDev")}
className="flex bg-gradient rounded-full p-2 gap-0 group-hover:gap-1 group-hover:pr-4 group-hover:pl-6 transition-all duration-300"
>
<span className="font-medium w-0 opacity-0 group-hover:w-[82px] group-hover:opacity-100 overflow-hidden transition-all duration-300">
<Trans i18nKey={"main.cards.button"}>Запустить</Trans>
</span>
<ArrowRightIcon />
</button>
</div>
</div>
<div className="grid lg:grid-cols-2 lg:gap-4 sm:gap-3 gap-2">
{i18n.language === "ru" ? (
<>
<div
className="group relative lg:h-[508px] sm:h-[284px] h-[264px] col-span-1 bg-gray-700 bg-no-repeat bg-center bg-cover"
style={{
backgroundImage: `url("/images/cards/nks.jpg")`,
}}
>
<div className="bg-gradient-card h-full transition-opacity group-hover:opacity-50 duration-300"></div>
<div className="p-6 absolute bottom-0 space-y-6">
<div>
<p className="xl:text-2xl text-xl font-gilroy font-semibold">
<Trans i18nKey={"main.cards.title"}>
МФК «Revolution towers»
</Trans>
</p>
@@ -143,16 +226,15 @@ function App() {
className="flex bg-gradient rounded-full p-2 gap-0 group-hover:gap-1 group-hover:pr-4 group-hover:pl-6 transition-all duration-300"
>
<span className="font-medium w-0 opacity-0 group-hover:w-[82px] group-hover:opacity-100 overflow-hidden transition-all duration-300">
<Trans i18nKey={"main.cards.button"}>Запустить</Trans>
<Trans i18nKey={"main.cards.button"}>
Запустить
</Trans>
</span>
<ArrowRightIcon />
</button>
</div>
</div>
<div className="grid lg:grid-cols-2 lg:gap-4 sm:gap-3 gap-2">
{i18n.language === "ru" ? (
<>
<div
className="group relative lg:h-[508px] sm:h-[284px] h-[264px] col-span-1 bg-gray-700 bg-no-repeat bg-center bg-cover"
style={{
@@ -189,7 +271,7 @@ function App() {
</div>
</div>
<div
{/* <div
className="group relative lg:h-[508px] sm:h-[284px] h-[264px] col-span-1 bg-gray-700 bg-no-repeat bg-center bg-cover"
style={{
backgroundImage: `url("/images/cards/aivaz.jpg")`,
@@ -223,7 +305,7 @@ function App() {
<ArrowRightIcon />
</button>
</div>
</div>
</div> */}
</>
) : (
<div
@@ -283,7 +365,7 @@ function App() {
</Trans>
</p>
<button
onClick={() => setIsOpenSidebar(true)}
onClick={() => setIsOpen(true)}
className="group relative px-6 py-2 bg-gradient rounded-full lg:text-base text-sm font-medium leading-normal w-fit"
>
<div className="absolute top-0 left-0 w-full h-full rounded-full bg-black opacity-0 group-hover:opacity-10 transition-all"></div>
@@ -477,8 +559,12 @@ function App() {
</>
) : (
<div className="bg-[#14161F] h-screen flex justify-center items-center">
<p className="self-center text-white flex items-center gap-2 w-fit font-gilroy">
<LoaderIcon className="animate-spin w-8 h-8" />
<p className="self-center text-white flex items-center gap-4 w-fit font-gilroy">
<img
src="/icons/Loader.png"
alt=""
className="animate-spin w-6 h-6"
/>
<span>
<Trans i18nKey="loading">Загрузка</Trans>
... {countdownTimer} <Trans i18nKey="loadingSub">сек</Trans>
-4
View File
@@ -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) {
+16 -4
View File
@@ -6,12 +6,24 @@ function ErrorBoundary() {
console.error(error);
return (
<div className="h-screen flex items-center justify-center p-8 text-white">
<div className="flex items-center gap-4">
<p className="font-gilroy text-2xl">{error.status}</p>
<p>{error.statusText}</p>
<main className="grid min-h-screen place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
<div className="text-center">
<p className="text-base font-semibold text-indigo-600">
{error.status}
</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-gray-900 sm:text-5xl">
{error.statusText}
</h1>
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href="/"
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Go back home
</a>
</div>
</div>
</main>
);
}
+3 -5
View File
@@ -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<number>();
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);
+9 -11
View File
@@ -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() {
</>
) : (
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center gap-4">
<LoaderIcon className="animate-spin w-8 h-8" />
<p className="flex items-center gap-4">
<img
src="/icons/Loader.png"
alt=""
className="animate-spin w-6 h-6"
/>
<Trans i18nKey="streamWaiting">Ожидание потока</Trans>
</p>
</div>
)
) : (
+3 -3
View File
@@ -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),
})
)
}
+23 -5
View File
@@ -181,10 +181,11 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
<div
ref={messagesRef}
className={`border-t border-b border-[#DAE0E5] h-full flex-1 overflow-y-scroll py-2 px-1`}
className={`border-t border-b border-[#DAE0E5] flex-1 flex flex-col gap-2 overflow-y-scroll overflow-x-hidden py-2 px-1`}
>
{messages.map((message) => (
{messages.map((message, index) => (
<div
key={index}
className={`flex gap-1 items-end ${
message.user.id === userId ? "self-end" : ""
}`}
@@ -198,8 +199,10 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
)}
<div className="relative">
<div
className={`p-2 rounded-tl-[4px] rounded-r-lg flex flex-col gap-1 ${
message.user.id !== userId ? "bg-[#F0F1F2]" : "bg-[#C4DDF5]"
className={`flex flex-col gap-1 p-2 rounded-t-lg ${
message.user.id !== userId
? "rounded-r-lg bg-[#F0F1F2]"
: "rounded-l-lg bg-[#C4DDF5]"
}`}
>
{message.user.id !== userId && (
@@ -217,10 +220,25 @@ function ChatNew({ isShow, socket, userId, name, onClose }: ChatNewProps) {
{message.time}
</p>
</div>
{message.user.id !== userId && (
{message.user.id !== userId ? (
<div className="absolute bottom-0 -left-[7px]">
<SubtracktIcon />
</div>
) : (
<div className="absolute bottom-0 -right-[7px]">
<svg
width="7"
height="12"
viewBox="0 0 7 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 0V12H6.75C6.88807 12 7 11.8881 7 11.75C7 11.6119 6.88996 11.4995 6.75615 11.4655C4.98232 11.0143 0 6.18654 0 0Z"
fill="#C4DDF5"
/>
</svg>
</div>
)}
</div>
</div>
+7 -7
View File
@@ -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) {
<a href="/" className="sm:hidden block">
<LogoMobileIcon />
</a>
<div className="flex sm:gap-8 gap-2">
{/* <div className="flex sm:gap-8 gap-2">
<div className="flex gap-1">
<button
className={[
@@ -43,7 +43,7 @@ function Header({ handleChangeLang }: HeaderProps) {
EN
</button>
</div>
</div>
</div> */}
</header>
);
}
+122
View File
@@ -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<string>("");
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: <MailIcon />,
closeButton: false,
});
setEmail("");
}
function handleChangeEmail(e: ChangeEvent<HTMLInputElement>) {
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: <LinkIcon />,
closeButton: false,
});
}
return (
<div className="w-[400px] bg-white rounded-lg">
<div className="flex justify-between items-center pl-6 pr-2 py-2 border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">Пригласить</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setModal(null)}
/>
</div>
<div className="px-6 py-4 border-b border-[#DAE0E5]">
<div className="flex items-center justify-between">
<p className="font-semibold text-center">
Отсканируйте QR-код,
<br />
чтобы присоедениться
<br />к демонстрации
</p>
<QRCode size={128} value={link} />
</div>
</div>
<div className="px-6 py-4 border-b border-[#DAE0E5]">
<form onSubmit={sendInvite} className="flex gap-2">
<Input
type="email"
placeholder="Email"
autoFocus
value={email}
onChange={handleChangeEmail}
/>
<Button type="submit" large>
<p className="text-xs">Пригласить</p>
</Button>
</form>
</div>
<div className="px-6 py-3">
<input
ref={clipboard.target}
type="hidden"
value={window.location.origin + window.location.pathname}
/>
<button
className="flex items-center gap-1 text-[#49A1F5] hover:text-[#4190DB] transition-colors"
onClick={handleClickClipboard}
>
<LinkIcon />
<span className="text-xs">Скопировать ссылку</span>
</button>
</div>
<ToastContainer />
</div>
);
}
export default InviteModal;
+3 -1
View File
@@ -24,11 +24,12 @@ function ModalContainer({ className }: ModalContainerProps) {
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
if (modal) {
return (
<div
onClick={() => 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",
"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(" ")}
>
@@ -37,6 +38,7 @@ function ModalContainer({ className }: ModalContainerProps) {
</div>
</div>
);
}
}
export default ModalContainer;
+38 -35
View File
@@ -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<AllSettings>;
@@ -24,13 +22,13 @@ export const PixelStreamingWrapper = ({
const videoParent = useRef<HTMLDivElement>(null);
// Pixel streaming library instance is stored into this state variable after initialization:
const [pixelStreaming, setPixelStreaming] = useState<PixelStreaming>();
const [_pixelStreaming, setPixelStreaming] = useState<PixelStreaming>();
// 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<LatencyTestResults>();
const [, setLatencyTestResult] = useState<LatencyTestResults>();
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 = ({
}}
>
<div
ref={videoParent}
style={{
width: "100%",
height: "100%",
}}
ref={videoParent}
/>
{clickToPlayVisible && (
{!isVideoInitialized && (
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<p className="flex items-center gap-4">
<span>
<img
src="/icons/Loader.png"
alt=""
className="animate-spin w-6 h-6"
/>
</span>
<Trans i18nKey="streamBuffering">Буферизация потока</Trans>
</p>
</div>
)}
{/* {clickToPlayVisible && (
<div
style={{
position: "absolute",
@@ -105,29 +124,13 @@ export const PixelStreamingWrapper = ({
setClickToPlayVisible(false);
}}
>
<div>Click to play</div>
<div className="bg-gradient rounded px-4 py-2 opacity-95 hover:opacity-100 transition-opacity">
<p>
<Trans i18nKey="clickToContinue">Нажмите, чтобы продолжить</Trans>
</p>
</div>
)}
<div className="absolute bottom-0 left-0 p-4 text-white text-xs bg-black">
{latencyTestResult &&
Object.entries(latencyTestResult).map(([key, value], i) => {
if (
[
"EncodeMs",
"CaptureToSendMs",
"latencyExcludingDecode",
"networkLatency",
"testStartTimeMs",
].includes(key)
)
return (
<div key={i}>
{key}: {value}
</div>
);
})}
</div>
)} */}
</div>
);
};
+1
View File
@@ -19,6 +19,7 @@ export const Player = ({ ss }: PlayerProps) => {
ss,
StartVideoMuted: false,
HoveringMouse: true,
WaitForStreamer: true,
}}
/>
</div>
+13 -9
View File
@@ -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<any[]>();
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);
+8 -18
View File
@@ -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<any[]>();
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()
+2 -17
View File
@@ -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) {
+19 -14
View File
@@ -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<boolean>(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);
+49 -8
View File
@@ -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: <InfoIcon />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
return (
<div className="sm:p-8 p-6 flex flex-col justify-between sm:gap-8 gap-6 min-h-full">
<div>
<div className="flex items-start justify-between">
<p className="text-2xl font-semibold font-gilroy w-fit leading-snug">
<span className="text-gradient">{name},</span>
<br />
<span className="text-gradient">
{name},{" "}
<Trans i18nKey={"sidebar.title4_1"}>спасибо за запись</Trans>
</span>
<br />
@@ -36,7 +52,32 @@ function SidebarTab5() {
</Trans>
</p>
<div className="mt-8 pb-6 font-gilroy font-semibold border-b border-[#3D425C]">
<div className="sm:mt-6 mt-4 text-sm">
<p className="font-semibold mb-4">Ссылка для подключения</p>
<input
type="text"
readOnly
value={url}
className="p-4 border border-[#3D425C] bg-transparent outline-none w-full mb-2"
onClick={() => console.log(1)}
/>
<p className="text-xs text-[#52587A] mb-4">
Ссылка, получаемая пользователем на почтовый адрес, для подключения
к демонстрации.
</p>
<button
className="px-4 py-3.5 border border-[#3D425C] bg-transparent outline-none w-full rounded-full bg-[#3D425C] bg-opacity-20 hover:bg-[#52587A] hover:bg-opacity-20 transition-colors"
onClick={handleClickCopy}
>
Скопировать
</button>
</div>
<ToastContainer />
</div>
<div>
<div className="mb-4 pb-6 font-gilroy font-semibold border-b border-[#3D425C] sm:block hidden">
<p>
<Trans i18nKey={"sidebar.tab5text2"}>Возникли вопросы?</Trans>
</p>
@@ -68,7 +109,6 @@ function SidebarTab5() {
</div>
</div>
</div>
</div>
<div className="flex flex-col sm:gap-6 gap-4">
<p className="text-center text-xs opacity-50 leading-tight">
@@ -91,6 +131,7 @@ function SidebarTab5() {
</div>
</div>
</div>
</div>
);
}
+5
View File
@@ -0,0 +1,5 @@
function ToastContainer() {
return <div></div>;
}
export default ToastContainer;
+27
View File
@@ -0,0 +1,27 @@
interface TooltipProps {
text: string;
}
function Tooltip({ text }: TooltipProps) {
return (
<div className="group-hover:opacity-100 opacity-0 transition-opacity absolute top-[calc(100%+12px)] -left-1.5 bg-[#111C26] rounded-lg px-4 py-2 z-10 pointer-events-none whitespace-nowrap">
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute -top-2.5"
>
<path
d="M5.14251 1.42916C5.53091 0.781817 6.46909 0.781816 6.85749 1.42915L12 10H0L5.14251 1.42916Z"
fill="#111C26"
/>
</svg>
<p className="text-white text-xs">{text}</p>
</div>
);
}
export default Tooltip;
+86
View File
@@ -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 (
<div ref={moreRef} className="relative">
<Button
variant="tertiary"
icon={<MoreIcon />}
onlyIcon
onClick={() => setIsShowMore((prev) => !prev)}
className="group relative"
>
{tooltip && <Tooltip text="Действия" />}
</Button>
{isShowMore && (
<div
className={`absolute top-[calc(100%+18px)] bg-white rounded-lg py-2 z-10 shadow ${className}`}
>
<button
className="flex items-center gap-2 px-4 py-1 hover:bg-[#E6ECF2] transition-colors w-full"
onClick={handleClickTransferControl}
>
<span className="text-[#77828C]">
<HandOnIcon />
</span>
<span className="text-sm whitespace-nowrap">
Передать управление
</span>
</button>
<button
className="flex items-center gap-2 px-4 py-1 hover:bg-[#E6ECF2] transition-colors w-full"
onClick={handleClickKickUser}
>
<span className="text-[#EB5757]">
<CloseIcon />
</span>
<span className="text-sm">Исключить</span>
</button>
</div>
)}
</div>
);
}
export default User;
+37
View File
@@ -0,0 +1,37 @@
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function AlertRedIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
stroke="currentColor"
strokeWidth={1.5}
d="M21.25 12a9.25 9.25 0 1 1-18.502 0 9.25 9.25 0 0 1 18.502 0Z"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeWidth={1.5}
d="M12 7v6"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12.008 17H12v.008h.008V17Z"
/>
</svg>
);
}
export default AlertRedIcon;
+10 -7
View File
@@ -1,16 +1,19 @@
function ChatIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function ChatIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
d="M17 6H6C4.89543 6 4 6.89543 4 8V15C4 16.1046 4.89543 17 6 17H11L14.3753 19.7002C15.0301 20.2241 16 19.7579 16 18.9194V17H17C18.1046 17 19 16.1046 19 15V8C19 6.89543 18.1046 6 17 6Z"
stroke="currentColor"
strokeWidth="1.5"
strokeWidth={1.5}
d="M17 6H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h5l3.375 2.7A1 1 0 0 0 16 18.92V17h1a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2Z"
/>
</svg>
);
+12 -18
View File
@@ -1,27 +1,21 @@
function DesktopIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function DesktopIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
{...props}
>
<g id="Icon/Desktop">
<path
id="Rectangle 1048"
d="M9.6 16.8889H5C4.44771 16.8889 4 16.4412 4 15.8889V7C4 6.44772 4.44772 6 5 6H19C19.5523 6 20 6.44772 20 7V15.8889C20 16.4412 19.5523 16.8889 19 16.8889H14.4M9.6 16.8889L8.96059 19.3755C8.87924 19.6919 9.11817 20 9.44484 20H14.5552C14.8818 20 15.1208 19.6919 15.0394 19.3755L14.4 16.8889M9.6 16.8889H14.4"
stroke="#F2F2F2"
stroke="currentColor"
strokeLinecap="round"
strokeWidth={1.5}
d="M2.667 3.879c0-.67.596-1.212 1.333-1.212h8c.736 0 1.333.542 1.333 1.212V8.12c0 .67-.597 1.212-1.333 1.212H4c-.737 0-1.333-.542-1.333-1.212V3.88ZM13.28 11H2.72a1 1 0 1 0 0 2h10.56a1 1 0 1 0 0-2Z"
/>
<rect
id="Rectangle 1049"
x="6"
y="8"
width="12"
height="7"
stroke="#F2F2F2"
/>
</g>
</svg>
);
}
+12 -14
View File
@@ -1,25 +1,23 @@
function FullscreenIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function FullscreenIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
d="M4 20V14M4 20H10M4 20L10 14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20 4V10M20 4H14M20 4L14 10"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 20v-6m0 6h6m-6 0 6-6M20 4v6m0-6h-6m6 0-6 6"
/>
</svg>
);
+21 -13
View File
@@ -1,23 +1,31 @@
function HandOnIcon() {
return (
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
const HandOnIcon = (
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) => (
<svg
width="24"
height="24"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="Icon/HandOn">
<g clipPath="url(#clip0_644_34553)">
<path
id="Vector"
d="M9.49525 2.00294C9.89847 2.00294 10.2047 2.31686 10.2047 2.73115V8.58062V11.5728C10.2047 11.6516 10.2198 11.7297 10.2492 11.8025C10.2786 11.8753 10.3217 11.9415 10.376 11.9973C10.4303 12.053 10.4947 12.0972 10.5657 12.1274C10.6366 12.1576 10.7126 12.1731 10.7894 12.1731C10.8662 12.1731 10.9422 12.1576 11.0131 12.1274C11.0841 12.0972 11.1485 12.053 11.2028 11.9973C11.2571 11.9415 11.3002 11.8753 11.3296 11.8025C11.359 11.7297 11.3741 11.6516 11.3741 11.5728V8.58062C11.3741 8.16633 11.6806 7.8516 12.0835 7.8516C12.4865 7.8516 12.793 8.16633 12.793 8.58062V9.38624V11.5728C12.793 11.6516 12.8081 11.7297 12.8375 11.8025C12.8669 11.8753 12.91 11.9415 12.9643 11.9973C13.0186 12.053 13.083 12.0972 13.1539 12.1274C13.2249 12.1576 13.3009 12.1731 13.3777 12.1731C13.4545 12.1731 13.5305 12.1576 13.6014 12.1274C13.6724 12.0972 13.7368 12.053 13.7911 11.9973C13.8454 11.9415 13.8885 11.8753 13.9179 11.8025C13.9473 11.7297 13.9624 11.6516 13.9624 11.5728V9.38624C13.9624 8.97195 14.2689 8.65723 14.6718 8.65723C15.0748 8.65723 15.3813 8.97195 15.3813 9.38624V10.3801V11.5728C15.3813 11.6516 15.3964 11.7297 15.4258 11.8025C15.4552 11.8753 15.4983 11.9415 15.5526 11.9973C15.6069 12.053 15.6713 12.0972 15.7422 12.1274C15.8132 12.1576 15.8892 12.1731 15.966 12.1731C16.0428 12.1731 16.1188 12.1576 16.1897 12.1274C16.2607 12.0972 16.3251 12.053 16.3794 11.9973C16.4337 11.9415 16.4768 11.8753 16.5062 11.8025C16.5356 11.7297 16.5507 11.6516 16.5507 11.5728V10.3801C16.5507 9.96551 16.8572 9.65078 17.2601 9.65078C17.6631 9.65078 17.9696 9.96551 17.9696 10.3801C17.9696 10.3801 18.0375 13.6677 17.9704 15.2491C17.934 16.1003 17.7234 17.2992 16.9211 18.2591C16.1194 19.219 14.7072 20.0222 11.9962 19.9995C10.3875 19.9728 9.3806 19.2654 8.64072 18.3736C7.90109 17.4815 7.4893 16.4337 7.20255 15.7982C6.54793 14.3476 5.10014 11.6571 5.10014 11.6571C4.89736 11.2983 5.01175 10.8678 5.36011 10.6609C5.70926 10.4527 6.12678 10.5701 6.32852 10.9278L7.7004 13.3685C7.76482 13.4828 7.86416 13.5721 7.98304 13.6226C8.10192 13.6731 8.23369 13.6819 8.35794 13.6477C8.4822 13.6135 8.592 13.5382 8.67033 13.4334C8.74867 13.3286 8.79117 13.2002 8.79124 13.0682V2.72821C8.79124 2.31392 9.09749 2 9.50097 2L9.49525 2.00294Z"
fill="#F2F2F2"
stroke="#F2F2F2"
strokeWidth="1.2"
d="M7.69615 15L7.69617 14.5M10.6962 7C10.6962 6.5 10.3962 5.5 9.19615 5.5C7.99615 5.5 7.69615 6.5 7.69615 7L7.69617 14.5M10.6962 7V12M10.6962 7V4.5C10.6962 4 10.9962 3 12.1962 3C13.3962 3 13.6962 4 13.6962 4.5V6M13.6962 12V6M13.6962 6C13.6962 5.5 13.9962 4.5 15.1962 4.5C16.3962 4.5 16.6962 5.5 16.6962 6V10M16.6962 13V10M16.6962 10C16.6962 9.5 16.9962 8.5 18.1962 8.5C19.3962 8.5 19.6962 9.5 19.6962 10V16C19.6962 18 17.6335 22 12.1962 22C10.0262 22 8 21 7 19.5C4.9972 17.9297 4.69615 16.4895 4.69615 15C4.69615 14.6667 4.59615 13.8 4.19615 13C3.69615 12 4.19615 11 5.19615 11C5.99615 11 6.5295 11.6667 6.69617 12C7.02951 12.5 7.69618 13.7 7.69617 14.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_644_34553">
<rect width={24} height={24} fill="white" />
</clipPath>
</defs>
</svg>
);
}
);
export default HandOnIcon;
+35
View File
@@ -0,0 +1,35 @@
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function InfoIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
stroke="#49A1F5"
strokeWidth={1.5}
d="M21.25 12a9.25 9.25 0 1 1-18.502 0 9.25 9.25 0 0 1 18.502 0Z"
/>
<path
stroke="#49A1F5"
strokeLinecap="round"
strokeWidth={1.5}
d="M12 17.008v-6"
/>
<path
stroke="#49A1F5"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12.008 7.008H12V7h.008v.008Z"
/>
</svg>
);
}
export default InfoIcon;
+35
View File
@@ -0,0 +1,35 @@
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function InfoIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
stroke="currentColor"
strokeWidth={1.5}
d="M21.25 12a9.25 9.25 0 1 1-18.502 0 9.25 9.25 0 0 1 18.502 0Z"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeWidth={1.5}
d="M12 7v6"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12.008 17H12v.008h.008V17Z"
/>
</svg>
);
}
export default InfoIcon;
+21
View File
@@ -0,0 +1,21 @@
function LinkIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.73231 5.96185C9.1322 6.15278 9.48043 6.43688 9.74775 6.7903C10.0151 7.14372 10.1937 7.55614 10.2686 7.9929C10.3434 8.42966 10.3124 8.87801 10.1781 9.30029C10.0438 9.72257 9.81007 10.1065 9.49661 10.4197L6.72739 13.1889C6.20806 13.7082 5.50369 14 4.76924 14C4.03479 14 3.33042 13.7082 2.81109 13.1889C2.29176 12.6696 2 11.9652 2 11.2308C2 10.4963 2.29176 9.79195 2.81109 9.27261L3.89232 8.19138M12.1077 7.80862L13.1889 6.72739C13.7082 6.20806 14 5.50369 14 4.76924C14 4.03479 13.7082 3.33042 13.1889 2.81109C12.6696 2.29176 11.9652 2 11.2308 2C10.4963 2 9.79195 2.29176 9.27261 2.81109L6.50339 5.58031C6.18994 5.89355 5.95623 6.27743 5.8219 6.69971C5.68758 7.12199 5.65655 7.57035 5.73143 8.00711C5.80632 8.44387 5.98492 8.85628 6.25225 9.2097C6.51957 9.56312 6.8678 9.84722 7.26769 10.0381"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export default LinkIcon;
+19 -14
View File
@@ -1,26 +1,31 @@
function MicroOffIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function MicroOffIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.25 11.3407V12.6429C8.25 13.5643 8.67801 14.4198 9.39059 15.0306C10.0996 15.6383 11.0396 15.9643 12 15.9643C12.2691 15.9643 12.5366 15.9387 12.7978 15.8885L11.2757 14.3664C10.9294 14.2701 10.6173 14.1064 10.3668 13.8917C10.0204 13.5948 9.821 13.2283 9.76584 12.8565L8.25 11.3407ZM14.25 11.16V5.57143C14.25 5.12894 14.0459 4.67629 13.6332 4.3226C13.217 3.96584 12.6309 3.75 12 3.75C11.3691 3.75 10.783 3.96584 10.3668 4.3226C9.95413 4.67629 9.75 5.12894 9.75 5.57143V6.65997L8.27524 5.18521C8.37666 4.41304 8.77871 3.70817 9.39059 3.18371C10.0996 2.576 11.0396 2.25 12 2.25C12.9604 2.25 13.9004 2.576 14.6094 3.18371C15.322 3.79449 15.75 4.64994 15.75 5.57143V12.6429C15.75 12.6485 15.75 12.6542 15.75 12.6599L14.25 11.16ZM13.7058 16.7965C13.1641 16.9528 12.5886 17.0357 12 17.0357C10.5735 17.0357 9.22428 16.549 8.24545 15.71C7.2702 14.874 6.75 13.7673 6.75 12.6429V11.3571C6.75 10.9429 6.41421 10.6071 6 10.6071C5.58579 10.6071 5.25 10.9429 5.25 11.3571V12.6429C5.25 14.2463 5.99408 15.7558 7.26927 16.8489C8.35879 17.7827 9.76505 18.3547 11.25 18.4993V20.25H8.25C7.83579 20.25 7.5 20.5858 7.5 21C7.5 21.4142 7.83579 21.75 8.25 21.75H12H15.75C16.1642 21.75 16.5 21.4142 16.5 21C16.5 20.5858 16.1642 20.25 15.75 20.25H12.75V18.4993C13.4901 18.4272 14.2106 18.249 14.8821 17.9728L13.7058 16.7965ZM18.1549 15.0648L17.0189 13.9289C17.1717 13.5127 17.25 13.0791 17.25 12.6429V11.3571C17.25 10.9429 17.5858 10.6071 18 10.6071C18.4142 10.6071 18.75 10.9429 18.75 11.3571V12.6429C18.75 13.4926 18.5411 14.3159 18.1549 15.0648Z"
fill="currentColor"
fillRule="evenodd"
d="M8.25 11.34v1.303c0 .921.428 1.777 1.14 2.388.71.607 1.65.933 2.61.933.27 0 .537-.025.798-.075l-1.522-1.523a2.374 2.374 0 0 1-.91-.474c-.346-.297-.545-.664-.6-1.035L8.25 11.34Zm6-.18V5.571c0-.442-.204-.895-.617-1.248-.416-.357-1.002-.573-1.633-.573-.63 0-1.217.216-1.633.573-.413.353-.617.806-.617 1.248V6.66L8.275 5.185c.102-.772.504-1.477 1.116-2.001C10.1 2.576 11.04 2.25 12 2.25s1.9.326 2.61.934c.712.61 1.14 1.466 1.14 2.387v7.089l-1.5-1.5Zm-.544 5.637a6.161 6.161 0 0 1-1.706.239c-1.427 0-2.776-.487-3.755-1.326-.975-.836-1.495-1.943-1.495-3.067v-1.286a.75.75 0 0 0-1.5 0v1.286c0 1.603.744 3.113 2.02 4.206 1.089.934 2.495 1.506 3.98 1.65v1.751h-3a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-3V18.5c.74-.073 1.46-.251 2.132-.527l-1.176-1.176Zm4.449-1.732-1.136-1.136c.153-.416.231-.85.231-1.286v-1.286a.75.75 0 0 1 1.5 0v1.286a5.27 5.27 0 0 1-.595 2.422Z"
clipRule="evenodd"
/>
<rect
x="6"
y="4.58618"
width="18.9662"
height="2"
rx="1"
transform="rotate(45 6 4.58618)"
width={18.966}
height={2}
x={6}
y={4.586}
fill="#E94444"
rx={1}
transform="rotate(45 6 4.586)"
/>
</svg>
);
+10 -7
View File
@@ -1,18 +1,21 @@
function MicroOnIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function MicroOnIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
d="M12 17.7857C13.5913 17.7857 15.1174 17.2439 16.2426 16.2794C17.3679 15.3149 18 14.0068 18 12.6429V11.3571M12 17.7857C10.4087 17.7857 8.88258 17.2439 7.75736 16.2794C6.63214 15.3149 6 14.0068 6 12.6429V11.3571M12 17.7857V21M8.25 21H15.75M12 15.2143C11.2044 15.2143 10.4413 14.9434 9.87868 14.4611C9.31607 13.9789 9 13.3248 9 12.6429V5.57143C9 4.88944 9.31607 4.23539 9.87868 3.75315C10.4413 3.27092 11.2044 3 12 3C12.7956 3 13.5587 3.27092 14.1213 3.75315C14.6839 4.23539 15 4.88944 15 5.57143V12.6429C15 13.3248 14.6839 13.9789 14.1213 14.4611C13.5587 14.9434 12.7956 15.2143 12 15.2143Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 17.786c1.591 0 3.117-.542 4.243-1.507C17.368 15.315 18 14.007 18 12.643v-1.286m-6 6.429c-1.591 0-3.117-.542-4.243-1.507C6.632 15.315 6 14.007 6 12.643v-1.286m6 6.429V21m-3.75 0h7.5M12 15.214c-.796 0-1.559-.27-2.121-.753C9.316 13.98 9 13.325 9 12.643V5.57c0-.682.316-1.336.879-1.818C10.44 3.271 11.204 3 12 3s1.559.27 2.121.753c.563.482.879 1.136.879 1.818v7.072c0 .682-.316 1.336-.879 1.818-.562.482-1.325.753-2.121.753Z"
/>
</svg>
);
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import { JSX } from "react/jsx-runtime";
function MobileIcon(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeWidth={1.5}
d="M7.444 3.333h1.111M6 14h4c.736 0 1.333-.597 1.333-1.333V3.333C11.333 2.597 10.736 2 10 2H6c-.737 0-1.333.597-1.333 1.333v9.334C4.667 13.403 5.263 14 6 14Z"
/>
</svg>
);
}
export default MobileIcon;
+39
View File
@@ -0,0 +1,39 @@
import React from "react";
import { JSX } from "react/jsx-runtime";
function Rotate64Icon(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={64}
height={64}
fill="none"
{...props}
>
<g clipPath="url(#a)">
<g clipPath="url(#b)">
<path
stroke="#C4DDF5"
strokeWidth={2}
d="M54.21 22.894a23.89 23.89 0 0 0-7.53-9.88c-10.485-8.108-25.558-6.18-33.666 4.305l-1.631 2.11M9.79 41.105a23.89 23.89 0 0 0 7.53 9.88c10.485 8.109 25.558 6.181 33.666-4.304l1.631-2.11M11.383 19.43l-.957-7.482m.957 7.482 7.481-.957m33.753 26.1-7.481.956m7.481-.957.957 7.482"
/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h64v64H0z" />
</clipPath>
<clipPath id="b">
<path
fill="#fff"
d="m26.26-12.89 50.63 39.15-39.15 50.629-50.63-39.149z"
/>
</clipPath>
</defs>
</svg>
);
}
export default Rotate64Icon;
+39
View File
@@ -0,0 +1,39 @@
import React from "react";
import { JSX } from "react/jsx-runtime";
function RotateIcon(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<g clipPath="url(#a)">
<g clipPath="url(#b)">
<path
stroke="currentColor"
strokeWidth={1.5}
d="M20.33 8.585a9 9 0 0 0-15.448-2.09l-.613.79m-.597 8.13a9 9 0 0 0 15.448 2.09l.612-.79M4.27 7.284 3.91 4.48m.36 2.806 2.805-.359m12.657 9.787-2.805.359m2.805-.359.359 2.806"
/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="currentColor" d="M0 0h24v24H0z" />
</clipPath>
<clipPath id="b">
<path
fill="currentColor"
d="m9.848-4.833 18.986 14.68-14.681 18.986-18.986-14.68z"
/>
</clipPath>
</defs>
</svg>
);
}
export default RotateIcon;
-2
View File
@@ -1,5 +1,3 @@
import React from "react";
function SendChatIcon() {
return (
<svg
+10 -7
View File
@@ -1,18 +1,21 @@
function ShareIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function ShareIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<path
d="M7.57375 11.305C7.36853 10.9357 7.04651 10.6451 6.65823 10.4786C6.26995 10.3122 5.83739 10.2794 5.42845 10.3854C5.01952 10.4914 4.65736 10.7302 4.39882 11.0643C4.14028 11.3984 4 11.8089 4 12.2313C4 12.6538 4.14028 13.0643 4.39882 13.3984C4.65736 13.7325 5.01952 13.9713 5.42845 14.0773C5.83739 14.1833 6.26995 14.1505 6.65823 13.984C7.04651 13.8176 7.36853 13.5269 7.57375 13.1577M7.57375 11.305C7.7263 11.5796 7.8136 11.8949 7.8136 12.2313C7.8136 12.5678 7.7263 12.8839 7.57375 13.1577M7.57375 11.305L15.6812 6.80124M7.57375 13.1577L15.6812 17.6614M15.6812 6.80124C15.8 7.0251 15.9624 7.22302 16.1586 7.38343C16.3549 7.54383 16.5811 7.6635 16.8242 7.73545C17.0672 7.80739 17.3221 7.83015 17.5741 7.80241C17.826 7.77468 18.0699 7.69699 18.2915 7.57389C18.5131 7.4508 18.7079 7.28477 18.8645 7.08551C19.0212 6.88625 19.1365 6.65776 19.2038 6.4134C19.2712 6.16903 19.2891 5.9137 19.2566 5.66233C19.2241 5.41096 19.1418 5.16859 19.0145 4.94939C18.7637 4.51745 18.3534 4.20115 17.8719 4.06849C17.3903 3.93583 16.8759 3.99741 16.4393 4.23998C16.0027 4.48255 15.6787 4.88675 15.5369 5.36569C15.3951 5.84464 15.4469 6.36009 15.6812 6.80124ZM15.6812 17.6614C15.5595 17.8804 15.4822 18.1212 15.4536 18.3701C15.425 18.6189 15.4457 18.871 15.5146 19.1119C15.5834 19.3527 15.699 19.5777 15.8547 19.7738C16.0105 19.97 16.2034 20.1336 16.4223 20.2553C16.6413 20.3769 16.8821 20.4543 17.131 20.4828C17.3798 20.5114 17.6319 20.4907 17.8728 20.4219C18.1136 20.3531 18.3386 20.2375 18.5348 20.0817C18.7309 19.926 18.8945 19.7331 19.0162 19.5141C19.2619 19.0719 19.3218 18.5501 19.1828 18.0637C19.0438 17.5773 18.7173 17.1659 18.275 16.9203C17.8328 16.6746 17.311 16.6146 16.8246 16.7536C16.3382 16.8926 15.9269 17.2192 15.6812 17.6614Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7.574 11.305a1.907 1.907 0 1 0 0 1.853m0-1.853a1.9 1.9 0 0 1 0 1.853m0-1.853 8.107-4.504m-8.107 6.357 8.107 4.503m0-10.86a1.907 1.907 0 1 0 3.368-1.788 1.907 1.907 0 0 0-3.368 1.788Zm0 10.86a1.907 1.907 0 1 0 3.334 1.853 1.907 1.907 0 0 0-3.334-1.853Z"
/>
</svg>
);
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import { JSX } from "react/jsx-runtime";
function Star12Icon(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={12}
height={12}
fill="none"
{...props}
>
<path
fill="#49A1F5"
stroke="#fff"
d="m7.486 3.955-.82-2.524c-.21-.645-1.122-.645-1.332 0l-.82 2.524H1.86c-.678 0-.96.868-.411 1.266l2.147 1.56-.82 2.524c-.21.645.528 1.181 1.077.783L6 8.528l2.147 1.56c.549.398 1.287-.138 1.077-.783l-.82-2.524 2.147-1.56c.549-.398.267-1.266-.411-1.266H7.486Z"
/>
</svg>
);
}
export default Star12Icon;
+12 -15
View File
@@ -1,24 +1,21 @@
function UserIcon() {
import { SVGProps } from "react";
import { JSX } from "react/jsx-runtime";
function UserIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
{...props}
>
<g id="Icon/Person">
<g id="user">
<path
d="M16.6114 6.60385C16.6114 9.14648 14.5502 11.8077 12.0076 11.8077C9.46496 11.8077 7.40376 9.14648 7.40376 6.60385C7.40376 4.06121 9.46496 2 12.0076 2C14.5502 2 16.6114 4.06121 16.6114 6.60385Z"
fill="#F2F2F2"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth={1.5}
d="M11.55 8.635c0 2.007 1.545 4.108 3.45 4.108 1.905 0 3.45-2.101 3.45-4.108C18.45 6.627 16.904 5 15 5c-1.905 0-3.45 1.627-3.45 3.635Zm0 0c0-1.345.694-2.52 1.724-3.148M11.55 8.635c0 .485.09.975.254 1.44M15 14.967c1.782 0 3.23-1.882 4.487-.551.505.534.637.885.96 1.643.246.577.488 1.305.547 2.034.044.533-.167 1.048-.594 1.328-.913.598-1.983.579-5.4.579-3.417 0-4.487.019-5.4-.58-.427-.279-.638-.794-.594-1.327.059-.729.3-1.457.547-2.034M15 14.967c-1.782 0-3.23-1.882-4.487-.551m4.487.551c-.702 0-1.352-.292-1.959-.565-.934-.42-1.767-.793-2.528.014m4.487.551c.255 0 .504-.039.745-.1m-5.232-.451c-.505.534-.637.885-.96 1.643m.96-1.643c-.477.505-.621.846-.908 1.52l-.052.123m0 0c-.236.553-.467 1.243-.539 1.94m5.745-5.267c-.617-.058-1.19-.334-1.676-.75M3.913 7.665c0 1.472 1.158 3.013 2.587 3.013 1.429 0 2.587-1.54 2.587-3.013C9.087 6.193 7.93 5 6.5 5 5.07 5 3.913 6.193 3.913 7.665Zm0 0c0-.986.52-1.847 1.293-2.308M3.913 7.665c0 .356.067.716.19 1.057m6.483 5.388c-.243-.556-.342-.813-.72-1.205-.943-.976-2.03.404-3.366.404m0 0c-1.336 0-2.423-1.38-3.365-.404m3.365.404c-.526 0-1.014-.214-1.47-.414-.7-.308-1.324-.582-1.895.01m3.365.404c.191 0 .378-.028.56-.074m-3.925-.33c-.379.392-.478.65-.72 1.205m.72-1.205c-.358.37-.466.62-.681 1.115l-.04.09m0 0c-.184.423-.365.957-.41 1.491-.032.391.126.77.446.974.685.439 1.487.425 4.05.425 1.184 0 1.993.003 2.587-.037M2.414 14.11c-.176.405-.35.912-.403 1.423M6.32 10.67c-.464-.042-.893-.245-1.258-.55"
/>
<path
d="M12.0077 21.0002C16.5682 21.0002 17.9956 21.0238 19.2145 20.2667C19.7848 19.9124 20.0659 19.26 20.0079 18.5842C19.9287 17.6613 19.6062 16.7395 19.2779 16.0084C18.8468 15.0484 18.6695 14.6039 17.9965 13.9271C16.3197 12.2408 14.3857 14.6252 12.0077 14.6252C9.62965 14.6252 7.69569 12.2408 6.01888 13.9271C5.34585 14.6039 5.1686 15.0484 4.7375 16.0084C4.40916 16.7395 4.08669 17.6613 4.00747 18.5842C3.94947 19.26 4.23057 19.9124 4.80091 20.2667C6.01975 21.0238 7.44722 21.0002 12.0077 21.0002Z"
fill="#F2F2F2"
/>
</g>
</g>
</svg>
);
}
+2 -3
View File
@@ -17,9 +17,8 @@ function AFKTimerModal() {
);
}
async function checkAFK(interval: number) {
async function checkAFK() {
if (afkTimerRef.current && afkTimerRef.current <= 1) {
clearInterval(interval);
await endSession();
window.location.reload();
return;
@@ -30,7 +29,7 @@ function AFKTimerModal() {
useEffect(() => {
const interval = setInterval(() => {
checkAFK(interval);
checkAFK();
}, 1000);
return () => {
+4 -8
View File
@@ -7,6 +7,7 @@ interface ButtonProps {
onlyIcon?: boolean;
disabled?: boolean;
children?: React.ReactNode;
className?: string;
onClick?: () => void;
}
@@ -19,6 +20,7 @@ function Button({
onlyIcon,
disabled,
children,
className,
onClick,
}: ButtonProps) {
const variantClasses = [
@@ -43,23 +45,17 @@ function Button({
<button
disabled={disabled}
type={type}
className={`rounded-lg text-sm font-semibold transition-colors flex items-center justify-center gap-1 ${
className={`rounded-lg text-sm font-semibold transition-colors flex items-center justify-center gap-1 outline-none ${
variantClasses.find((item) => item.name === variant)?.classes
} ${fullWidth ? "w-full" : "w-fit"} ${
large
? `h-10 ${onlyIcon ? "p-2" : icon ? "pl-4 pr-6" : "px-4"}`
: `h-8 ${onlyIcon ? "p-1" : icon ? "pl-2 pr-4" : "px-6"}`
} `}
} ${className}`}
onClick={onClick}
>
{onlyIcon ? (
icon
) : (
<>
{icon}
{children}
</>
)}
</button>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { ChangeEvent } from "react";
interface InputProps {
type?: "text" | "password" | "email";
placeholder?: string;
autoFocus?: boolean;
value?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
}
function Input({
type = "text",
placeholder,
autoFocus,
value,
onChange,
}: InputProps) {
return (
<input
type={type}
placeholder={placeholder}
className="bg-white border border-[#DAE0E5] w-[296px] h-10 px-2 py-2.5 rounded-lg text-sm outline-none"
autoFocus={autoFocus}
value={value}
onChange={(e) => onChange && onChange(e)}
/>
);
}
export default Input;
+28 -14
View File
@@ -1,13 +1,10 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
// the translations
// (tip move them in a JSON file and import them,
// or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files)
const resources = {
ru: {
translation: {
writeAMessage: "Напишите сообщение...",
loading: "Загрузка",
loadingSub: "сек",
rotateDevice: "Поверните устройство",
@@ -37,8 +34,10 @@ const resources = {
title1: "МФК «Revolution towers»",
title2: "ЖК «Life Резиденция»",
title3: "ЖК «Айвазовский City»",
title4: "ЖК «Upside Towers»",
city1: "Россия, Екатеринбург",
city2: "Россия, Тюмень",
city3: "Россия, Москва",
button: "Запустить",
},
},
@@ -123,6 +122,27 @@ const resources = {
},
en: {
translation: {
writeAMessage: "Write a message...",
hello: "Hello", // Здравствуйте
pleaseIntroduceYourself: "Please introduce yourself", // Пожалуйста, представьтесь
communicateWithYou: "This way we will know how to communicate with you", // Так мы будем знать, как к вам обращаться
name: "Name", // Имя
skip: "Skip", // Пропустить (не указывать)
сontinue: "Continue", // Продолжить
partInDiscussion: "Want to take part in the discussion", // Хотите принять участие в обсуждении?
allowMicrophoneUse: "Allow microphone use", // Разрешите использование микрофона
turnOffMicrophone: "You can turn off the microphone at any time", // Выключить микрофон можно в любой момент
connectingToVoiceServer: "Connecting to a voice server", // Подключение к голосовому серверу
pleaseWait: "Please, wait", // Пожалуйста, подождите
connection: "Connection", // Подключение
rotateYourDevice: "Rotate your device", // Поверните устройство
demonstrationCompleted: "This demonstration has been completed", // Данная демонстрация была завершена
allow: "Allow", // Разрешить
members: "Members", // Участники
invite: "Invite", // Пригласить
chat: "Chat", // Чат
scanQRCode: "Scan the QR code<br />to join the demonstration", // Отсканируйте QR-код, чтобы присоединиться к демонстрации
copyLinkToConnect: "Copy link to connect", // Скопировать ссылку для подключения
loading: "Loading",
loadingSub: "sec",
rotateDevice: "Rotate device",
@@ -135,9 +155,7 @@ const resources = {
windowedMode: "Windowed mode",
inviteByQRCode: "Invite by QR Code",
inviteByLink: "Invite by link",
members: "Members",
you: "You",
scanQRCode: "Scan the QR code to connect<br /> to the current demo",
title: "Remote demonstration",
header: {
buttonFirst: "Sign up",
@@ -150,8 +168,10 @@ const resources = {
title1: "Revolution towers",
title2: "Life Residence",
title3: "Aivazovsky City",
title4: "Upside Towers",
city1: "Russia, Yekaterinburg",
city2: "Russia, Tyumen",
city3: "Russia, Moscow",
button: "Run demo",
},
},
@@ -236,18 +256,12 @@ const resources = {
},
};
void i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
void i18n.use(initReactI18next).init({
resources,
fallbackLng: "ru",
interpolation: {
escapeValue: false,
},
// detection: {
// caches: [],
// },
});
});
export default i18n;
+16 -16
View File
@@ -27,22 +27,6 @@ input {
color: transparent;
}
.Toastify__toast-theme--dark {
background: #151619 !important;
font-family: "Inter", sans-serif !important;
font-size: 14px;
}
.Toastify__toast-icon {
width: 40px !important;
height: 40px !important;
background: linear-gradient(23deg, #798fff 16.71%, #d375ff 96.35%) !important;
display: flex;
justify-content: center;
align-items: center;
border-radius: 9999px;
}
.lk-button {
background: #131313 !important;
@apply w-10 h-10 relative outline-none rounded-full shadow-lg shadow-[#131313] text-white opacity-90 flex flex-col justify-center items-center;
@@ -63,3 +47,19 @@ input {
.exited {
opacity: 0;
}
/* Scrollbar */
*::-webkit-scrollbar {
width: 8px;
}
*::-webkit-scrollbar-thumb {
background-color: #858585;
border: 2.5px solid #fff;
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover {
border-width: 2px;
}
+4 -55
View File
@@ -1,27 +1,20 @@
// import React from "react";
import ReactDOM from "react-dom/client";
import {
createBrowserRouter,
Navigate,
RouterProvider,
} from "react-router-dom";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./index.css";
import "./i18n";
import App from "./App";
// import ErrorBoundary from "./ErrorBoundary";
import HistoryPage from "./HistoryPage";
import ScheduledPage from "./ScheduledPage";
import CalendarPage from "./CalendarPage";
import useAuthStore from "./stores/useAuthStore";
import PersonalAreaLoginPage from "./PersonalAreaLoginPage";
import PersonalAreaDashboardPage from "./PersonalAreaDashboardPage";
import StreamPage2 from "./pages/StreamPage2";
import ErrorBoundary from "./ErrorBoundary";
// import StreamPage from "./StreamPage";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorBoundary />,
// errorElement: <ErrorBoundary />,
},
{
path: "/stream/:id",
@@ -35,52 +28,8 @@ const router = createBrowserRouter([
path: "/scheduled/:sessionId",
element: <ScheduledPage />,
},
{
path: "/calendar/:username",
element: <CalendarPage />,
},
{
path: "/personal-area",
element: <Navigate to="/personal-area/login" replace />,
},
{
path: "/personal-area/login",
element: (
<ProtectedRoute>
<PersonalAreaLoginPage />
</ProtectedRoute>
),
},
{
path: "/personal-area/dashboard",
element: (
<ProtectedRoute>
<PersonalAreaDashboardPage />
</ProtectedRoute>
),
},
]);
export function ProtectedRoute({ children }: { children: JSX.Element }) {
const accessToken = useAuthStore((state) => state.accessToken);
if (accessToken) {
if (location.pathname === "/personal-area/login") {
return <Navigate to="/personal-area/dashboard" replace />;
} else {
return <>{children}</>;
}
} else {
if (location.pathname !== "/personal-area/login") {
return <Navigate to="/personal-area/login" replace />;
} else {
return <>{children}</>;
}
}
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
// <React.StrictMode>
<RouterProvider router={router} />
// </React.StrictMode>,
);
+22
View File
@@ -13,3 +13,25 @@
.exited {
opacity: 0;
}
:root {
--toastify-toast-offset: 52px;
}
.Toastify__toast-body {
padding: 0;
margin: 0 4px 0 0;
align-items: normal;
font-family: "Inter", sans-serif;
font-size: 14px;
color: #111c26;
}
.Toastify__toast-icon {
width: auto;
margin-inline-end: 8px;
}
/* LiveKit */
+815 -100
View File
File diff suppressed because it is too large Load Diff
+13 -5
View File
@@ -16,8 +16,12 @@ interface SidebarState {
setPhone: (phone: string) => void;
email: string;
setEmail: (email: string) => void;
companyId: string;
setCompanyId: (buildId: string) => void;
buildId: string;
setBuildId: (buildId: string) => void;
url: string;
setUrl: (url: string) => void;
}
const useSidebarStore = create<SidebarState>()(
@@ -25,21 +29,25 @@ const useSidebarStore = create<SidebarState>()(
// persist(
(set) => ({
isOpen: false,
setIsOpen: (value) => set(() => ({ isOpen: value })),
setIsOpen: (value) => set({ isOpen: value }),
currentTab: 1,
setCurrentTab: (tab) => set(() => ({ currentTab: tab })),
setCurrentTab: (tab) => set({ currentTab: tab }),
selectedDay: null,
setSelectedDay: (day) => set(() => ({ selectedDay: day })),
setSelectedDay: (day) => set({ selectedDay: day }),
selectedTime: null,
setSelectedTime: (time) => set(() => ({ selectedTime: time })),
setSelectedTime: (time) => set({ selectedTime: time }),
name: "",
setName: (name) => set(() => ({ name })),
phone: "",
setPhone: (phone) => set(() => ({ phone })),
email: "",
setEmail: (email) => set(() => ({ email })),
companyId: "653673cd20af0dadee9003e6",
setCompanyId: (companyId) => set({ companyId }),
buildId: "653675f420af0dadee9003ec",
setBuildId: (buildId) => set(() => ({ buildId })),
setBuildId: (buildId) => set({ buildId }),
url: "",
setUrl: (url) => set({ url }),
})
// {
// name: "tab-storage",
+15
View File
@@ -0,0 +1,15 @@
// import { create } from "zustand";
// import { devtools } from "zustand/middleware";
// interface Toast {
// toasts: React.ReactNode[];
// }
// const useToastStore = create<Toast>()(
// devtools((set) => ({
// toasts: [],
// addToast: (toast: React.ReactNode) => set({[...toasts, toast]}),
// }))
// );
// export default useToastStore;
+7
View File
@@ -0,0 +1,7 @@
interface IMessage {
userId: string;
name: string;
text: string;
}
export default IMessage;
+10
View File
@@ -0,0 +1,10 @@
interface IUser {
id: string;
name: string;
device: string;
isAdmin: boolean;
isControlAllowed: boolean;
isMicAllowed: boolean;
}
export default IUser;
+5
View File
@@ -0,0 +1,5 @@
function removeSpaces(value: string) {
return value.replace(/\s+/g, " ").trim();
}
export default removeSpaces;
+674 -542
View File
File diff suppressed because it is too large Load Diff