This commit is contained in:
2023-08-10 14:05:56 +05:00
parent 12ca24ad6a
commit 1dac8116fb
12 changed files with 395 additions and 175 deletions
+14
View File
@@ -1,4 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--
⠟⢦⡀
⢷⡄⠈⡓⠢⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⠤⠂⢹
⠈⡷⡄⠈⠲⢤⣈⠻⠉⠛⠉⠉⠁⠒⠖⠉⠉⠉⠒⠶⢦⣤⠴⠒⢉⣡⠴
⠀⢸⡿⡂⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⡞⠉⠀⢀⣠⡞
⠀⠀⢙⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⢠⡼⡟
⠀⠀⡼⠋⠀⣤⣀⠀⠀⠀⠀⠀⠈⠐⣂⣄⠀⠀⠀⠀⠀⠀⠀⢀⠀⣰⡟⠁
⠀⢠⡇⠀⠀⠘⠛⠃⠀⠀⠀⠀⠾⣿⠿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⢻
⠀⢸⡇⢺⡀⠀⢠⡒⠠⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⡀⠀⠀⠸⡇
⠀⢸⡇⣘⠑⡀⠀⠙⢏⣁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠂⠀⣔⣇
⠀⢸⡇⡁⠀⢳⣶⣾⣷⣦⣄⣀⡀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿
⠀⠸⡇⠁⠀⠀⢏⠉⠀⠀⠙⠛⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢈⡏
⠀⠀⠯⣀⣈⣀⣈⣐⣲⣄⣄⣤⣴⣆⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣈⣛⡧
-->
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
Binary file not shown.
+63 -5
View File
@@ -17,8 +17,10 @@ import useSidebarStore from "./stores/useSidebarStore";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import ky from "ky"; import ky from "ky";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { ToastContainer, toast } from "react-toastify";
import LoaderIcon from "./components/icons/LoaderIcon"; import LoaderIcon from "./components/icons/LoaderIcon";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import AlertIcon from "./components/icons/AlertIcon";
function App() { function App() {
const [isOpen] = useSidebarStore((state) => [state.isOpen]); const [isOpen] = useSidebarStore((state) => [state.isOpen]);
@@ -35,16 +37,19 @@ function App() {
const location = i18n.language === "ru" ? "a1" : "a2"; const location = i18n.language === "ru" ? "a1" : "a2";
const build = searchParams.get("build") || null; const build = searchParams.get("build") || null;
const [setIsOpenSidebar] = useSidebarStore((state) => [state.setIsOpen]);
function toastError(text: string) { function toastError(text: string) {
toast.error(text, { toast.error(text, {
position: "bottom-right", position: "top-center",
autoClose: 5000, autoClose: 2000,
hideProgressBar: false, hideProgressBar: true,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "dark", theme: "dark",
icon: <AlertIcon />,
closeButton: false,
}); });
} }
@@ -265,6 +270,59 @@ function App() {
</div> </div>
</div> </div>
<div className="lg:mt-40 sm:mt-[120px] mt-[88px]">
<div className=" grid xl:grid-cols-3 lg:grid-cols-2 lg:gap-4 gap-8">
<div className="flex flex-col justify-between gap-8 pb-4 border-b border-[#3D425C]">
<div className="flex flex-col xl:gap-8 gap-6">
<p className="2xl:text-[64px] xl:text-5xl text-[40px] text-gradient font-gilroy w-fit leading-none font-medium">
<Trans i18nKey={"signUp.title"}>
Запись на
<br />
удаленную
<br />
демонстрацию
</Trans>
</p>
<p className="2xl:text-base text-sm">
<Trans i18nKey={"signUp.desc"}>
Запись на демонстрацию может быть
<br />
оформлена в виде блока на сайте
<br />
застройщика или жилого комплекса.
</Trans>
</p>
<button
onClick={() => setIsOpenSidebar(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>
<span className="relative">
<Trans i18nKey={"signUp.button"}>Записаться</Trans>
</span>
</button>
</div>
<p className="2xl:text-sm text-xs text-[#52587A]">
<Trans i18nKey={"signUp.notice"}>
Запись доступна в демонстрационном режиме.
<br />
Указанные при записи данные не будут сохранены.
</Trans>
</p>
</div>
<div className="xl:col-span-2">
<video
src="/videos/video.mp4"
playsInline
autoPlay
muted
loop
></video>
</div>
</div>
</div>
<div className="lg:mt-40 sm:mt-[120px] mt-[88px]"> <div className="lg:mt-40 sm:mt-[120px] mt-[88px]">
<div className="grid lg:grid-cols-4 grid-cols-1 lg:gap-4 gap-6"> <div className="grid lg:grid-cols-4 grid-cols-1 lg:gap-4 gap-6">
<div className="col-span-1"> <div className="col-span-1">
+135 -85
View File
@@ -54,94 +54,144 @@ function MonitoringPage() {
}, []); }, []);
return ( return (
<div className="min-h-screen text-[#F2F2F2] p-4 gap-4 flex flex-col"> <div className="min-h-screen text-[#F2F2F2] p-4 flex flex-col">
{servers.map((server: any) => ( <div className="flex flex-col gap-2 border-b border-[#22222A] pb-4 text-sm">
<div key={server.id} className="p-4 bg-[#22222A]"> <p>Сервера:</p>
<div> <div className="grid lg:grid-cols-3 gap-2">
{differenceInSeconds(new Date(), parseISO(server.updatedAt)) >= {servers.length > 0 ? (
10 ? ( servers.map((server: any) => (
<p className="flex items-center gap-2"> <div key={server.id} className="p-4 bg-[#22222A] rounded">
<span className="h-2 w-2 rounded-full bg-red-500"></span> <div>
<span>Не в сети</span> {differenceInSeconds(
</p> new Date(),
) : ( parseISO(server.updatedAt)
<p className="flex items-center gap-2"> ) >= 10 ? (
<span className="h-2 w-2 rounded-full bg-green-500"></span> <p className="flex items-center gap-2">
<span>В сети</span> <span className="h-2 w-2 rounded-full bg-red-500"></span>
</p> <span>Не в сети</span>
)} </p>
</div> ) : (
<p>Локация: "{server.location}"</p> <p className="flex items-center gap-2">
<p>Имя сервера: "{server.name}"</p> <span className="h-2 w-2 rounded-full bg-green-500"></span>
<p>Лимит процессов: {server.limit_process}</p> <span>В сети</span>
<div> </p>
<p>CPU: {server.cpu}</p> )}
<p>RAM: {server.ram}</p> </div>
<p className="flex space-x-2"> <p>
<span>GPU: </span> Локация:{" "}
<span className="flex space-x-4"> <span className="font-medium">{server.location}</span>
{server.gpu?.map((item: any, index: number) => ( </p>
<span key={index}> <p>
{item} {index === 2 && "°C"} Имя сервера:{" "}
</span> <span className="font-medium">{server.name}</span>
))} </p>
</span> <p>
</p> Лимит процессов:{" "}
</div> <span className="font-medium">{server.limit_process}</span>
</p>
<div>
<p>
CPU: <span className="font-medium">{server.cpu}</span>
</p>
<p>
RAM: <span className="font-medium">{server.ram}</span>
</p>
{/* <p className="flex space-x-2">
<span>GPU: </span>
<span className="flex space-x-4">
{server.gpu?.map((item: any, index: number) => (
<span key={index}>
{item} {index === 2 && "°C"}
</span>
))}
</span>
</p> */}
</div>
</div>
))
) : (
<p className="opacity-50">Нет запущенных серверов</p>
)}
</div> </div>
))} </div>
{sessions.map((session: any) => ( <div className="mt-4 flex flex-col gap-2 text-sm">
<div <p>Запущенные сессии:</p>
key={session.id} {sessions.length > 0 ? (
className="flex flex-wrap gap-4 justify-between items-center p-4 bg-[#22222A] rounded" sessions.map((session: any) => (
> <div
<div className="flex gap-4"> key={session.id}
<div className=""> className="flex flex-wrap gap-4 justify-between items-center p-4 bg-[#22222A] rounded"
<QRCode >
bgColor="#22222A" <div className="flex items-center gap-4">
fgColor="#F2F2F2" <div className="">
size={128} <QRCode
value={`https://stream.graff.tech/stream/?data=wss://${session.location}.sess.stream.graff.tech/${session.server}/${session.cirrusPort}/`} bgColor="#22222A"
viewBox={`0 0 128 128`} fgColor="#F2F2F2"
/> size={128}
</div> value={`https://stream.graff.tech/stream/?data=wss://${session.location}.sess.stream.graff.tech/${session.server}/${session.cirrusPort}/`}
<div className="sm:text-base text-sm"> viewBox={`0 0 128 128`}
<p>Локация: "{session.location}"</p> />
<p>Сервер: "{session.server}"</p> </div>
<p>Сборка: "{session.title}"</p> <div>
<p>Порт: {session.cirrusPort}</p> <p>
<p>Пользователи: {session.connectedPlayersCount || 0}</p> Локация:{" "}
<p> <span className="font-medium">{session.location}</span>
Время запуска: {new Date(session.createdAt).toLocaleString()} </p>
</p> <p>
</div> Сервер:{" "}
</div> <span className="font-medium">{session.server}</span>
</p>
<p>
Сборка: <span className="font-medium">{session.title}</span>
</p>
<p>
Порт:{" "}
<span className="font-medium">{session.cirrusPort}</span>
</p>
<p>
Пользователи:{" "}
<span className="font-medium">
{session.connectedPlayersCount || 0}
</span>
</p>
<p>
Дата и время запуска:{" "}
<span className="font-medium">
{new Date(session.createdAt).toLocaleString()}
</span>
</p>
</div>
</div>
<div className="space-x-4"> <div className="grid lg:grid-cols-1 grid-cols-2 gap-4 lg:w-auto w-full">
<a <a
href={`https://stream.graff.tech/stream/${session.id}`} href={`https://stream.graff.tech/stream/${session.id}`}
target="_blank" target="_blank"
className="inline-block px-4 py-2 bg-blue-600 hover:bg-blue-500 transition-colors rounded" className="inline-block text-center px-4 py-2 bg-blue-600 hover:bg-blue-500 transition-colors rounded"
> >
Открыть в новом окне Открыть в новом окне
</a> </a>
<button <button
onClick={() => onClick={() =>
endActiveSession( endActiveSession(
session.location, session.location,
session.server, session.server,
session.uePort, session.uePort,
session.cirrusPort session.cirrusPort
) )
} }
className="px-4 py-2 bg-red-600 hover:bg-red-500 transition-colors rounded" className="px-4 py-2 bg-red-600 hover:bg-red-500 transition-colors rounded"
> >
Завершить сессию Завершить сессию
</button> </button>
</div> </div>
</div> </div>
))} ))
) : (
<p className="opacity-50">Нет запущенных сессий</p>
)}
</div>
</div> </div>
); );
} }
+42 -16
View File
@@ -16,14 +16,18 @@ import ModalContainer from "./components/ModalContainer";
import useModalStore from "./stores/useModalStore"; import useModalStore from "./stores/useModalStore";
import ShareModal from "./components/modals/ShareModal"; import ShareModal from "./components/modals/ShareModal";
import { Transition } from "react-transition-group"; import { Transition } from "react-transition-group";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import FullscreenIcon from "./components/icons/FullscreenIcon"; import FullscreenIcon from "./components/icons/FullscreenIcon";
import WindowedModeIcon from "./components/icons/WindowedModeIcon"; import WindowedModeIcon from "./components/icons/WindowedModeIcon";
import QRIcon from "./components/icons/QRIcon"; import QRIcon from "./components/icons/QRIcon";
import QRCodeModal from "./components/modals/QRCodeModal"; import QRCodeModal from "./components/modals/QRCodeModal";
import PersonsIcon from "./components/icons/PersonsIcon"; import PersonsIcon from "./components/icons/PersonsIcon";
import UsersManagementModal from "./components/modals/UsersManagementModal"; import UsersManagementModal from "./components/modals/UsersManagementModal";
import useStreamUserStore from "./stores/useStreamUserStore";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import UserIcon from "./components/icons/UserIcon";
import HandOnIcon from "./components/icons/HandOnIcon";
import AlertIcon from "./components/icons/AlertIcon";
function StreamPage() { function StreamPage() {
const params = useParams(); const params = useParams();
@@ -32,7 +36,10 @@ function StreamPage() {
const [isStreamEnded, setIsStreamEnded] = useState<boolean>(false); const [isStreamEnded, setIsStreamEnded] = useState<boolean>(false);
const [isStreamLoaded, setStreamLoaded] = useState<boolean>(false); const [isStreamLoaded, setStreamLoaded] = useState<boolean>(false);
const [socket, setSocket] = useState<any>(null); const [socket, setSocket] = useState<any>(null);
const [users, setUsers] = useState<any>([]); const [users, setUsers] = useStreamUserStore((state) => [
state.users,
state.setUsers,
]);
const [me, setMe] = useState<any>({}); const [me, setMe] = useState<any>({});
// const screen = useScreen(); // const screen = useScreen();
@@ -49,27 +56,43 @@ function StreamPage() {
function toastWarn(text: string) { function toastWarn(text: string) {
toast.warn(text, { toast.warn(text, {
position: "bottom-right", position: "top-center",
autoClose: 5000, autoClose: 2000,
hideProgressBar: false, hideProgressBar: true,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "dark", theme: "dark",
icon: <AlertIcon />,
closeButton: false,
}); });
} }
function toastInfo(text: string) { function toastUser(text: string) {
toast.info(text, { toast.info(text, {
position: "bottom-right", position: "top-center",
autoClose: 5000, autoClose: 2000,
hideProgressBar: false, hideProgressBar: true,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "dark", theme: "dark",
icon: <UserIcon />,
closeButton: false,
});
}
function toastHandOn(text: string) {
toast.info(text, {
position: "top-center",
autoClose: 2000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
theme: "dark",
icon: <HandOnIcon />,
closeButton: false,
}); });
} }
@@ -113,7 +136,7 @@ function StreamPage() {
setUsers(room.users); setUsers(room.users);
// console.log("join: ", _socketId, room.users); // console.log("join: ", _socketId, room.users);
toastInfo("Присоеденился новый участник"); toastUser("Присоеденился новый участник");
}); });
socket.on("update", (_socketId, room) => { socket.on("update", (_socketId, room) => {
@@ -126,7 +149,11 @@ function StreamPage() {
}); });
socket.on("leave", (socketId) => { socket.on("leave", (socketId) => {
setUsers((prev: any) => prev.filter((user: any) => user.id !== socketId)); setUsers(
useStreamUserStore
.getState()
.users.filter((user: any) => user.id !== socketId)
);
}); });
return () => { return () => {
@@ -140,7 +167,7 @@ function StreamPage() {
useEffect(() => { useEffect(() => {
if (me && me.allowControl && !me.admin) { if (me && me.allowControl && !me.admin) {
toastInfo("Управление получено"); toastHandOn("Управление получено");
} }
}, [me]); }, [me]);
@@ -234,7 +261,6 @@ function StreamPage() {
onClick={() => onClick={() =>
setModal( setModal(
<UsersManagementModal <UsersManagementModal
users={users}
me={me} me={me}
handleUpdate={(socketId, params) => update(socketId, params)} handleUpdate={(socketId, params) => update(socketId, params)}
handleKick={(socketId) => kick(socketId)} handleKick={(socketId) => kick(socketId)}
+2 -15
View File
@@ -1,15 +1,14 @@
import { Trans } from "react-i18next";
import LogoIcon from "./icons/LogoIcon"; import LogoIcon from "./icons/LogoIcon";
import LogoMobileIcon from "./icons/LogoMobileIcon"; import LogoMobileIcon from "./icons/LogoMobileIcon";
import i18n from "../i18n"; import i18n from "../i18n";
import useSidebarStore from "../stores/useSidebarStore"; // import useSidebarStore from "../stores/useSidebarStore";
interface HeaderProps { interface HeaderProps {
handleChangeLang: (lang: string) => void; handleChangeLang: (lang: string) => void;
} }
function Header({ handleChangeLang }: HeaderProps) { function Header({ handleChangeLang }: HeaderProps) {
const [setIsOpen] = useSidebarStore((state) => [state.setIsOpen]); // const [setIsOpen] = useSidebarStore((state) => [state.setIsOpen]);
return ( return (
<header className="sm:py-6 py-4 flex justify-between"> <header className="sm:py-6 py-4 flex justify-between">
@@ -20,18 +19,6 @@ function Header({ handleChangeLang }: HeaderProps) {
<LogoMobileIcon /> <LogoMobileIcon />
</a> </a>
<div className="flex sm:gap-8 gap-2"> <div className="flex sm:gap-8 gap-2">
<button
onClick={() => setIsOpen(true)}
className="group relative sm:px-8 px-6 py-2 bg-gradient rounded-full lg:text-base text-sm font-medium leading-normal"
>
<div className="absolute top-0 left-0 w-full h-full rounded-full bg-black opacity-0 group-hover:opacity-10 transition-all"></div>
<span className="relative">
<Trans i18nKey={"header.buttonFirst"}>Записаться</Trans>{" "}
<span className="sm:inline hidden">
<Trans i18nKey={"header.buttonSecond"}>на демонстрацию</Trans>
</span>
</span>
</button>
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
className={[ className={[
+30
View File
@@ -0,0 +1,30 @@
function AlertIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Icon/Alert">
<path
id="Ellipse 224"
d="M21 12C21 13.1819 20.7672 14.3522 20.3149 15.4442C19.8626 16.5361 19.1997 17.5282 18.364 18.364C17.5282 19.1997 16.5361 19.8626 15.4442 20.3149C14.3522 20.7672 13.1819 21 12 21C10.8181 21 9.64778 20.7672 8.55585 20.3149C7.46392 19.8626 6.47177 19.1997 5.63604 18.364C4.80031 17.5282 4.13738 16.5361 3.68508 15.4442C3.23279 14.3522 3 13.1819 3 12C3 10.8181 3.23279 9.64778 3.68508 8.55585C4.13738 7.46392 4.80031 6.47177 5.63604 5.63604C6.47177 4.80031 7.46392 4.13738 8.55585 3.68508C9.64778 3.23279 10.8181 3 12 3C13.1819 3 14.3522 3.23279 15.4442 3.68508C16.5361 4.13738 17.5282 4.80031 18.364 5.63604C19.1997 6.47177 19.8626 7.46392 20.3149 8.55585C20.7672 9.64778 21 10.8181 21 12L21 12Z"
stroke="#F2F2F2"
strokeWidth="2"
/>
<path
id="Vector 1834"
d="M12 7V13"
stroke="#F2F2F2"
strokeWidth="2"
strokeLinecap="round"
/>
<circle id="Ellipse 225" cx="12" cy="17" r="1" fill="#F2F2F2" />
</g>
</svg>
);
}
export default AlertIcon;
+6 -4
View File
@@ -4,6 +4,7 @@ import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
import useModalStore from "../../stores/useModalStore"; import useModalStore from "../../stores/useModalStore";
import CloseIcon from "../icons/CloseIcon"; import CloseIcon from "../icons/CloseIcon";
import AlertIcon from "../icons/AlertIcon";
function ShareModal() { function ShareModal() {
const [setModal] = useModalStore((state) => [state.setModal]); const [setModal] = useModalStore((state) => [state.setModal]);
@@ -11,14 +12,15 @@ function ShareModal() {
function toastInfo(text: string) { function toastInfo(text: string) {
toast.info(text, { toast.info(text, {
position: "bottom-right", position: "top-center",
autoClose: 5000, autoClose: 2000,
hideProgressBar: false, hideProgressBar: true,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined,
theme: "dark", theme: "dark",
icon: <AlertIcon />,
closeButton: false,
}); });
} }
+54 -48
View File
@@ -8,80 +8,86 @@ import HandOnIcon from "../icons/HandOnIcon";
import MobilePhoneIcon from "../icons/MobilePhoneIcon"; import MobilePhoneIcon from "../icons/MobilePhoneIcon";
import UserIcon from "../icons/UserIcon"; import UserIcon from "../icons/UserIcon";
import CloseIcon from "../icons/CloseIcon"; import CloseIcon from "../icons/CloseIcon";
import useStreamUserStore from "../../stores/useStreamUserStore";
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
interface UsersManagementModalProps { interface UsersManagementModalProps {
users: any[];
me: any; me: any;
handleUpdate: (socketId: string, params: any) => void; handleUpdate: (socketId: string, params: any) => void;
handleKick: (socketId: string) => void; handleKick: (socketId: string) => void;
} }
function UsersManagementModal({ function UsersManagementModal({
users,
me, me,
handleUpdate, handleUpdate,
handleKick, handleKick,
}: UsersManagementModalProps) { }: UsersManagementModalProps) {
const [setModal] = useModalStore((state) => [state.setModal]); const [setModal] = useModalStore((state) => [state.setModal]);
const [users] = useStreamUserStore((state) => [state.users]);
return ( return (
<div className="relative lg:p-10 p-6 bg-[#131317] rounded shadow-lg w-[320px] flex flex-col gap-2"> <div className="relative lg:p-10 p-6 bg-[#131317] rounded shadow-lg w-[320px] flex flex-col gap-2">
{users.map((user: any, index: number) => ( <div className="flex flex-col lg:gap-8 gap-4">
<div key={index} className="relative"> <p className="font-gilroy lg:text-2xl">Участники</p>
<div className="flex items-center gap-2 text-white"> <div className="flex flex-col gap-2">
<div className="flex flex-col"> {users.map((user: any, index: number) => (
{user.admin ? <AdminUserIcon /> : <UserIcon />} <div key={index} className="relative">
<div className="flex items-center gap-2 text-white">
<div className="relative flex flex-col">
{user.admin ? <AdminUserIcon /> : <UserIcon />}
</div>
{me && me.id === user.id && ( {new userAgentParser(user.ua).getDevice().type !== "mobile" ? (
<span className="text-[#73788C] text-sm">Вы</span> <DesktopIcon />
)}
</div>
{new userAgentParser().getDevice().type !== "mobile" ? (
<DesktopIcon />
) : (
<MobilePhoneIcon />
)}
<span className="text-sm">{user.city}</span>
{!user.admin && (
<>
{!user.allowControl ? (
<button
onClick={() =>
handleUpdate(user.id, { allowControl: true })
}
className="outline-none"
>
<HandOffIcon />
</button>
) : ( ) : (
<MobilePhoneIcon />
)}
<span className="text-sm">{user.city}</span>
{!user.admin && (
<>
{!user.allowControl ? (
<button
onClick={() =>
handleUpdate(user.id, { allowControl: true })
}
className="outline-none"
>
<HandOffIcon />
</button>
) : (
<button
onClick={() =>
handleUpdate(user.id, { allowControl: false })
}
className="outline-none"
>
<HandOnIcon />
</button>
)}
</>
)}
{me && me.admin && !user.admin && (
<button <button
onClick={() => onClick={() => handleKick(user.id)}
handleUpdate(user.id, { allowControl: false })
}
className="outline-none" className="outline-none"
> >
<HandOnIcon /> <ExitIcon />
</button> </button>
)} )}
</>
)}
{me && me.admin && !user.admin && ( <div>
<button {me && me.id === user.id && (
onClick={() => handleKick(user.id)} <span className="text-[#73788C] text-xs">Вы</span>
className="outline-none" )}
> </div>
<ExitIcon /> </div>
</button> </div>
)} ))}
</div>
<div className="relative"></div>
</div> </div>
))} </div>
<button <button
onClick={() => setModal(null)} onClick={() => setModal(null)}
+16 -2
View File
@@ -38,6 +38,13 @@ const resources = {
button: "Запустить", button: "Запустить",
}, },
}, },
signUp: {
title: "Запись на<br />удаленную<br />демонстрацию",
desc: "Запись на демонстрацию может быть<br />оформлена в виде блока на сайте<br />застройщика или жилого комплекса.",
button: "Записаться",
notice:
"Запись доступна в демонстрационном режиме.<br />Указанные при записи данные не будут сохранены.",
},
feedback: { feedback: {
title: "Свяжитесь<br />с нами", title: "Свяжитесь<br />с нами",
desc: "Хотите увеличить конверсию?<br />Давайте обсудим детали!", desc: "Хотите увеличить конверсию?<br />Давайте обсудим детали!",
@@ -131,6 +138,13 @@ const resources = {
button: "Run demo", button: "Run demo",
}, },
}, },
signUp: {
title: "Sign up for a<br />remote demo",
desc: "Registration for a demonstration can be<br />designed as a block on the site<br />developer or residential complex.",
button: "Sign up",
notice:
"The recording is available in demo mode.<br />The data specified during recording will not be saved.",
},
feedback: { feedback: {
title: "Contact us", title: "Contact us",
desc: "Want to increase conversion?<br />Let's discuss the details!", desc: "Want to increase conversion?<br />Let's discuss the details!",
@@ -200,11 +214,11 @@ void i18n
.use(initReactI18next) // passes i18n down to react-i18next .use(initReactI18next) // passes i18n down to react-i18next
.init({ .init({
resources, resources,
fallbackLng: ["ru", "en"], fallbackLng: ["ru"],
interpolation: { interpolation: {
escapeValue: false, // react already safes from xss escapeValue: false, // react already safes from xss
}, },
debug: true, // debug: true,
}); });
export default i18n; export default i18n;
+16
View File
@@ -26,3 +26,19 @@ input {
background-clip: text; background-clip: text;
color: transparent; 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;
}
+17
View File
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { create } from "zustand";
import { devtools } from "zustand/middleware";
interface StreamUserState {
users: any[];
setUsers: (users: any[]) => void;
}
const useStreamUserStore = create<StreamUserState>()(
devtools((set) => ({
users: [],
setUsers: (users) => set({ users }),
}))
);
export default useStreamUserStore;