upd
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { clsx as cn } from "clsx";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
variant?: "link" | "primary" | "secondary" | "tertiary";
|
||||
className?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
onlyIcon?: boolean;
|
||||
}
|
||||
|
||||
function Button({
|
||||
children,
|
||||
variant = "primary",
|
||||
size = "medium",
|
||||
onlyIcon,
|
||||
className,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={cn(
|
||||
"transition-all rounded-lg flex items-center justify-center",
|
||||
variant !== "link" && [
|
||||
size === "small" && (onlyIcon ? "p-2" : "px-3 py-2 gap-2"),
|
||||
size === "medium" && (onlyIcon ? "p-3.5" : "px-5 py-3.5 gap-3.5"),
|
||||
size === "large" && (onlyIcon ? "p-4" : "px-6 py-4 gap-4"),
|
||||
],
|
||||
variant === "link" && "text-sm text-black/50 w-fit",
|
||||
variant === "primary" && "bg-[#1E1E1E] text-white",
|
||||
variant === "secondary" && "bg-white",
|
||||
variant === "tertiary" && "bg-transparent text-[#767676] hover:bg-black/5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,72 @@
|
||||
import Button from "./Button";
|
||||
import SessionIcon from "./icons/SessionIcon";
|
||||
import Input from "./Input";
|
||||
import useQueryApps from "../queries/useQueryApps.ts";
|
||||
import useQueryServers from "../queries/useQueryServers.ts";
|
||||
import Select from "./Select.tsx";
|
||||
|
||||
export default function CreateSessionModal() {
|
||||
const { data: apps } = useQueryApps();
|
||||
|
||||
const { data: servers } = useQueryServers();
|
||||
|
||||
return (
|
||||
<div className="w-[27.222vw] h-full rounded-[0.833vw] bg-white p-[1.667vw] flex flex-col justify-between gap-[1.111vw]">
|
||||
<div className="gap-y-[1.111vw] flex flex-col justify-between">
|
||||
<div className="space-y-[0.556vw]">
|
||||
<div className="rounded-[0.556vw] w-fit p-[0.833vw] bg-[#2D68F60D]">
|
||||
<div className="min-w-[1.389vw] min-h-[1.389vw] text-[#2D68F6]">
|
||||
<SessionIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[1.389vw]">Создание сеанса</p>
|
||||
<p className="text-[0.833vw] text-black/20">
|
||||
Укажите данные клиента, выберите менеджера и стол
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-black/10" />
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Имя <span className="text-[#C6C6C699]">*</span>
|
||||
</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Input placeholder="Константин" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Номер <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" />
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
{apps && (
|
||||
<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]">
|
||||
<Select
|
||||
options={apps.map(({ id, name }) => ({ id, value: name }))}
|
||||
/>
|
||||
</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]">
|
||||
<p className="text-black font-medium text-[0.972vw]">Отменить</p>
|
||||
</Button>
|
||||
<Button className="bg-[#2D68F6] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]">
|
||||
<p className="text-[0.972vw] font-medium">Запустить сеанс</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import Button from "./Button";
|
||||
import MoreIcon from "./icons/MoreIcon";
|
||||
import api from "../utils/api";
|
||||
import PlusIcon from "./icons/PlusIcon";
|
||||
import { IServer } from "../types/IServer";
|
||||
|
||||
interface IDesktopCardProps {
|
||||
server: IServer;
|
||||
}
|
||||
|
||||
export default function DesktopCard({ server }: IDesktopCardProps) {
|
||||
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: endSession } = useMutation({
|
||||
mutationKey: ["sessions", server.sessions?.[0]?.id],
|
||||
mutationFn: () =>
|
||||
api.put(`sessions/${server.sessions?.[0]?.id}`, {
|
||||
json: { status: "ending" },
|
||||
}),
|
||||
onMutate: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }),
|
||||
});
|
||||
|
||||
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">
|
||||
<div className="space-y-[0.278vw]">
|
||||
<p className="leading-none text-[1.111vw]">{server.name}</p>
|
||||
<p className="opacity-50 leading-none text-[0.694vw]">
|
||||
{server.location}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onlyIcon
|
||||
size="small"
|
||||
className="rounded-[0.278vw] bg-[#F5F5F5]"
|
||||
>
|
||||
<div className="min-w-[0.833vw] min-h-[0.833vw] text-black">
|
||||
<MoreIcon />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-[0.556vw]">
|
||||
<div className="px-[0.556vw] py-[0.278vw] flex gap-[0.278vw] items-center bg-[#108C330D] rounded-[0.278vw] w-fit">
|
||||
{server.sessions?.[0]?.status === "started" && (
|
||||
<div className="rounded-full w-[0.278vw] h-[0.278vw] bg-[#108C33]" />
|
||||
)}
|
||||
<p className="font-medium leading-none text-[#108C33] text-[0.556vw]">
|
||||
{server.sessions?.[0]?.status === "started"
|
||||
? "Сеанс запущен"
|
||||
: "Свободен"}
|
||||
</p>
|
||||
</div>
|
||||
{server.sessions?.[0]?.status === "started" && (
|
||||
<>
|
||||
<p className="leading-none">
|
||||
{server.sessions?.[0]?.client?.fullname}
|
||||
</p>
|
||||
<p className="opacity-40 leading-none">
|
||||
{server.sessions?.[0]?.app?.name}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="opacity-40">0 мин</p>
|
||||
</div>
|
||||
<div className="flex gap-[0.278vw]">
|
||||
{server.sessions?.[0]?.status === "started" ? (
|
||||
<div className="flex items-center gap-[0.278vw]">
|
||||
{/* <Button
|
||||
variant="secondary"
|
||||
className="py-[0.694vw] border rounded-lg border-[#2D68F6] w-full"
|
||||
>
|
||||
<p className="text-center leading-none font-medium text-[#2D68F6]">
|
||||
Подробнее о сеансе
|
||||
</p>
|
||||
</Button> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="py-[0.694vw] border rounded-lg border-[#DC0404] w-full"
|
||||
onClick={() => endSession()}
|
||||
>
|
||||
<p className="text-center leading-none font-medium text-[#DC0404]">
|
||||
Завершить сеанс
|
||||
</p>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="bg-[#798FFF] w-full rounded-[0.556vw]"
|
||||
onClick={() => createSession()}
|
||||
>
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="w-[1.389vw] h-[1.389vw]">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
<p className="font-medium leading-[115%]">Начать сеанс</p>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ChevronDownIcon from "./icons/ChevronDownIcon";
|
||||
import { IUser } from "../types/IUser";
|
||||
import SearchInput from "./SearchInput";
|
||||
|
||||
function Header() {
|
||||
const queryClient = useQueryClient();
|
||||
const me = queryClient.getQueryData<IUser>(["me"]);
|
||||
|
||||
return (
|
||||
<div className="h-[66px] bg-white">
|
||||
<div className="w-[952px] mx-auto flex items-center justify-between h-full">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-[134px]">
|
||||
<img src="/logo-mate.svg" alt="logo" />
|
||||
</div>
|
||||
<SearchInput placeholder="Поиск по клиентам" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="flex flex-col gap-1.5 justify-between h-9">
|
||||
<p className="text-sm leading-none">{me?.fullname}</p>
|
||||
<p className="text-[10px] leading-none text-black/40">
|
||||
Старший менеджер
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-[#767676]">
|
||||
<ChevronDownIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function Input({ label, ...props }: InputProps) {
|
||||
return (
|
||||
<label className="space-y-2">
|
||||
{label && <p className="text-xs text-black/50">{label}</p>}
|
||||
<input
|
||||
{...props}
|
||||
className="bg-white rounded-lg px-5 py-3.5 outline-none ring-1 ring-transparent focus:ring-[#363636] transition-[ring-color] inline-block w-full"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default Input;
|
||||
@@ -0,0 +1,92 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useRef } from "react";
|
||||
import useModalStore from "../stores/useModalStore";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import Button from "./Button";
|
||||
|
||||
const duration = 300;
|
||||
|
||||
function ModalContainer() {
|
||||
const { modal, setModal, isOpen, setIsOpen } = useModalStore();
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function handleResize() {
|
||||
if (!popoverRef.current) return;
|
||||
|
||||
if (divRef.current!.clientHeight > popoverRef.current!.clientHeight) {
|
||||
buttonRef.current!.style.height = `${divRef.current!.clientHeight}px`;
|
||||
} else {
|
||||
buttonRef.current!.style.height = `100%`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key !== "Escape") return;
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => setIsOpen(!!modal), [modal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) return;
|
||||
|
||||
setTimeout(() => {
|
||||
setModal(null);
|
||||
}, duration);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={`h-full transition-opacity duration-300`}
|
||||
>
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="fixed top-0 left-0 w-full h-full bg-black/70 overflow-y-auto flex flex-col items-center justify-center"
|
||||
>
|
||||
<div className="max-h-full">
|
||||
<div ref={divRef} className="p-[0.972vw]">
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className="absolute top-0 left-0 w-full h-full cursor-pointer"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="relative w-full">
|
||||
{modal}
|
||||
<Button
|
||||
onlyIcon
|
||||
className="absolute top-[1.667vw] right-[1.667vw] p-[0.556vw] !rounded-full bg-[#F9F9F9]"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<span className="w-[1.389vw] h-[1.389vw] text-black">
|
||||
<CloseIcon />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalContainer;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useState } from "react";
|
||||
import SearchIcon from "./icons/SearchIcon";
|
||||
|
||||
function SearchInput(props: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
className="outline-none pl-2.5 pr-8 pt-[7px] pb-2 bg-[#F6F6F6] rounded-lg leading-none text-sm"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
{...props}
|
||||
/>
|
||||
{!value && (
|
||||
<div className="text-[#858585] absolute top-0 right-0 p-2">
|
||||
<SearchIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchInput;
|
||||
@@ -0,0 +1,28 @@
|
||||
interface IOption {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ISelectProps<T extends IOption>
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
multiple?: boolean;
|
||||
options: T[];
|
||||
selected?: T;
|
||||
// onClose: () => void;
|
||||
}
|
||||
|
||||
export default function Select<T extends IOption>({
|
||||
multiple,
|
||||
options,
|
||||
selected,
|
||||
onClose,
|
||||
...props
|
||||
}: ISelectProps<T>) {
|
||||
return (
|
||||
<select {...props}>
|
||||
{options.map(({ id, value }) => (
|
||||
<option key={id}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import AddressBookIcon from "./icons/AddressBookIcon";
|
||||
import DisplayIcon from "./icons/DisplayIcon";
|
||||
import HomeIcon from "./icons/HomeIcon";
|
||||
import LibIcon from "./icons/LibIcon";
|
||||
import PeoplesIcon from "./icons/PeopleIcon";
|
||||
import SidebarButton from "./SidebarButton";
|
||||
|
||||
function Sidebar() {
|
||||
return (
|
||||
<div className="space-y-[0.278vw]">
|
||||
<SidebarButton active>
|
||||
<span className="w-[1.111vw] h-[1.111vw]">
|
||||
<HomeIcon />
|
||||
</span>{" "}
|
||||
Главная
|
||||
</SidebarButton>
|
||||
<SidebarButton>
|
||||
<span className="w-[1.111vw] h-[1.111vw]">
|
||||
<DisplayIcon />
|
||||
</span>{" "}
|
||||
Сеансы
|
||||
</SidebarButton>
|
||||
<SidebarButton>
|
||||
<span className="w-[1.111vw] h-[1.111vw]">
|
||||
<PeoplesIcon />
|
||||
</span>{" "}
|
||||
Менеджеры
|
||||
</SidebarButton>
|
||||
<SidebarButton>
|
||||
<span className="w-[1.111vw] h-[1.111vw]">
|
||||
<AddressBookIcon />
|
||||
</span>{" "}
|
||||
Клиенты
|
||||
</SidebarButton>
|
||||
<SidebarButton>
|
||||
<span className="w-[1.111vw] h-[1.111vw]">
|
||||
<LibIcon />
|
||||
</span>{" "}
|
||||
Контент
|
||||
</SidebarButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { clsx as cn } from "clsx";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
variant?: "link" | "primary" | "secondary" | "tertiary";
|
||||
className?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
onlyIcon?: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
function SidebarButton({ children, className, active, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={cn(
|
||||
"transition-all flex items-center gap-[0.833vw] px-[1.111vw] py-[0.833vw] text-[0.972vw] rounded-[0.556vw] hover:bg-[#7B60F3]/10 leading-[115%] font-medium tracking-tight w-full",
|
||||
active ? "bg-[#7B60F3]/10 text-[#7B60F3]" : "text-[#242424]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarButton;
|
||||
@@ -0,0 +1,22 @@
|
||||
function AddressBookIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2.667 11.999v2.666h10.666V1.332H2.667v2.667M2 10h1.333M2 8h1.333M2 6h1.333"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 6.999a1.333 1.333 0 1 0 0-2.667 1.333 1.333 0 0 0 0 2.667Zm2.667 4.668a2.667 2.667 0 1 0-5.334 0"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddressBookIcon;
|
||||
@@ -0,0 +1,21 @@
|
||||
function ChevronDownIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.833 8.333 10 12.5l4.167-4.167"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChevronDownIcon;
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function CloseIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m10 10 4.167-4.166M10.001 10 5.834 5.834M10.001 10l4.166 4.167M10.001 10l-4.167 4.167"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
function CogIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.095 14.39a6.67 6.67 0 0 1-2.899-1.768 2 2 0 0 0-1.727-3.28A6.7 6.7 0 0 1 1.639 6h.028a2 2 0 0 0 1.795-2.883 6.66 6.66 0 0 1 2.755-1.543 2 2 0 0 0 3.566 0 6.66 6.66 0 0 1 2.756 1.543A2 2 0 0 0 14.362 6a6.7 6.7 0 0 1 .17 3.343 2 2 0 0 0-1.727 3.28 6.67 6.67 0 0 1-2.9 1.767 2.001 2.001 0 0 0-3.81 0Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 10.333a2.333 2.333 0 1 0 0-4.666 2.333 2.333 0 0 0 0 4.666Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default CogIcon;
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function DesktopIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.083 2.5H2.917c-.69 0-1.25.56-1.25 1.25v10c0 .69.56 1.25 1.25 1.25h14.166c.69 0 1.25-.56 1.25-1.25v-10c0-.69-.56-1.25-1.25-1.25ZM10 15v2.917m-5.833 0h11.666"
|
||||
opacity={0.8}
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
function DisplayIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.632 3H3.368c-.32 0-.578.26-.578.579V8.21c0 .32.259.58.578.58h9.264a.58.58 0 0 0 .579-.58V3.58a.58.58 0 0 0-.58-.579M3.368 13.999l1.416-1.737h6.304l1.544 1.736M8 8.79V14"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default DisplayIcon;
|
||||
@@ -0,0 +1,14 @@
|
||||
function HomeIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M13.333 6.645v5.022a1 1 0 0 1-1 1H10.6a1 1 0 0 1-1-1v-1.2a1 1 0 0 0-1-1H7.4a1 1 0 0 0-1 1v1.2a1 1 0 0 1-1 1H3.667a1 1 0 0 1-1-1V6.645a1 1 0 0 1 .385-.788l4.333-3.378a1 1 0 0 1 1.23 0l4.333 3.378a1 1 0 0 1 .385.788Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="square"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default HomeIcon;
|
||||
@@ -0,0 +1,14 @@
|
||||
function LibIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M3.688 9.371h2.295c.38 0 .688.308.688.688v2.295c0 .38-.308.689-.688.689H3.688A.69.69 0 0 1 3 12.354V10.06c0-.38.308-.689.688-.689Zm5.829 0h2.295c.38 0 .688.308.688.688v2.295c0 .38-.308.689-.688.689H9.517a.69.69 0 0 1-.689-.689V10.06a.69.69 0 0 1 .689-.689Zm0-5.414h2.295c.38 0 .688.308.688.688V6.94c0 .38-.308.689-.688.689H9.517a.69.69 0 0 1-.689-.689V4.645c0-.38.309-.688.689-.688Zm-5.829 0h2.295c.38 0 .688.308.688.688V6.94c0 .38-.308.689-.688.689H3.688A.69.69 0 0 1 3 6.94V4.645c0-.38.308-.688.688-.688Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibIcon;
|
||||
@@ -0,0 +1,32 @@
|
||||
export default function MoreIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx={1}
|
||||
cy={1}
|
||||
transform="matrix(1 0 0 -1 2.333 9)"
|
||||
fill="currentColor"
|
||||
r={1}
|
||||
/>
|
||||
<circle
|
||||
cx={1}
|
||||
cy={1}
|
||||
r={1}
|
||||
transform="matrix(1 0 0 -1 7 9)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<circle
|
||||
cx={1}
|
||||
cy={1}
|
||||
r={1}
|
||||
transform="matrix(1 0 0 -1 11.667 9)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg> )
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
function PeopleIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6.611 6.889a1.944 1.944 0 1 0 0-3.889 1.944 1.944 0 0 0 0 3.889Zm3.779-3.612a1.943 1.943 0 0 1 0 3.334m-7.945 6.054v.333h8.333v-.333c0-1.245 0-1.867-.242-2.342a2.22 2.22 0 0 0-.972-.971c-.475-.243-1.097-.243-2.342-.243H6c-1.244 0-1.867 0-2.342.243a2.22 2.22 0 0 0-.971.97c-.242.476-.242 1.098-.242 2.343Zm11.11.333v-.333c0-1.245 0-1.867-.242-2.342a2.22 2.22 0 0 0-.971-.971"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinecap="square"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default PeopleIcon;
|
||||
@@ -0,0 +1,17 @@
|
||||
export default function PlusIcon() {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.5 8h4m-4 0V4m0 4v4m0-4h-4"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
function SearchIcon() {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7 12.667A5.667 5.667 0 1 0 7 1.333a5.667 5.667 0 0 0 0 11.334Zm4.074-1.593 2.828 2.828"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.2}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchIcon;
|
||||
@@ -0,0 +1,72 @@
|
||||
import Button from "../Button.tsx";
|
||||
import SessionIcon from "../icons/SessionIcon.tsx";
|
||||
import Input from "../Input.tsx";
|
||||
import useQueryApps from "../../queries/useQueryApps.ts";
|
||||
import useQueryServers from "../../queries/useQueryServers.ts";
|
||||
import Select from "../Select.tsx";
|
||||
import DisplayIcon from "../icons/DisplayIcon.tsx";
|
||||
|
||||
export default function CreateSessionModal() {
|
||||
const { data: apps } = useQueryApps();
|
||||
const { data: servers } = useQueryServers();
|
||||
|
||||
return (
|
||||
<div className="w-[34.375vw] h-full rounded-[0.833vw] bg-white p-[1.667vw] flex flex-col justify-between gap-[1.111vw]">
|
||||
<div className="gap-y-[1.111vw] flex flex-col justify-between">
|
||||
<div className="space-y-[0.556vw]">
|
||||
<div className="p-[0.833vw] ring-1 w-fit rounded-[0.556vw]">
|
||||
<div className="w-[1.389vw] h-[1.389vw]">
|
||||
<DisplayIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[1.389vw]">Создание сеанса</p>
|
||||
<p className="text-[0.833vw] text-black/20">
|
||||
Укажите данные клиента, выберите менеджера и стол
|
||||
</p>
|
||||
</div>
|
||||
<hr className="border-black/10" />
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Имя <span className="text-[#C6C6C699]">*</span>
|
||||
</p>
|
||||
<div className="outline outline-black/10 rounded-[0.556vw] w-[13.889vw]">
|
||||
<Input placeholder="Константин" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[0.972vw]">
|
||||
Номер <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" />
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
{apps && (
|
||||
<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]">
|
||||
<Select
|
||||
options={apps.map(({ id, name }) => ({ id, value: name }))}
|
||||
/>
|
||||
</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]">
|
||||
<p className="text-black font-medium text-[0.972vw]">Отменить</p>
|
||||
</Button>
|
||||
<Button className="bg-[#2D68F6] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]">
|
||||
<p className="text-[0.972vw] font-medium">Запустить сеанс</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap");
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
background-color: #f2f0fe;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router";
|
||||
import routes from "./routes";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import ModalContainer from "./components/ModalContainer";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
<ModalContainer />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { IUser } from "../types/IUser";
|
||||
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>(),
|
||||
});
|
||||
|
||||
const { data: servers } = useQuery({
|
||||
queryKey: ["servers"],
|
||||
queryFn: () => api.get("servers?withLastSession=true").json<IServer[]>(),
|
||||
enabled: !!me,
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
// async function logout() {
|
||||
// return await api.get("auth/logout").json();
|
||||
// }
|
||||
|
||||
// async function handleClickLogout() {
|
||||
// try {
|
||||
// await logout();
|
||||
// setToken(null);
|
||||
// } catch (error) {
|
||||
// toast.error((await (error as HTTPError).response.json<IError>()).error);
|
||||
// }
|
||||
// }
|
||||
|
||||
return (
|
||||
// <div className="space-y-6">
|
||||
// <Header />
|
||||
// <div className="w-[952px] min-w-[952px] mx-auto">
|
||||
// <div className="flex gap-5">
|
||||
// <Sidebar />
|
||||
// <div className="main-content flex-1 bg-white rounded-xl p-8 w-full space-y-5">
|
||||
// {/* Main content */}
|
||||
// {servers?.map((server) => (
|
||||
// <DesktopCard key={server.id} server={server} />
|
||||
// ))}
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
<div className="p-[0.833vw] flex gap-[0.833vw] 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]">
|
||||
<div className="px-[1.111vw] py-[0.556vw]">
|
||||
<img
|
||||
src="/logo-mate.svg"
|
||||
alt="logo"
|
||||
className="w-[4.569vw] h-[1.944vw]"
|
||||
/>
|
||||
</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="border w-[38.889vw]">
|
||||
<div className="space-y-4">
|
||||
{servers?.map((server) => (
|
||||
<DesktopCard key={server.id} server={server} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-green-200 w-[13.889vw]">
|
||||
<Button onClick={() => setModal(<CreateSessionModal />)}>
|
||||
открыть
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,93 @@
|
||||
import toast from "react-hot-toast";
|
||||
import Button from "../components/Button";
|
||||
import Input from "../components/Input";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import api from "../utils/api";
|
||||
import { useState } from "react";
|
||||
import { HTTPError } from "ky";
|
||||
import { IError } from "../types/IError";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const { token, setToken } = useAuthStore();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
async function login() {
|
||||
return await api
|
||||
.post("auth/login", {
|
||||
json: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.json<{ token: string }>();
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const { token } = await login();
|
||||
setToken(token);
|
||||
|
||||
toast.success("Вы успешно вошли в систему");
|
||||
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
toast.error((await (error as HTTPError).response.json<IError>()).error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-dvh flex flex-col items-center p-8 bg-[#F6F6F6]">
|
||||
<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}>
|
||||
<div>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Input
|
||||
label="Пароль"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Button type="button" variant="link">
|
||||
Забыли пароль?
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="submit" className="w-full">
|
||||
Войти
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="button" variant="link" className="mx-auto">
|
||||
Нет данных для входа?
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="h-[30px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Navigate, Outlet } from "react-router";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
function ProtectedPage() {
|
||||
const { token } = useAuthStore();
|
||||
|
||||
return token ? <Outlet /> : <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
export default ProtectedPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IApp } from "../types/IApp.ts";
|
||||
import api from "../utils/api.ts";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function useQueryApps() {
|
||||
return useQuery({
|
||||
queryKey: ["apps"],
|
||||
queryFn: () => api.get("apps").json<IApp[]>(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { IServer } from "../types/IServer.ts";
|
||||
import api from "../utils/api.ts";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function useQueryServers() {
|
||||
return useQuery({
|
||||
queryKey: ["servers"],
|
||||
queryFn: () => api.get("servers").json<IServer[]>(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import DashboardPage from "./pages/DashboardPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import ProtectedPage from "./pages/ProtectedPage";
|
||||
|
||||
export default [
|
||||
{
|
||||
path: "/",
|
||||
element: <ProtectedPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <DashboardPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
// import { IUser } from "../types/IUser";
|
||||
|
||||
interface State {
|
||||
token: string | null;
|
||||
setToken: (token: string | null) => void;
|
||||
// user: IUser | null;
|
||||
// setUser: (user: IUser | null) => void;
|
||||
}
|
||||
|
||||
const useAuthStore = create<State>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
setToken: (token) => set({ token }),
|
||||
// user: null,
|
||||
// setUser: (user) => set({ user }),
|
||||
}),
|
||||
{
|
||||
name: "auth", // name of the item in the storage (must be unique)
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useAuthStore;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { create } from "zustand";
|
||||
import { ReactNode } from "react";
|
||||
import { devtools } from "zustand/middleware";
|
||||
|
||||
interface State {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
modal: ReactNode | null;
|
||||
setModal: (modal: ReactNode | null) => void;
|
||||
}
|
||||
|
||||
const useModalStore = create<State>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (isOpen) => set({ isOpen }),
|
||||
modal: null,
|
||||
setModal: (modal) => set({ modal }),
|
||||
}),
|
||||
{
|
||||
name: "modal",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useModalStore;
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface IApp {
|
||||
id: string;
|
||||
name: string;
|
||||
fileName: string;
|
||||
companyId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IClient {
|
||||
id: string;
|
||||
fullname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
companyId: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface IError {
|
||||
error: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IApp } from "./IApp";
|
||||
import { IClient } from "./IClient";
|
||||
import { ISession } from "./ISession";
|
||||
|
||||
export interface IServer {
|
||||
id: string;
|
||||
hostname: string;
|
||||
name: string;
|
||||
location: string;
|
||||
companyId: string;
|
||||
sessions?: ISession[];
|
||||
client?: IClient;
|
||||
app?: IApp;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IApp } from "./IApp";
|
||||
import { IServer } from "./IServer";
|
||||
import { IUser } from "./IUser";
|
||||
|
||||
export interface ISession {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
serverId: string;
|
||||
clientId: string;
|
||||
companyId: string;
|
||||
status: "starting" | "started" | "restarted" | "ending" | "ended";
|
||||
server: IServer;
|
||||
client: IUser;
|
||||
app: IApp
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface IUser {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import ky from "ky";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
|
||||
const api = ky.create({
|
||||
prefixUrl: import.meta.env.VITE_API_URL,
|
||||
retry: 0,
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(request) => {
|
||||
const token = useAuthStore.getState().token;
|
||||
|
||||
if (token) {
|
||||
request.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export default api;
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user