servers and apps selects in modal
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"@react-router/dev": "^7.3.0",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.67.3",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.7.5",
|
||||
"motion": "^12.5.0",
|
||||
@@ -324,6 +325,8 @@
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg=="],
|
||||
|
||||
"@uidotdev/usehooks": ["@uidotdev/usehooks@2.4.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg=="],
|
||||
|
||||
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.0", "", { "dependencies": { "@swc/core": "^1.10.15" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-T4sHPvS+DIqDP51ifPqa9XIRAz/kIvIi8oXcnOZZgHmMotgmmdxe/DD5tMFlt5nuIRzT0/QuiwmKlH0503Aapw=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@react-router/dev": "^7.3.0",
|
||||
"@tailwindcss/vite": "^4.0.13",
|
||||
"@tanstack/react-query": "^5.67.3",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.7.5",
|
||||
"motion": "^12.5.0",
|
||||
|
||||
+19
-15
@@ -1,36 +1,40 @@
|
||||
import React from "react";
|
||||
import { clsx as cn } from "clsx";
|
||||
import React from 'react';
|
||||
import { clsx as cn } from 'clsx';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
variant?: "link" | "primary" | "secondary" | "tertiary";
|
||||
variant?: 'link' | 'primary' | 'secondary' | 'tertiary';
|
||||
className?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
onlyIcon?: boolean;
|
||||
ref?: React.RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
function Button({
|
||||
children,
|
||||
variant = "primary",
|
||||
size = "medium",
|
||||
variant = 'primary',
|
||||
size = 'medium',
|
||||
onlyIcon,
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
{...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"),
|
||||
'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",
|
||||
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
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,28 +1,85 @@
|
||||
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;
|
||||
onChange: (server: IServer) => void;
|
||||
}
|
||||
|
||||
export default function DesktopSelect({ servers }: Props) {
|
||||
export default function DesktopSelect({ servers, value, onChange }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsOpen(false));
|
||||
|
||||
return (
|
||||
<div>
|
||||
{servers.map(({ id, name, sessions }) => (
|
||||
<div key={id} className="flex gap-[0.278vw] items-center">
|
||||
{sessions && sessions?.length > 0 && (
|
||||
<div
|
||||
className="rounded-full w-[0.417vw] aspect-square"
|
||||
style={{
|
||||
backgroundColor:
|
||||
sessions[sessions.length - 1].status === 'started'
|
||||
? '#EF3C26'
|
||||
: '#108C33',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="text-[0.972vw] leading-[115%]">{name}</p>
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative outline-black/10 outline w-[13.889vw] rounded-[0.556vw]"
|
||||
>
|
||||
<Button
|
||||
onlyIcon
|
||||
variant="tertiary"
|
||||
className="px-[0.833vw] py-[0.417vw] !justify-between w-full"
|
||||
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>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={`w-[1.389vw] transition-transform h-[1.389vw]${
|
||||
isOpen ? ' rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<div className="absolute z-1 top-full w-full bg-white rounded-[0.556vw] outline outline-black/10">
|
||||
{servers.map((server) => (
|
||||
<Button
|
||||
key={server.id}
|
||||
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(server);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-[0.278vw] items-center">
|
||||
<div
|
||||
className="rounded-full w-[0.417vw] aspect-square"
|
||||
style={{
|
||||
backgroundColor:
|
||||
server.sessions &&
|
||||
server.sessions?.length > 0 &&
|
||||
server.sessions[server.sessions.length - 1].status ===
|
||||
'started'
|
||||
? '#EF3C26'
|
||||
: '#108C33',
|
||||
}}
|
||||
/>
|
||||
<p className="text-[0.972vw] leading-[115%]">{server.name}</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+52
-21
@@ -1,28 +1,59 @@
|
||||
interface IOption {
|
||||
id: string;
|
||||
import { 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;
|
||||
}
|
||||
|
||||
interface ISelectProps<T extends IOption>
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
multiple?: boolean;
|
||||
options: T[];
|
||||
selected?: T;
|
||||
// onClose: () => void;
|
||||
}
|
||||
export default function Select({ options, value, onChange }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsOpen(false));
|
||||
|
||||
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>
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative outline-black/10 outline w-[13.889vw] rounded-[0.556vw]"
|
||||
>
|
||||
<Button
|
||||
onlyIcon
|
||||
variant="tertiary"
|
||||
className="px-[0.833vw] py-[0.417vw] !justify-between w-full"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<p className="text-[0.972vw] leading-[115%]">{value}</p>
|
||||
<div
|
||||
className={`w-[1.389vw] transition-transform h-[1.389vw]${
|
||||
isOpen ? ' rotate-180' : ''
|
||||
}`}
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</div>
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<div className="absolute top-full w-full bg-white rounded-[0.556vw] outline outline-black/10">
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option}
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-[0.278vw] items-center">
|
||||
<p className="text-[0.972vw] leading-[115%]">{option}</p>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function ArrowDownIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="m5.834 8.333 4.167 4.167 4.166-4.167"
|
||||
stroke="#767676"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,41 @@
|
||||
import { IServer } from "../../types/IServer.ts";
|
||||
import Button from "../Button.tsx";
|
||||
import Input from "../Input.tsx";
|
||||
import DisplayIcon from "../icons/DisplayIcon.tsx";
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
servers: IServer[];
|
||||
}
|
||||
|
||||
export default function CreateSessionModal({ servers }: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const user = queryClient.getQueryData<IUser>(['me']);
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<IServer | undefined>(
|
||||
servers.find(
|
||||
({ sessions }) =>
|
||||
!sessions || !sessions.length || sessions[0].status === 'ended'
|
||||
)
|
||||
);
|
||||
|
||||
const [selectedApp, setSelectedApp] = useState<IApp | undefined>(
|
||||
user?.company.apps.filter((app) => app.serverId === selectedServer?.id)[0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedApp(
|
||||
user?.company.apps.filter((app) => app.serverId === selectedServer?.id)[0]
|
||||
);
|
||||
}, [selectedServer, user?.company.apps]);
|
||||
|
||||
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]">
|
||||
<div className="gap-y-[1.111vw] flex flex-col justify-between">
|
||||
@@ -45,13 +73,48 @@ export default function CreateSessionModal({ servers }: Props) {
|
||||
<Input placeholder="sample@mail.ru" type="email" />
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[0.694vw] text-black/30 w-[13.889vw] leading-[115%] self-end">
|
||||
При запуске нового сеанса текущий будет завершен принудительно.
|
||||
</p>
|
||||
</div>
|
||||
{user && 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)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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]">
|
||||
<Button className="bg-[#EF3C26] px-[2.222vw] py-[0.556vw] !rounded-[0.556vw]">
|
||||
<p className="text-[0.972vw] font-medium">Запустить сеанс</p>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Navigate, Outlet } from "react-router";
|
||||
import useAuthStore from "../stores/useAuthStore";
|
||||
import api from "../utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { IUser } from "../types/IUser";
|
||||
import { Navigate, Outlet } from 'react-router';
|
||||
import useAuthStore from '../stores/useAuthStore';
|
||||
import api from '../utils/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { IUser } from '../types/IUser';
|
||||
|
||||
function ProtectedPage() {
|
||||
const { token } = useAuthStore();
|
||||
|
||||
const { data: user, isLoading } = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: () => api.get("auth/me").json<IUser>(),
|
||||
queryKey: ['me'],
|
||||
queryFn: () => api.get('users/me').json<IUser>(),
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export interface IApp {
|
||||
companyId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { IApp } from "./IApp";
|
||||
import { IServer } from "./IServer";
|
||||
import { IUser } from "./IUser";
|
||||
|
||||
export interface ICompany {
|
||||
id: string;
|
||||
name: string;
|
||||
apps: IApp[];
|
||||
servers: IServer[];
|
||||
users: IUser[];
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { IApp } from "./IApp";
|
||||
import { IClient } from "./IClient";
|
||||
import { ISession } from "./ISession";
|
||||
import { ISession } from './ISession';
|
||||
|
||||
export interface IServer {
|
||||
id: string;
|
||||
@@ -9,6 +7,4 @@ export interface IServer {
|
||||
location: string;
|
||||
companyId: string;
|
||||
sessions?: ISession[];
|
||||
client?: IClient;
|
||||
app?: IApp;
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
import { ICompany } from "./ICompany";
|
||||
|
||||
export interface IUser {
|
||||
id: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
|
||||
companyId: string;
|
||||
company: ICompany
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user