This commit is contained in:
2025-03-24 12:24:58 +05:00
parent 15dbaab168
commit 143ba63e09
11 changed files with 232 additions and 134 deletions
+27
View File
@@ -0,0 +1,27 @@
import { useEffect, useState } from "react";
interface Props {
options: string[];
customOption?: React.ReactNode;
}
function CustomSelect({ options, customOption }: Props) {
const [selectedOption, setSelectedOption] = useState<string | null>(null);
// Сбрасываем selectedOption при изменении options
useEffect(() => {
setSelectedOption(null);
}, [options]);
return (
<div>
{options.map((option, index) => (
<div key={index} onClick={() => setSelectedOption(option)}>
{customOption ? customOption(option) : option}
</div>
))}
</div>
);
}
export default CustomSelect;
+20 -12
View File
@@ -4,25 +4,28 @@ import MoreIcon from "./icons/MoreIcon";
import api from "../utils/api";
import PlusIcon from "./icons/PlusIcon";
import { IServer } from "../types/IServer";
import useModalStore from "../stores/useModalStore";
import CreateSessionModal from "./modals/CreateSessionModal";
interface IDesktopCardProps {
server: IServer;
}
export default function DesktopCard({ server }: IDesktopCardProps) {
const { setModal, setPosition } = useModalStore();
const queryClient = useQueryClient();
const { mutate: createSession } = useMutation({
mutationFn: () =>
api.post(`sessions`, {
json: {
serverId: server.id,
clientId: "abcfd570-2fa8-4f55-957b-5007f84f8f96",
appId: "b8a9995c-a799-4593-8f96-03942050cb21",
},
}),
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
});
// const { mutate: createSession } = useMutation({
// mutationFn: () =>
// api.post(`sessions`, {
// json: {
// serverId: server.id,
// clientId: "abcfd570-2fa8-4f55-957b-5007f84f8f96",
// appId: "b8a9995c-a799-4593-8f96-03942050cb21",
// },
// }),
// onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
// });
const { mutate: endSession } = useMutation({
mutationKey: ["sessions", server.sessions?.[0]?.id],
@@ -33,6 +36,11 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
});
async function handleClickCreateSession() {
setPosition("right");
setModal(<CreateSessionModal servers={[server]} />);
}
return (
<div className="p-[1.111vw] rounded-[0.833vw] flex flex-col justify-between gap-[1.111vw] aspect-[272/149] w-[25.208vw] border border-[#00000014]">
<div className="flex justify-between items-start">
@@ -102,7 +110,7 @@ export default function DesktopCard({ server }: IDesktopCardProps) {
<Button
variant="primary"
className="bg-[#798FFF] w-full rounded-[0.556vw]"
onClick={() => createSession()}
onClick={handleClickCreateSession}
>
<div className="flex gap-1 items-center">
<span className="w-[1.389vw] h-[1.389vw]">
+26 -23
View File
@@ -1,18 +1,17 @@
import { useState } from 'react';
import { IServer } from '../types/IServer';
import Button from './Button';
import { useClickAway } from '@uidotdev/usehooks';
import ArrowDownIcon from './icons/ArrowDownIcon';
import { useState } from "react";
import { IServer } from "../types/IServer";
import Button from "./Button";
import { useClickAway } from "@uidotdev/usehooks";
import ArrowDownIcon from "./icons/ArrowDownIcon";
interface Props {
servers: IServer[];
value: IServer;
value: IServer | undefined;
onChange: (server: IServer) => void;
}
export default function DesktopSelect({ servers, value, onChange }: Props) {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickAway<HTMLDivElement>(() => setIsOpen(false));
return (
@@ -27,22 +26,26 @@ export default function DesktopSelect({ servers, value, onChange }: Props) {
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex gap-[0.278vw] items-center">
<div
className="rounded-full w-[0.417vw] aspect-square"
style={{
backgroundColor:
value.sessions &&
value.sessions?.length > 0 &&
value.sessions[value.sessions.length - 1].status === 'started'
? '#EF3C26'
: '#108C33',
}}
/>
<p className="text-[0.972vw] leading-[115%]">{value.name}</p>
{value?.name && (
<div
className="rounded-full w-[0.417vw] aspect-square"
style={{
backgroundColor:
value?.sessions &&
value.sessions?.length > 0 &&
value.sessions[value.sessions.length - 1].status === "started"
? "#EF3C26"
: "#108C33",
}}
/>
)}
<p className="text-[0.972vw] leading-[115%]">
{value?.name || "Выберите из списка"}
</p>
</div>
<div
className={`w-[1.389vw] transition-transform h-[1.389vw]${
isOpen ? ' rotate-180' : ''
isOpen ? " rotate-180" : ""
}`}
>
<ArrowDownIcon />
@@ -69,9 +72,9 @@ export default function DesktopSelect({ servers, value, onChange }: Props) {
server.sessions &&
server.sessions?.length > 0 &&
server.sessions[server.sessions.length - 1].status ===
'started'
? '#EF3C26'
: '#108C33',
"started"
? "#EF3C26"
: "#108C33",
}}
/>
<p className="text-[0.972vw] leading-[115%]">{server.name}</p>
+30 -15
View File
@@ -1,19 +1,35 @@
import { useState } from 'react';
import Button from './Button';
import { useClickAway } from '@uidotdev/usehooks';
import ArrowDownIcon from './icons/ArrowDownIcon';
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import Button from "./Button";
import { useClickAway } from "@uidotdev/usehooks";
import ArrowDownIcon from "./icons/ArrowDownIcon";
interface Props {
options: string[];
value: string;
onChange: (option: string) => void;
options: string[]; // ["StroyProject"]
onChange: (option: string | undefined) => void;
}
export default function Select({ options, value, onChange }: Props) {
export default function Select({ options, onChange }: Props) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | undefined>();
const ref = useClickAway<HTMLDivElement>(() => setIsOpen(false));
function handleClickOption(option: string) {
setSelectedOption(option);
onChange(option);
setIsOpen(false);
}
useEffect(() => {
console.log(selectedOption);
}, [selectedOption]);
useEffect(() => {
if (selectedOption && !options.includes(selectedOption)) {
setSelectedOption(undefined);
}
}, [options]);
return (
<div
ref={ref}
@@ -25,10 +41,12 @@ export default function Select({ options, value, onChange }: Props) {
className="px-[0.833vw] py-[0.417vw] !justify-between w-full"
onClick={() => setIsOpen(!isOpen)}
>
<p className="text-[0.972vw] leading-[115%]">{value}</p>
<p className="text-[0.972vw] leading-[115%]">
{selectedOption || "Выберите из списка"}
</p>
<div
className={`w-[1.389vw] transition-transform h-[1.389vw]${
isOpen ? ' rotate-180' : ''
isOpen ? "rotate-180" : ""
}`}
>
<ArrowDownIcon />
@@ -42,10 +60,7 @@ export default function Select({ options, value, onChange }: Props) {
onlyIcon
variant="tertiary"
className="px-[0.833vw] py-[0.417vw] !justify-start w-full !first:rounded-t-[0.556vw] !last:rounded-b-[0.556vw]"
onClick={() => {
onChange(option);
setIsOpen(false);
}}
onClick={() => handleClickOption(option)}
>
<div className="flex gap-[0.278vw] items-center">
<p className="text-[0.972vw] leading-[115%]">{option}</p>
+107 -55
View File
@@ -1,43 +1,74 @@
import { useEffect } from 'react';
import { useState } from 'react';
import { IServer } from '../../types/IServer.ts';
import Button from '../Button.tsx';
import DesktopSelect from '../DesktopSelect.tsx';
import Input from '../Input.tsx';
import DisplayIcon from '../icons/DisplayIcon.tsx';
import Select from '../Select.tsx';
import { useQueryClient } from '@tanstack/react-query';
import { IUser } from '../../types/IUser.ts';
import { IApp } from '../../types/IApp.ts';
import { IServer } from "../../types/IServer.ts";
import Button from "../Button.tsx";
import DesktopSelect from "../DesktopSelect.tsx";
import Input from "../Input.tsx";
import DisplayIcon from "../icons/DisplayIcon.tsx";
import Select from "../Select.tsx";
import { useState } from "react";
import { IApp } from "../../types/IApp.ts";
import api from "../../utils/api.ts";
import { ISession } from "../../types/ISession.ts";
import { IClient } from "../../types/IClient.ts";
import useModalStore from "../../stores/useModalStore.ts";
interface Props {
servers: IServer[];
}
export default function CreateSessionModal({ servers }: Props) {
const queryClient = useQueryClient();
const { setModal } = useModalStore();
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [selectedServer, setSelectedServer] = useState<IServer>();
const [selectedApp, setSelectedApp] = useState<IApp>();
const user = queryClient.getQueryData<IUser>(['me']);
async function createClient() {
console.log(name, phone, email);
const [selectedServer, setSelectedServer] = useState<IServer | undefined>(
servers.find(
({ sessions }) =>
!sessions || !sessions.length || sessions[0].status === 'ended'
)
);
return await api
.post("clients", {
json: {
name,
phone,
email,
},
})
.json<IClient>();
}
const [selectedApp, setSelectedApp] = useState<IApp | undefined>(
user?.company.apps.filter((app) => app.serverId === selectedServer?.id)[0]
);
async function createSession(clientId: string) {
return await api
.post("sessions", {
json: {
clientId,
serverId: selectedServer?.id,
appId: selectedApp?.id,
},
})
.json<ISession>();
}
useEffect(() => {
setSelectedApp(
user?.company.apps.filter((app) => app.serverId === selectedServer?.id)[0]
);
}, [selectedServer, user?.company.apps]);
async function handleClickCreateSession(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!name || !phone || !selectedServer || !selectedApp) return;
try {
const client = await createClient();
await createSession(client.id);
setModal(null);
} catch (error) {
console.log(error);
}
}
return (
<div className="w-[34.375vw] rounded-[0.833vw] bg-white p-[1.667vw] flex flex-col min-h-[calc(100dvh-0.972vw*2)] justify-between gap-[1.111vw]">
<form
className="w-[34.375vw] rounded-[0.833vw] bg-white p-[1.667vw] flex flex-col min-h-[calc(100dvh-0.972vw*2)] justify-between gap-[1.111vw]"
onSubmit={handleClickCreateSession}
>
<div className="gap-y-[1.111vw] flex flex-col justify-between">
<div className="space-y-[0.556vw]">
<div className="p-[0.833vw] ring-[0.069vw] ring-[#E6E6E6] w-fit rounded-[0.556vw]">
@@ -56,7 +87,12 @@ export default function CreateSessionModal({ servers }: Props) {
Имя <span className="text-[#C6C6C699]">*</span>
</p>
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
<Input placeholder="Константин" required />
<Input
placeholder="Константин"
required
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
<div className="flex justify-between items-center">
@@ -64,60 +100,76 @@ export default function CreateSessionModal({ servers }: Props) {
Номер <span className="text-[#C6C6C699]">*</span>
</p>
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
<Input placeholder="+ 7 (999) 99 99 99" required type="tel" />
<Input
placeholder="79221234567"
required
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
</div>
<div className="flex justify-between items-center">
<p className="text-[0.972vw]">Электронная почта</p>
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
<Input placeholder="sample@mail.ru" type="email" />
<Input
placeholder="sample@mail.ru"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
</div>
<div className="flex flex-col gap-y-[0.556vw]">
<div className="flex justify-between items-center">
<p className="text-[0.972vw]">Стол</p>
{selectedServer && (
<DesktopSelect
servers={servers}
value={selectedServer}
onChange={setSelectedServer}
/>
)}
<DesktopSelect
servers={servers.filter(
({ sessions }) =>
!sessions ||
!sessions.length ||
sessions[0]?.status === "ended"
)}
value={selectedServer || undefined}
onChange={setSelectedServer}
/>
</div>
<p className="text-[0.694vw] text-black/30 w-[13.889vw] leading-[115%] self-end">
При запуске нового сеанса текущий будет завершен принудительно.
</p>
</div>
{user && selectedServer && (
{selectedServer && (
<div className="flex justify-between items-center">
<p className="text-[0.972vw]">Проекты</p>
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
{selectedApp && (
<Select
options={user.company.apps
.filter((app) => app.serverId === selectedServer.id)
.map((app) => app.name)}
value={selectedApp?.name}
onChange={(option) =>
setSelectedApp(
user.company.apps.find((app) => app.name === option)
)
}
/>
)}
<Select
options={selectedServer.apps?.map((app) => app.name) || []}
onChange={(option) =>
setSelectedApp(
selectedServer.apps?.find((app) => app.name === option)
)
}
/>
</div>
</div>
)}
<hr className="border-black/10" />
</div>
<div className="flex justify-between">
<Button className="bg-[#F9F9F9] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]">
<Button
type="button"
className="bg-[#F9F9F9] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]"
onClick={() => setModal(null)}
>
<p className="text-black font-medium text-[0.972vw]">Отменить</p>
</Button>
<Button className="bg-[#EF3C26] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]">
<Button
type="submit"
className="bg-[#EF3C26] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]"
>
<p className="text-[0.972vw] font-medium">Запустить сеанс</p>
</Button>
</div>
</div>
</form>
);
}
+3 -15
View File
@@ -4,14 +4,11 @@ import api from "../utils/api";
import Sidebar from "../components/Sidebar";
import { IServer } from "../types/IServer";
import DesktopCard from "../components/DesktopCard";
import Button from "../components/Button";
import useModalStore from "../stores/useModalStore";
import CreateSessionModal from "../components/modals/CreateSessionModal";
function DashboardPage() {
const { data: me } = useQuery({
queryKey: ["me"],
queryFn: () => api.get("users/me").json<IUser>(),
queryFn: () => api.get("auth/me").json<IUser>(),
});
const { data: servers } = useQuery({
@@ -21,13 +18,6 @@ function DashboardPage() {
refetchInterval: 1000,
});
const { setModal, setPosition } = useModalStore();
function handleClickCreateSession() {
setPosition("right");
setModal(<CreateSessionModal servers={servers || []} />);
}
// async function logout() {
// return await api.get("auth/logout").json();
// }
@@ -42,7 +32,7 @@ function DashboardPage() {
// }
return (
<div className="p-[0.833vw] flex gap-[0.833vw] h-dvh">
<div className="p-[0.833vw] flex gap-[0.833vw] min-h-dvh">
<div className="w-[3.333vw] flex flex-col justify-between"></div>
<div className="bg-white w-full rounded-[1.667vw] border border-black/5 p-[1.667vw] flex justify-between">
<div className="w-[13.889vw] space-y-[1.667vw]">
@@ -62,9 +52,7 @@ function DashboardPage() {
))}
</div>
</div>
<div className="bg-green-200 w-[13.889vw]">
<Button onClick={handleClickCreateSession}>открыть</Button>
</div>
<div className="bg-green-200 w-[13.889vw]">...</div>
</div>
</div>
);
+11 -8
View File
@@ -10,7 +10,7 @@ import { useNavigate } from "react-router";
function LoginPage() {
const navigate = useNavigate();
const { token, setToken } = useAuthStore();
const { setToken } = useAuthStore();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@@ -28,16 +28,20 @@ function LoginPage() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
let result;
try {
const { token } = await login();
setToken(token);
toast.success("Вы успешно вошли в систему");
navigate("/");
result = await login();
} catch (error) {
toast.error((await (error as HTTPError).response.json<IError>()).error);
}
if (!result?.token) {
return;
}
setToken(result.token);
navigate("/");
}
return (
@@ -45,7 +49,6 @@ function LoginPage() {
<div className="flex-1 flex flex-col gap-4 justify-between w-[380px]">
<div className="flex flex-col items-center justify-between gap-4">
<img src="/logo-mate.svg" alt="logo" />
<p>{token}</p>
</div>
<div>
<form onSubmit={handleSubmit}>
+1 -1
View File
@@ -9,7 +9,7 @@ function ProtectedPage() {
const { data: user, isLoading } = useQuery({
queryKey: ['me'],
queryFn: () => api.get('users/me').json<IUser>(),
queryFn: () => api.get('auth/me').json<IUser>(),
enabled: !!token,
});
+3 -3
View File
@@ -5,7 +5,7 @@ import { IUser } from "./IUser";
export interface ICompany {
id: string;
name: string;
apps: IApp[];
servers: IServer[];
users: IUser[];
apps?: IApp[];
servers?: IServer[];
users?: IUser[];
}
+3 -1
View File
@@ -1,4 +1,5 @@
import { ISession } from './ISession';
import { IApp } from "./IApp";
import { ISession } from "./ISession";
export interface IServer {
id: string;
@@ -7,4 +8,5 @@ export interface IServer {
location: string;
companyId: string;
sessions?: ISession[];
apps?: IApp[];
}
+1 -1
View File
@@ -5,5 +5,5 @@ export interface IUser {
email: string;
fullname: string;
companyId: string;
company: ICompany
company?: ICompany;
}